Rubyの文字列クリーンアップ、使い方次第で最大13倍速くなる
コードを書くとき、私たちは無意識のうちに「一番慣れているメソッド」を選びがちです。目の前の文字列を整形したいと思った瞬間、指が勝手にいつものメソッドを打ち込んでしまう——そんな経験はないでしょうか。
自動的に選ばれるのは、たいてい最も汎用的なRubyのメソッドです。読む機会も書く機会も他より多いためです。例えば#gsubは、文字列内の文字を置換するための万能メソッドですが、Rubyには標準的な操作のための、より特化した便利なメソッドが数多く用意されています。
Rubyの豊かなイディオムが好きな理由は、コードがよりエレガントになり、読みやすくなるからです。この豊かさの恩恵を受けるには、文字列のクリーンアップのような単純な処理でさえリファクタリングの時間をかけ、自分の「語彙」を広げる努力が必要になります。問題は、その追加の努力に見合う価値があるのかどうかです。
空白を削除する4つの方法
ここにクレジットカード番号を表す文字列 "055 444 285" があります。これを扱うために、空白を取り除きたいとしましょう。#gsubでも実現できます。#gsubは「何でも何にでも置き換えられる」万能ツールです。しかし、他にも選択肢があります。
string = "055 444 285"
string.gsub(/ /, '')
string.gsub(' ', '')
string.tr(' ', '')
string.delete(' ')
# => "055444285"専用メソッドで私が一番気に入っているのは、その表現力です。最後の例はその好例で、「空白を削除する(delete spaces)」以上に分かりやすいコードはありません。選択肢のトレードオフを考えるとき、私はまず可読性を優先します。ただし、パフォーマンス上の問題を引き起こす場合は話が別です。では、私のお気に入りの#deleteは、実際どれほどの代償を払うことになるのでしょうか。
上記の例をベンチマークで計測してみました。どのメソッドが最速だと思いますか?
Benchmark.ips do |x|
x.config(time: 30, warmup: 2)
x.report('gsub') { string.gsub(/ /, '') }
x.report('gsub, no regex') { string.gsub(' ', '') }
x.report('tr') { string.tr(' ','') }
x.report('delete') { string.delete(' ') }
x.compare!
end速い順に並べ替えて予想してみてください。答えはトグルを開くと表示されます
Comparison:
delete: 2326817.5 i/s
tr: 2121629.8 i/s - 1.10x slower
gsub, no regex: 868184.1 i/s - 2.68x slower
gsub: 474970.5 i/s - 4.90x slower
順位自体は意外ではありませんでしたが、速度差には驚かされました。#gsubは遅いだけでなく、引数を読み手が「解読」するための余分な負担も発生します。次に、空白以外もまとめてクリーンアップする場合に、この比較がどうなるか見てみましょう。
数字だけを取り出す
次の電話番号 '(408) 974-2414' を例にします。必要なのは数字だけ、つまり 4089742414 だとしましょう。#scanも候補に加えました。不要なものをすべて削除するのではなく、欲しいものだけを狙って取り出す、という意図がより明確に表せるからです。
Benchmark.ips do |x|
x.config(time: 30, warmup: 2)
x.report('gsub') { string.gsub(/[^0-9]/, '') }
x.report('tr') { string.tr("^0-9", "") }
x.report('delete_chars') { string.delete("^0-9") }
x.report('scan') { string.scan(/[0-9]/).join }
x.compare!
endこちらも順位を予想してから、トグルを開いて答えを確認してください
Comparison:
delete_chars: 2006750.8 i/s
tr: 1856429.0 i/s - 1.08x slower
gsub: 523174.7 i/s - 3.84x slower
scan: 227717.4 i/s - 8.81x slower
正規表現を使うと遅くなるのは驚くことではありません。#scanの「意図を明確に示す表現力」には、それ相応のコストがかかるということです。しかし、Rubyの専用メソッドがクリーンアップをどう処理するのかを見ているうちに、さらなる検証がしたくなりました。
通貨記号を取り除く
今度は、文字列 "€ 300" から部分文字列 "€ " を取り除く方法をいくつか試してみます。以下の解法の中には、正確に "€ " を指定するものもあれば、通貨記号や非数値文字をすべて削除するものもあります。
Benchmark.ips do |x|
x.config(time: 30, warmup: 2)
x.report('delete specific chars') { string.delete("€ ") }
x.report('delete non-numericals') { string.delete("^0-9") }
x.report('delete prefix') { string.delete_prefix("€ ") }
x.report('delete prefix, strip') { string.delete_prefix("€").strip }
x.report('gsub') { string.gsub(/€ /, '') }
x.report('gsub-non-nums') { string.gsub(/[^0-9]/, '') }
x.report('tr') { string.tr("€ ", "") }
x.report('slice array') { string.chars.slice(2..-1).join }
x.report('split') { string.split.last }
x.report('scan nums') { string.scan(/\d/).join }
x.compare!
end勝者は#delete系のどれかだと予想するでしょうし、実際その通りです。しかし、どの#delete系が最速だと思いますか?さらに、#delete系以外のメソッドの中に、一部の#deleteより速いものが紛れています。それはどれでしょうか?
予想してからトグルを開いてください
Comparison:
delete prefix: 4236218.6 i/s
delete prefix, strip: 3116439.6 i/s - 1.36x slower
split: 2139602.2 i/s - 1.98x slower
delete non-numericals: 1949754.0 i/s - 2.17x slower
delete specific chars: 1045651.9 i/s - 4.05x slower
tr: 951352.0 i/s - 4.45x slower
slice array: 681196.2 i/s - 6.22x slower
gsub: 548588.3 i/s - 7.72x slower
gsub-non-nums: 489744.8 i/s - 8.65x slower
scan nums: 418978.8 i/s - 10.11x slower
配列をスライスするような操作でさえ#gsubより速かったのは意外でしたし、#splitの速さにはいつも感心させられます。そして注目すべきは、特定の部分文字列を削除するよりも、非数値をすべて削除する方が速いという点です。
末尾の通貨単位を取り除く
続いて、数値の後ろにある通貨単位を取り除きます(より遅い#gsubのバリエーションは省略しました)。
Benchmark.ips do |x|
x.config(time: 30, warmup: 2)
x.report('gsub') { string.gsub(/ USD/, '') }
x.report('tr') { string.tr(" USD", "") }
x.report('delete_chars') { string.delete("^0-9") }
x.report('delete_suffix') { string.delete_suffix(" USD") }
x.report('to_i.to_s') { string.to_i.to_s }
x.report('split') { string.split.first }
x.compare!
end今回は勝者が2つ並び、引き分けになりました。どの2つが最速を争うと思いますか?
さらに、#gsubがここでどれほど遅いかも予想してみましょう
Comparison:
delete_suffix: 4354205.4 i/s
to_i.to_s: 4307614.6 i/s - same-ish: difference falls within error
split: 2870187.8 i/s - 1.52x slower
delete_chars: 1989566.1 i/s - 2.19x slower
tr: 1853957.1 i/s - 2.35x slower
gsub: 524080.6 i/s - 13.22x slower
常にニーズに合った専用メソッドが存在するとは限りません。先頭の「0」を保持する必要があるなら#to_iは使えませんし、#delete_suffixは「通貨は米ドルである」という前提に大きく依存しています。
専用メソッドは精密工具のようなもので、特定のコンテキストにおける特定のタスクに適しています。だからこそ、#gsubこそがまさに求めているものになるケースは必ず存在します。汎用性が高く、常に真っ先に思い浮かぶ存在です。しかし、コードとしてはやや理解しづらく、しばしば遅くなりがちです——私の予想を超えて遅いこともあります。私にとって、Rubyの豊かさこそが開発を楽しくしてくれる理由の一つであり、速度面でのメリットは嬉しいおまけのようなものです。
-
Rubyの文字列フォーマット入門:%演算子と書式指定子の使い方
この記事では、Rubyで文字列をフォーマットする方法について詳しく解説します。 なぜ文字列のフォーマットが必要なのでしょうか?例えば、10未満の数値にも先頭にゼロを付けて表示したい場合(01、02、03など)、あるいはコンソール出力を見やすいように桁を揃えて整形したい場合などが挙げられます。 他の言語ではprintf関数を使って文字列をフォーマットできます。C言語を使ったことがある人なら馴染み深いでしょう。printfでは、書式指定子(フォーマット指定子)のリストと、変数や値のリストを定義して使用します。 Rubyでの文字列フォーマットの基本 Rubyでもsprintfメソッドが利用可能ですが
-
Javaで文字列を比較する方法|equals()とequalsIgnoreCase()の使い分けを徹底解説
Javaで文字列の等価性を比較する際は、Stringクラスが提供するequalsメソッドまたはequalsIgnoreCaseメソッドを使用するのが基本です。この記事では、それぞれのメソッドの使い方に加えて、==演算子で文字列を比較してはいけない理由についても詳しく解説します。 equals()メソッドで文字列を比較する 2つの文字列を比較し、大文字と小文字の違いまで厳密に判定したい場合はequals()メソッドを使用します。 次のコード例では、2つのStringインスタンスが大文字小文字を含むすべての文字において一致しているかどうかを判定しています。 public class Compare