irb(main):037:0> ("\x00" .. "\x7f").to_a.size=> 128irb(main):038:0> ("\x00" .. "\x80").to_a.size=> 58なんで減るん??バグってない??
という呟きがあって。たしかに実行してみるとそうなる。
irb(main):001:0> ("\x00" .. "\x7f").to_a.size => 128 irb(main):002:0> ("\x00" .. "\x80").to_a.size => 58
RangeでASCII文字列を指定すると128種類のものが出てくるのは分かる。けど"\x80"までにするとなんで58に?
irb(main):003:0> ("\x00" .. "\x80").to_a => ["\u0000", "\u0001", "\u0002", "\u0003", "\u0004", "\u0005", "\u0006", "\a", "\b", "\t", "\n", "\v", "\f", "\r", "\u000E", "\u000F", "\u0010", "\u0011", "\u0012", "\u0013", "\u0014", "\u0015", "\u0016", "\u0017", "\u0018", "\u0019", "\u001A", "\e", "\u001C", "\u001D", "\u001E", "\u001F", " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
どうやら"9"までで止まってしまい、その次にくるはずの":"から先が無くなっている模様。何故…?
と思ってソースコードとかを追いかけてみた。
Rangeのto_a
はEnumerableのもので、これはRangeに定義されたeachを回しているに違いない、ということで見てみると range.cにrange_each
という関数がある。
https://github.com/ruby/ruby/blob/ruby_2_2/range.c#L768
ここの中では始点と終点の型によって処理が幾つかに分かれているらしい。fixnumやsymbolの場合は特殊な処理をするようになっているらしいが、文字列が始点にある場合に通る行には
rb_block_call(tmp, rb_intern("upto"), 2, args, each_i, 0);
という行がある。どうやらString.upto
を使って列挙していくようになっているようだ。
確かにRange使わずにString.upto
でも同じような現象が起きている。
irb(main):004:0> "\x00".upto("\x80").to_a => ["\u0000", "\u0001", "\u0002", "\u0003", "\u0004", "\u0005", "\u0006", "\a", "\b", "\t", "\n", "\v", "\f", "\r", "\u000E", "\u000F", "\u0010", "\u0011", "\u0012", "\u0013", "\u0014", "\u0015", "\u0016", "\u0017", "\u0018", "\u0019", "\u001A", "\e", "\u001C", "\u001D", "\u001E", "\u001F", " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
で、string.cのrb_str_upto
という関数を見てみると…
https://github.com/ruby/ruby/blob/ruby_2_2/string.c#L3485
/* single character */ if (RSTRING_LEN(beg) == 1 && RSTRING_LEN(end) == 1 && ascii) { ... } /* both edges are all digits */ if (ascii && ISDIGIT(RSTRING_PTR(beg)[0]) && ISDIGIT(RSTRING_PTR(end)[0])) { ... } /* normal case */ no_digits: ...
という具合に、(1)「始点と終点がともに1文字のASCII文字の場合」という場合と(2)「始点と終点が数字(と見なすことのできる文字列)の場合」という場合と (3)それ以外の場合、と分かれている。
なので"\x00".."\x7F"
の場合は(1)の分岐に入るし、"\x00".."\x80"
だと(3)の分岐に入ることになる。
(1)の中では単純にascii codeの値がインクリメントされていくが、(3)の中では次の値を.succ
によって決定されているようだった。
ここで、String.succ
の動きは やはり文字列がアルファベットか数字か、など様々な条件で異なる。
instance method String#next (Ruby 2.2.0)
数字でもアルファベットでもないascii codeの場合は(1)のようにインクリメントするのと変わらないが、数字(と見なせる文字列)の場合は
irb(main):005:0> "9".succ => "10"
と、"9"の次は":"でなく"10"となる。これが終点の文字列より長くなってしまっているためにupto
のループがここで終わってしまう、ということのようだ。
irb(main):006:0> "\x40".upto("\x80").to_a => ["@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"] irb(main):007:0> "\x50".upto("\x80").to_a => ["P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"] irb(main):008:0> "\x60".upto("\x80").to_a => ["`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] irb(main):009:0> "\x70".upto("\x80").to_a => ["p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] irb(main):010:0> "\x20".upto("\x80\x80").to_a => [" ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99"]
なるほど、"\x40"や"\x50"から開始すると"Z"の次が"AA"になるからここで止まるし、"\x60"や"\x70"から開始すると"z"の次が"aa"になるからここで止まる。
終点の文字列を2文字にすれば、"\x20"(" ")から"\x39"("9")まではascii codeで増えたあと、そこから"10"にsuccされた後"99"までは数字として増える不思議な配列が出来たりする。
面白い。
@sonots 不思議な挙動ではあるけれど、バグかというとそうではない、という感じ…?
2015-10-16 18:50:08 via Twitter for Mac to @sonots
@sugyan んー、でも "9" まで列挙されるのは期待している姿じゃないし、どうせなら空配列になってくれたほうがまだうれしい気持ちはある。
2015-10-16 18:52:33 via Echofon to @sugyan
@sonots String.uptoの終端条件が変われば空配列になるようにはなるのかな…。そのへんは言語デザインの話になってくるんですかねぇ。「.succで次の文字(列)を返す」っていうのが出来てしまうのがスゴいことだからw それをどこまで続けられるか定義するのは難しそう
2015-10-16 18:59:11 via Twitter for Mac to @sonots