String.uptoは引数によって挙動が違う

という呟きがあって。たしかに実行してみるとそうなる。

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"までは数字として増える不思議な配列が出来たりする。

面白い。