Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Ruby 2.4 の Unicode サポートを徹底テスト──「最小驚きの原則」から見る文字列操作の落とし穴

Ruby 2.4 で導入された新機能の一つに、改善された Unicode サポートがあります。具体的には、upcasedowncase といったメソッドが期待通りに動作し、「ä」を「Ä」に、そして元に戻せるようになりました。この変更をきっかけに、2013 年に André Arko 氏がブログ記事「Strings in Ruby are UTF-8 now… right??」を公開して以来、どの程度 Unicode 対応が進んだのか気になり、調査を行いました。

技術的なバグを探すのではなく、「最小驚きの原則(Principle of Least Surprise)」の観点から、Ruby のすべての文字列メソッドをテストしました。具体的には、以下の 5 つの前提を満たすかどうかを検証しています。

  1. 異なる文字は異なるものとして扱われる:「e」と「ë」は、「e」と「E」が異なるのと同様に、別の文字として扱われるべきです。
  2. 単一の文字は単一の文字として数えられる: Unicode でどのように表現されていようと、一つの文字は一つの文字です。「e」も「ë」もそれぞれ 1 文字ですが、後者は 2 つのコードポイントで構成されます。
  3. 文字は不変である: 文字列を反転させても、個々の文字が変化してはなりません。
  4. 空白は空白として扱われる: トリッキーな Unicode 空白文字であっても、空白として処理されるべきです。
  5. 数字は数字として扱われる: 書き方に関わらず、数字の 2 は常に 2 として認識されるべきです。

残念ながら、Ruby の文字列操作メソッドの大半はこれらのテストをパスしません。Unicode 文字列を扱う際は、どのメソッドを使うか極めて慎重になる必要があります。

注記: 公開後、いくつかの読者から「テスト文字列を Unicode 正規化していれば、指摘された失敗の多くは起きなかったはずだ」との指摘を受けました。これは事実です。しかし、Ruby や Rails が自動的に文字列を正規化することはありません(私がテストしたアプリケーションのいずれにおいても)。これらのテストは常に「最悪のケース」を示すことを意図しており、その観点からは依然として有用だと考えています。

Ruby 2.4.0 における Unicode テスト結果

以下の表は、主要な文字列メソッドを対象に、結合文字(例:「ä」=「a」+結合ダイエレシス U+0308)を用いてテストした結果です。「Expected」は「最小驚きの原則」に基づく期待値、「Result」は実際の実行結果、「Verdict」は判定を示します。

メソッド テストコード 期待値 実行結果 判定
#%"%s" % "noël""noël""noël"OK
#*"noël" * 2"noëlnoël""noëlnoël"OK
#<<"noël" << "ë""noëlë""noëlë"OK
#<=>"ä" <=> "z"-1-1OK
#=="ä" == "ä"truetrueOK
#=~"ä" =~ /a./nil0要注意
#[]"ä"[0]"ä""a"要注意
#[]="ä"[0] = "u""u""u"OK
#b"ä".b.encoding.to_s"ASCII-8BIT""ASCII-8BIT"OK
#bytes"ä".bytes[97, 204, 136][97, 204, 136]OK
#bytesize"ä".bytesize33OK
#byteslice"ä".byteslice(1)"\xCC""\xCC"OK
#capitalize"ä".capitalize"Ä""Ä"OK
#casecmp"äa".casecmp("äz")-1-1OK
#center"ä".center(3)" ä ""ä "要注意
#chars"ä".chars["ä"]["a", "̈"]要注意
#chomp"ä\n".chomp"ä""ä"OK
#chop"ä".chop"""a"要注意
#chr"ä".chr"ä""a"要注意
#clear"ä".clear""""OK
#codepoints"ä".codepoints[97, 776][97, 776]OK
#concat"ä".concat("x")"äx""äx"OK
#count"ä".count("a")01要注意
#crypt"123".crypt("ää") == "123".crypt("aa")falsefalseOK
#delete"ä".delete("a")"ä""̈"要注意
#downcase"Ä".downcase"ä""ä"OK
#dump"ä".dump"\"a\\u0308\"""\"a\\u0308\""OK
#each_byte"ä".each_byte.to_a[97, 204, 136][97, 204, 136]OK
#each_char"ä".each_char.to_a["ä"]["a", "̈"]要注意
#each_codepoint"ä".each_codepoint.to_a[97, 776][97, 776]OK
#each_line"ä".each_line.to_a["ä"]["ä"]OK
#empty?"ä".empty?falsefalseOK
#encode"ä".encode("ASCII", undef: :replace)"a?""a?"OK
#encoding"ä".encoding.to_s"UTF-8""UTF-8"OK
#end_with?"ä".end_with?("ä")truetrueOK
#eql?"ä".eql?("a")falsefalseOK
#force_encoding"ä".force_encoding("ASCII")"a\xCC\x88""a\xCC\x88"OK
#getbyte"ä".getbyte(2)136136OK
#gsub"ä".gsub("a", "x")"ä""ẍ"要注意
#hash"ä".hash == "a".hashfalsefalseOK
#include?"ä".include?("a")falsetrue要注意
#index"ä".index("a")nil0要注意
#replace"ä".replace("u")"u""u"OK
#insert"ä".insert(1, "u")"äu""aü"要注意
#inspect"ä".inspect"\"ä\"""\"ä\""OK
#intern"ä".internOK
#length"ä".length12要注意
#ljust"ä".ljust(3, "_")"ä__""ä_"要注意
#lstrip" ä".lstrip"ä""ä"OK
#match"ä".match("a")nil#<MatchData "a">要注意
#next"ä".next"ä""b̈"要注意
#ord"ä".ord9797OK
#partition"händ".partition("a")["händ"]["h", "a", "̈nd"]要注意
#prepend"ä".prepend("ä")"ää""ää"OK
#replace (2)"ä".replace("ẍ")"ẍ""ẍ"OK
#reverse"händ".reverse"dnäh""dn̈ah"要注意
#rpartition"händ".rpartition("a")["händ"]["h", "a", "̈nd"]要注意
#rstrip"line ".rstrip"line""line "要注意
#scrub"ä".scrub"ä""ä"OK
#setbytes = "ä"; s.setbyte(0, "x".ord); s"ẍ""ẍ"OK
#size"ä".size12要注意
#slice"ä".slice(0)"ä""a"要注意
#split"ä".split("a")["ä"]["", "̈"]要注意
#squeeze"ää".squeeze("ä")"ä""ää"要注意
#start_with?"ä".start_with?("a")falsetrue要注意
#strip" line ".strip"line"" line "要注意
#sub"ä".sub("a", "x")"ä""ẍ"要注意
#succ"ä".succ"b̈""b̈"OK
#swapcase"ä".swapcase"Ä""Ä"OK
#to_c"١".to_c(1+0i)(0+0i)要注意
#to_f"١".to_f1.00.0要注意
#to_i"١".to_i10要注意
#to_r"١".to_r(1/1)(0/1)要注意
#to_sym"ä".to_sym:äOK
#tr"ä".tr("a", "b")"ä""b̈"要注意
#unpack"ä".unpack("CCC")[97, 204, 136][97, 204, 136]OK
#upto"ä".upto("c̈").to_a["ä", "b̈", "c̈"]["ä", "b̈", "c̈"]OK
#valid_encoding?"ä".valid_encoding?truetrueOK

