From: akr@... Date: 2014-05-08T23:46:46+00:00 Subject: [ruby-dev:48195] [ruby-trunk - Feature #9816] 文字列内の数字を数値として比較するメソッド Issue #9816 has been updated by Akira Tanaka. Yui NARUSE wrote: > なお、メソッド名は String#numericcmp としています。 "2.9" と "2.10" の比較を考えると、 整数の並びの辞書順の比較として 2.9 が大きいとするか、 数 (実数) の比較として 2.10 が大きいとするか、 という考え方の違いがあります。 numericcmp という名前は後者を想起させる気がします。 今回欲しいのは前者だと思うので、それを想起させる名前がいいんじゃないでしょうか。 ではどういう名前にするかというと、あまりいい名前が思いつくわけでもないのですが、intcmp とか? (intarycmp はあまりにナニ) versioncmp は機能じゃなくて (大きな) 用法からの命名といえるのかも。 (あと numericcmp は cc という同じ文字の並びの間で単語が分かれるというのが、 ぱっとみたときに単語分割しにくいように思います。) ---------------------------------------- Feature #9816: 文字列内の数字を数値として比較するメソッド https://bugs.ruby-lang.org/issues/9816#change-46625 * Author: Yui NARUSE * Status: Assigned * Priority: Normal * Assignee: Yukihiro Matsumoto * Category: core * Target version: ---------------------------------------- 文字列内の数字を数値として比較するメソッドを追加しませんか そのような比較は一般的な用途としてはGUIシェルのファイラーが比較に用いており、 Windows では StrCmpLogicalW が、OS X では NSString:compare:options:へのNSNumericSearch定数が提供されています。 http://msdn.microsoft.com/en-us/library/windows/desktop/bb759947(v=vs.85).aspx https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSString_Class/Reference/NSString.html#//apple_ref/c/econst/NSNumericSearch 上記のような処理自体はさほど難しいものではありませんが、Rubyレベルで実装すると大量のオブジェクトを作ってしまいます。 例えば `Gem::Version.new("2.1.10".freeze)<=>Gem::Version.new("2.1.9".freeze)` は47個、 `"2.1.10".freeze.split('.').map(&:to_i)<=>"2.1.9".freeze.split('.').map(&:to_i)` だと16個のオブジェクトを作ります。 `"2.1.10".freeze.numericcmp"2.1.9".freeze` ならば、もちろんオブジェクトは一つも作りません。 なお、上記の例でも示唆していますが、本メソッドは Ruby のバージョン表記の TEENY が2桁になった場合の比較に用いることができます。 パッチは以下の通りです。 なお、メソッド名は String#numericcmp としています。 (String#casecmpを念頭に置いた) ``` diff --git a/string.c b/string.c index c589c80..66f667f 100644 --- a/string.c +++ b/string.c @@ -2569,6 +2569,131 @@ rb_str_casecmp(VALUE str1, VALUE str2) return INT2FIX(-1); } +VALUE +numerical_compare(const char **pp1, const char *p1end, const char **pp2, const char *p2end) +{ + const char *s1 = *pp1, *p1, *s2 = *pp2, *p2; + ptrdiff_t len1, len2; + int r; + + while (s1 < p1end && *s1 == '0') s1++; + p1 = s1; + while (p1 < p1end && ISDIGIT(*p1)) p1++; + len1 = p1 - s1; + + while (s2 < p2end && *s2 == '0') s2++; + p2 = s2; + while (p2 < p2end && ISDIGIT(*p2)) p2++; + len2 = p2 - s2; + + if (len1 != len2) { + return INT2FIX(len1 < len2 ? -1 : 1); + } + + r = memcmp(s1, s2, len1); + if (r) return r < 0 ? INT2FIX(-1) : INT2FIX(1); + + len1 = s1 - *pp1; + len2 = s2 - *pp2; + if (len1 != len2) { + return INT2FIX(len1 < len2 ? -1 : 1); + } + + *pp1 = p1; + *pp2 = p2; + return Qnil; +} + +/* + * call-seq: + * str.numericcmp(other_str) -> -1, 0, +1 or nil + * + * Variant of String#<=>, which considers digits in strings + * are numeric value.. + * + * "a1".numericcmp("a1") #=> 0 + * "aa".numericcmp("a1") #=> 1 + * "a1".numericcmp("aa") #=> -1 + * "a1".numericcmp("a01") #=> -1 + * "2.1.2".numericcmp("2.1.10") #=> 1 + */ + +static VALUE +rb_str_numericcmp(VALUE str1, VALUE str2) +{ + long len; + rb_encoding *enc; + const char *p1, *p1end, *p2, *p2end; + + StringValue(str2); + enc = rb_enc_compatible(str1, str2); + if (!enc) { + return Qnil; + } + + p1 = RSTRING_PTR(str1); p1end = RSTRING_END(str1); + p2 = RSTRING_PTR(str2); p2end = RSTRING_END(str2); + if (single_byte_optimizable(str1) && single_byte_optimizable(str2)) { + while (p1 < p1end && p2 < p2end) { + if (ISDIGIT(*p1)) { + if (ISDIGIT(*p2)) { + VALUE r = numerical_compare(&p1, p1end, &p2, p2end); + if (!NIL_P(r)) return r; + } + else { + return INT2FIX(-1); + } + } + else if (ISDIGIT(*p2)) { + return INT2FIX(1); + } + if (*p1 != *p2) return INT2FIX(*p1 < *p2 ? -1 : 1); + p1++; + p2++; + } + } + else { + while (p1 < p1end && p2 < p2end) { + int l1, c1 = rb_enc_ascget(p1, p1end, &l1, enc); + int l2, c2 = rb_enc_ascget(p2, p2end, &l2, enc); + + if (0 <= c1 && 0 <= c2) { + if (ISDIGIT(*p1)) { + if (ISDIGIT(*p2)) { + VALUE r = numerical_compare(&p1, p1end, &p2, p2end); + if (!NIL_P(r)) return r; + } + else { + return INT2FIX(-1); + } + } + else if (ISDIGIT(*p2)) { + return INT2FIX(1); + } + if (*p1 != *p2) return INT2FIX(*p1 < *p2 ? -1 : 1); + p1++; + p2++; + } + else { + int r; + l1 = rb_enc_mbclen(p1, p1end, enc); + l2 = rb_enc_mbclen(p2, p2end, enc); + len = l1 < l2 ? l1 : l2; + r = memcmp(p1, p2, len); + if (r != 0) + return INT2FIX(r < 0 ? -1 : 1); + if (l1 != l2) + return INT2FIX(l1 < l2 ? -1 : 1); + } + p1 += l1; + p2 += l2; + } + } + if (RSTRING_LEN(str1) == RSTRING_LEN(str2)) return INT2FIX(0); + if (RSTRING_LEN(str1) > RSTRING_LEN(str2)) return INT2FIX(1); + return INT2FIX(-1); +} + static long rb_str_index(VALUE str, VALUE sub, long offset) { @@ -8721,6 +8846,7 @@ Init_String(void) rb_define_method(rb_cString, "eql?", rb_str_eql, 1); rb_define_method(rb_cString, "hash", rb_str_hash_m, 0); rb_define_method(rb_cString, "casecmp", rb_str_casecmp, 1); + rb_define_method(rb_cString, "numericcmp", rb_str_numericcmp, 1); rb_define_method(rb_cString, "+", rb_str_plus, 1); rb_define_method(rb_cString, "*", rb_str_times, 1); rb_define_method(rb_cString, "%", rb_str_format_m, 1); diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index 8366424..f9c788b 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -2104,6 +2104,29 @@ class TestString < Test::Unit::TestCase assert_equal(1, "\u3042B".casecmp("\u3042a")) end + def test_numericcmp + assert_equal(-1, "2.1.0".numericcmp("2.1.1")) + assert_equal(-1, "2.1.9".numericcmp("2.1.10")) + assert_equal( 0, "a1".numericcmp("a1")) + assert_equal( 1, "aa".numericcmp("a1")) + assert_equal(-1, "a1".numericcmp("aa")) + assert_equal(-1, "a1".numericcmp("a01")) + assert_equal(-1, "a0001".numericcmp("a00001")) + assert_equal( 0, "a1a".numericcmp("a1a")) + assert_equal( 1, "a1b".numericcmp("a1a")) + assert_equal(-1, "a9a".numericcmp("a10a")) + assert_equal( 1, "b".numericcmp("a")) + assert_equal( 0, "\u30421".numericcmp("\u30421")) + assert_equal( 1, "\u3042\u3042".numericcmp("\u30421")) + assert_equal(-1, "\u30421".numericcmp("\u3042\u3042")) + assert_equal(-1, "\u30421".numericcmp("\u304201")) + assert_equal(-1, "\u30420001".numericcmp("\u304200001")) + assert_equal( 0, "\u30421\u3042".numericcmp("\u30421\u3042")) + assert_equal( 1, "\u30421\u3044".numericcmp("\u30421\u3042")) + assert_equal(-1, "\u30429\u3042".numericcmp("\u304210\u3042")) + assert_equal( 1, "\u3044".numericcmp("\u3042")) + end + def test_upcase2 assert_equal("\u3042AB", "\u3042aB".upcase) end ``` -- https://bugs.ruby-lang.org/