From: akr@... Date: 2014-09-12T14:21:50+00:00 Subject: [ruby-dev:48538] [ruby-trunk - Feature #9816] 文字列内の数字を数値として比較するメソッド Issue #9816 has been updated by Akira Tanaka. Yui NARUSE wrote: > 熟考の結果、Gem::Versionと仕様をあわせました。 > 理由は、 > * Gem::Versionでこれを使ってくれればオブジェクトの生成数が減る > * 2.2.0-preview1のようなRubyのバージョンの比較ができる > からです。 > > 順序のイメージは Prereleases sort between real releases (newest to oldest) のような感じです > > 1. 1.0 > 2. 1.0.b1 > 3. 1.0.a.2 > 4. 0.9 よくわからなかったので試してみたのですが、 Ruby のバージョンの比較ができるというのは、 preview, rc, patch release の順に並ぶという意味ではないですよね? (そうしたいという話ではありません。) ~~~ % cat t.rb puts %w[ 1.9.2-preview1 1.9.2-preview3 1.9.2-rc1 1.9.2-rc2 1.9.2-p0 1.9.2-p136 1.9.2-p180 1.9.2-p290 1.9.2-p320 1.9.2-p330 ].sort {|a, b| a.versioncmp(b) } % ./ruby t.rb 1.9.2-p0 1.9.2-p136 1.9.2-p180 1.9.2-p290 1.9.2-p320 1.9.2-p330 1.9.2-preview1 1.9.2-preview3 1.9.2-rc1 1.9.2-rc2 ~~~ 思うのですが、挙動の説明が足りない気がします。 ---------------------------------------- Feature #9816: 文字列内の数字を数値として比較するメソッド https://bugs.ruby-lang.org/issues/9816#change-48875 * 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/