Ruby で文字列を比較して、差分を強調表示させる

# やりたい事

二つの入力を比較して、マッチしていない部分を強調して表示させたい。
例えば、「AbcDefGh」と「abeffh」という入力があったら、以下のようなイメージで表示させたい。

![image](https://qiita-image-store.s3.amazonaws.com/0/41416/ef465b0a-6cf9-729b-b09b-c3d50cdfe698.png)

これでパッと見で差分がわかるようにする！

# `diff-lcs` で文字列を比較する

どうやって差分とろうかなあと考えていたら、[diff-lcs](https://github.com/halostatue/diff-lcs) で簡単に出来た。

```
gem 'diff-lcs'
```

インストールして、コンソールで実行してみる。

```rb
Diff::LCS.sdiff('AbcDefGh', 'abefffh')

=> [["!", [0, "A"], [0, "a"]],
 ["=", [1, "b"], [1, "b"]],
 ["-", [2, "c"], [2, nil]],
 ["-", [3, "D"], [2, nil]],
 ["=", [4, "e"], [2, "e"]],
 ["=", [5, "f"], [3, "f"]],
 ["!", [6, "G"], [4, "f"]],
 ["+", [7, nil], [5, "f"]],
 ["=", [7, "h"], [6, "h"]]]
```

んー便利。

ただの多次元配列に見えるけど、要素一つ一つが `Diff::LCS::ContextChange` のオブジェクトになっている。

```rb
Diff::LCS.sdiff('AbcDefGh', 'abefffh').first
#=> ["!", [0, "A"], [0, "a"]]

Diff::LCS.sdiff('AbcDefGh', 'abefffh').first.class
#=> Diff::LCS::ContextChange
```

最初の `!` は `Diff::LCS` 内で `action` と呼ばれていて、アンマッチを表している。他にも `=` はマッチ、`+` は追加、`-` は削除を表している。
また、`0` は `Diff::LCS` では `position`、`A` と `a` は `element` と呼ばれていて、文字列の位置と文字（値）を表している。

これらは、アクセッサが用意されていて、

```rb
Diff::LCS.sdiff('AbcDefGh', 'abefffh').first.action
#=> "!"
Diff::LCS.sdiff('AbcDefGh', 'abefffh').first.old_position
#=> 0
Diff::LCS.sdiff('AbcDefGh', 'abefffh').first.new_element
#=> "a"
```

こんな感じで取り出せる。
構成は `[action, [old_position, old_element], [new_position, new_element]]` となっている。

# 差分がある文字だけ取り出す

便利な便利な `Diff::LCS#sdiff` には、ブロックも渡せるみたい。

```rb
Diff::LCS.sdiff('AbcDefGh', 'abefffh') {|context_change| context_change.old_element if context_change.action != '=' }
#=> ["A", nil, "c", "D", nil, nil, "G", nil, nil]
```

おー取り出せた。
`Diff::LCS::Change#unchanged?` が用意されているので、

```rb
Diff::LCS.sdiff('AbcDefGh', 'abefffh') {|context_change| context_change.old_element unless context_change.unchanged? }
#=> ["A", nil, "c", "D", nil, nil, "G", nil, nil]
```

これで OK

# HTML ファイルを出力させる

もう出来そうな雰囲気がしてきたので、見た目が

![image](https://qiita-image-store.s3.amazonaws.com/0/41416/ef465b0a-6cf9-729b-b09b-c3d50cdfe698.png)

となるような HTML ファイルを出力する Ruby スクリプトを作る。

```rb
require 'diff-lcs'

str1, str2 = *ARGV

def decorate_diff(str1, str2)
  Diff::LCS.sdiff(str1, str2) {|context_change|
    if context_change.unchanged?
      context_change.old_element
    elsif !context_change.old_element.nil?
     "<span>#{context_change.old_element}</span>"
    end
  }.join('')
end

puts <<EOS
<html>
  <head>
    <style type="text/css">
      span {
        font-weight: bold;
        color: crimson;
      }
    </style>
  </head>
  <body>
    <ol>
      <li>#{decorate_diff(str1, str2)}</li>
      <li>#{decorate_diff(str2, str1)}</li>
    <ol>
  </body>
</html>
EOS
```

んー出来た！
ただ、これだけだとちょっと差分が見づらいので、片方にしかない文字を取り消し線で消すようにすると、、、

```rb
...
def decorate_diff(str1, str2)
  Diff::LCS.sdiff(str1, str2) {|context_change|
    if context_change.unchanged?
      context_change.old_element
    elsif context_change.deleting?
      "<span class='delete'>#{context_change.old_element}</span>"
    elsif !context_change.adding?
     "<span>#{context_change.old_element}</span>"
    end
  }.join('')
end

...
    <style type="text/css">
      span.delete {
        text-decoration: line-through;
      }
...
```

![image](https://qiita-image-store.s3.amazonaws.com/0/41416/4d8b4c53-82dd-1915-c3fa-7353d6e8916d.png)

出来た！！
もうちょっと見た目は工夫しないとだけど、、、当初の目的は果たせた！！