やりたい事
二つの入力を比較して、マッチしていない部分を強調して表示させたい。
例えば、「AbcDefGh」と「abeffh」という入力があったら、以下のようなイメージで表示させたい。
これでパッと見で差分がわかるようにする!
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
と呼ばれていて、アンマッチを表している。他にも =
はマッチ、+
は追加、-
は削除を表している。
また、0
は Diff::LCS
では position
、A
と a
は element
と呼ばれていて、文字列の位置と文字(値)を表している。
これらは、アクセッサが用意されていて、
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 ファイルを出力させる
もう出来そうな雰囲気がしてきたので、見た目が
となるような 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;
}
...
出来た!!
もうちょっと見た目は工夫しないとだけど、、、当初の目的は果たせた!!