まとめと対策

テスト結果からわかる通り、Ruby 2.4 で upcase/downcase などのケース変換が改善された一方で、文字単位で動作すべきメソッドの多くが依然としてコードポイント単位で動作し、「最小驚きの原則」を満たしていません。特に charseach_charlength/sizereversegsub/subsplit など、日常的に使うメソッドが「要注意」判定となっています。

推奨される対策

  • 文字列を NFC/NFD 正規化する: String#unicode_normalize(Ruby 2.2+)を用いて、処理の前後に正規化を行う。
  • グレム(書記素クラスタ)単位で操作する: String#grapheme_clusters(Ruby 3.2+)や、unicode-display_widthunicode-grapheme-cluster-break などの gem を活用する。
  • 正規表現では \X を使う: 書記素クラスタにマッチする \X を用いると、結合文字列を一つの単位として扱えます。
  • 数値変換は注意深く: to_i 等は ASCII 数字のみ対応。全角数字やアラビア・インディック数字(例:「١」)には対応していません。Unicode::Number 等のライブラリを検討してください。

Unicode を正しく扱うには、「文字列=バイト列」でも「文字列=コードポイント列」でもなく、「文字列=書記素クラスタの列」として捉え、適切なライブラリや正規化を併用することが不可欠です。Ruby の進化に期待しつつ、現時点では慎重な実装を心がけましょう。

  1. 25 のベスト API テスト ツール

    クラウド アプリと関連するプラットフォームの数が増えるにつれて、API テストはますます重要になっています。基本的に、API (アプリケーション プログラミング インターフェース) は、2 つのソフトウェア プログラムがやり取りしてデータを共有できるようにするメッセンジャーです。 .存在しないため、フロント エンドでは実行できない一種のソフトウェア テストです。この記事では、最高の無料 API テスト ツールをいくつか調べました。それでは、読み続けてください! 25 のベスト API テスト ツール API がソフトウェア開発の重要な側面になるにつれて、API テストはますます開発者と

  2. 17 の最高のモバイル テスト ツール

    モバイル領域は急速に拡大しています。モバイル アプリケーションは、ビデオからモバイル バンキングまで、さまざまな形式で利用できるようになりました。莫大な収益性があるため、多くの企業がモバイル アプリ業界での存在感を確立しようとしています。アプリを作成するためのモバイル テストに最適なシミュレーターをお探しですか?見つけるお手伝いをいたします。この投稿では、Android と iOS のテスト範囲、効率、精度を向上させるのに役立つ最適なモバイル テスト ツールを見ていきます。 17 の最高のモバイル アプリ テスト ツール モバイル アプリの開発中に考慮すべきさまざまなプラットフォームと