LoginSignup
35
30

More than 5 years have passed since last update.

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

Posted at

やりたい事

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

image

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

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

どうやって差分とろうかなあと考えていたら、diff-lcs で簡単に出来た。

gem 'diff-lcs'

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

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 のオブジェクトになっている。

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

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

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

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

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 には、ブロックも渡せるみたい。

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? が用意されているので、

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

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

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

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

...
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

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

35
30
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
30