こんにちは。
GMOアドマーケティングのR.Sです。
今回は、業務中に発生した簡単そうに見えて実は面倒だった開発について紹介したいと思います。
Rubyでの配列操作や文字列操作のお話がメインです。
やりたいこと
「改行区切りで入力された空白を含む文字列をソートして、重複しないように並び替える」
どういうことかというと、ユーザーが入力するワードを、
- スペース区切りを1単語、改行区切りを1つの単語の組み合わせとみなして
- 同じ行の中で1単語が重複しないように
- 他の行とは単語の組み合わせが重複しないように
- 良い感じにソートする
という感じです。
今回は下図のような入力と出力にすることを目指します。
受け取るパラメータ
ユーザーの入力は下記のようになります。
1 |
"words"=>"犬\r\n猫\r\n犬猫\r\n犬 猫\r\n猫 犬\r\n犬 猫 犬\r\nうさぎ ハムスター\r\nハムスター うさぎ 鳥 犬\r\n鳥 うさぎ 犬 ハムスター\r\n猫 犬 うさぎ 鳥 ハムスター\r\n" |
空白と改行(\r\n)が混ざった1つの文字列です。wordsという変数に代入して扱っていきます。
パラメータの操作
改行で区切る
まずはパラメータの文字列を改行で区切ります。splitを使います。
文字列.split(“区切り文字“) とすることで文字列を分割することができます。
1 2 3 4 5 6 7 |
# wordsにパラメータを代入 [1] pry(main)> words = "犬\r\n猫\r\n犬猫\r\n犬 猫\r\n猫 犬\r\n犬 猫 犬\r\nうさぎ ハムスター\r\nハムスター うさぎ 鳥 犬\r\n鳥 うさぎ 犬 ハムスター\r\n猫 犬 うさぎ 鳥 ハムスター\r\n" => "犬\r\n猫\r\n犬猫\r\n犬 猫\r\n猫 犬\r\n犬 猫 犬\r\nうさぎ ハムスター\r\nハムスター うさぎ 鳥 犬\r\n鳥 うさぎ 犬 ハムスター\r\n猫 犬 うさぎ 鳥 ハムスター\r\n" # splitを使って改行で区切る [2] pry(main)> words = words.split("\r\n") => ["犬", "猫", "犬猫", "犬 猫", "猫 犬", "犬 猫 犬", "うさぎ ハムスター", "ハムスター うさぎ 鳥 犬", "鳥 うさぎ 犬 ハムスター", "猫 犬 うさぎ 鳥 ハムスター"] |
重複を排除する
ここが1番めんどくさい大変です。
スペース区切りで1単語とみなして重複しないのがゴールなので、ひとまず、”犬 猫 犬”は”犬 猫”もしくは”猫 犬”という単語の組み合わせにしたいところです。
配列に対してuniqを使うと重複した要素を排除することができますが、配列全体に使っても消せないので、配列の要素を1つずつ確認していきます。
が、”犬 猫 犬”という文字列にはuniqは使えませんし、仮に[“犬 猫 犬”]という配列があったとして、それにuniqを使っても変化なしです。
1単語ずつスペースで区切って[“犬”, “猫”, “犬”]のような配列にする処理を挟みます。
文字列を区切るときに使うのはsplitでした。
それを踏まえて出力してみるとこんな感じです。
1 2 3 4 5 6 7 8 9 10 11 12 |
# スペース区切りになるようにmapを使って繰り返す [3] pry(main)> words.map{ |word| word.split(" ")} => [["犬"], ["猫"], ["犬猫"], ["犬", "猫"], ["猫", "犬"], ["犬", "猫", "犬"], ["うさぎ", "ハムスター"], ["ハムスター", "うさぎ", "鳥", "犬"], ["鳥", "うさぎ", "犬", "ハムスター"], ["猫", "犬", "うさぎ", "鳥", "ハムスター"]] |
mapを使って繰り返し処理を行うのは、結果を配列で戻してくれるためです。
先ほど改行区切りの配列にしたwordsに対して、uniqを使って重複を消します。また、後々のために単語をソートしておきます。(重要)
1 2 3 4 5 6 7 8 9 10 11 |
[4] pry(main)> words = words.map{ |word| word.split(" ").uniq.sort} => [["犬"], ["猫"], ["犬猫"], ["犬", "猫"], ["犬", "猫"], ["犬", "猫"], ["うさぎ", "ハムスター"], ["うさぎ", "ハムスター", "犬", "鳥"], ["うさぎ", "ハムスター", "犬", "鳥"], ["うさぎ", "ハムスター", "犬", "猫", "鳥"]] |
これで同じ行にあった重複は消えました。
さらに、単語ごとにソートすることで他の行と重複している単語の組み合わせがあるのがわかります。次はそれを消します。
wordsは既に二重配列になっていて単語もソートされているため、二重配列に対してuniqを使うと単語の組み合わせが1つの要素になっているので要素ごとの重複を消すことができます。
先ほどソートしていない場合は[“犬“, “猫“]と[“猫“, “犬“]のままなので、重複と捉えてくれず、uniqでは消せないまま残ります。(人間が見たら重複ってすぐわかるのに!)
1 2 |
[5] pry(main)> words = words.uniq => [["犬"], ["猫"], ["犬猫"], ["犬", "猫"], ["うさぎ", "ハムスター"], ["うさぎ", "ハムスター", "犬", "鳥"], ["うさぎ", "ハムスター", "犬", "猫", "鳥"]] |
元の形に戻す
ここまでで重複しないようにする要件は満たせました。
元の入力は単語は空白区切りだったのでjoinを使って元に戻します。
1 2 |
[6] pry(main)> words = words.map{ |word| word.join(' ') } => ["犬", "猫", "犬猫", "犬 猫", "うさぎ ハムスター", "うさぎ ハムスター 犬 鳥", "うさぎ ハムスター 犬 猫 鳥"] |
これでゴールに設定していたものにすることができました。
まとめ
今回の例は少し特殊でしたが、文字列に使うメソッドと配列に対して使うメソッドを使い分けて試行錯誤することでなんとかなりました。
人間が入力と出力を見る限りではわりと簡単に見えますが、文字を扱うのは大変ということに気がつきました。
実際に業務中に発生した開発要件はもっと複雑で大変だったので文字列をこねくりまわしたり、正規表現のバリデーションを使ったりしました。
特に、重複排除に関してはもっと良い方法があったかもしれないので、見つけたら直しても良いかも…と思います。
あ〜大変だった!
以上です。