範囲オブジェクトじゃなくてString#succ
文字列についても範囲オブジェクトは生成可能で、("ab".."bc")などとした場合、それが繰り上がりのある精妙なルールに基づいて行われているらしいことに昨日気がついた。それで少し調べてみた。
これは範囲オブジェクトの話とは直接関係がなく、String#succが何をやっているかという問題だ。手元にあったRuby 1.9のソースコードで、string.cの中を見てみた(使っているのはRuby 1.8.6だけど)。該当するのは以下の箇所。コードの中身を読むまでもなく、ちゃんと実例とともに規則がドキュメントされていた。String#succのルールではalphanumericは別扱いとなっていた。
実例を見て思い出した。Perlですごく便利だと思ったのは「<<198>>」のような文字列をインクリメントしたときに、「<<199>>」となってくれることだった。これこそまさに人間が望んでいる動作だ。
irb(main):088:0> ("<<1>>".."<<5>>").to_a => ["<<1>>", "<<2>>", "<<3>>", "<<4>>", "<<5>>"]
うおー、えぐい。
RubyのString#succのルールは、一番右側にあるalphanumericをインクリメントして、随時繰り上げ処理を行うというものだった。文字列にalphanumericが含まれていないときには、nonalphanumericについて「character set's collating sequece」にしたがってインクリメントする、とある。うーん、この表現が具体的に意味するのは何だろうか。「enc_succ_char(s, l, enc);」あたりが関係しそうな気がする。そしてencとあるのは、文字列オブジェクトごとに付与されたエンコーディング指定かしら? それってもしかして1.8じゃなくて、1.9固有のものかな? というのは、また時間があったら調べよう。
/* * call-seq: * str.succ => new_str * str.next => new_str * * Returns the successor to <i>str</i>. The successor is calculated by * incrementing characters starting from the rightmost alphanumeric (or * the rightmost character if there are no alphanumerics) in the * string. Incrementing a digit always results in another digit, and * incrementing a letter results in another letter of the same case. * Incrementing nonalphanumerics uses the underlying character set's * collating sequence. * * If the increment generates a ``carry,'' the character to the left of * it is incremented. This process repeats until there is no carry, * adding an additional character if necessary. * * "abcd".succ #=> "abce" * "THX1138".succ #=> "THX1139" * "<<koala>>".succ #=> "<<koalb>>" * "1999zzz".succ #=> "2000aaa" * "ZZZ9999".succ #=> "AAAA0000" * "***".succ #=> "**+" */ VALUE rb_str_succ(VALUE orig) { rb_encoding *enc; VALUE str; char *sbeg, *s, *e, *last_alnum = 0; int c = -1; long l; char carry[ONIGENC_CODE_TO_MBC_MAXLEN] = "\1"; int carry_pos = 0, carry_len = 1; enum neighbor_char neighbor = NEIGHBOR_FOUND; str = rb_str_new5(orig, RSTRING_PTR(orig), RSTRING_LEN(orig)); rb_enc_cr_str_copy_for_substr(str, orig); OBJ_INFECT(str, orig); if (RSTRING_LEN(str) == 0) return str; enc = STR_ENC_GET(orig); sbeg = RSTRING_PTR(str); s = e = sbeg + RSTRING_LEN(str); while ((s = rb_enc_prev_char(sbeg, s, enc)) != 0) { if (neighbor == NEIGHBOR_NOT_CHAR && last_alnum) { if (ISALPHA(*last_alnum) ? ISDIGIT(*s) : ISDIGIT(*last_alnum) ? ISALPHA(*s) : 0) { s = last_alnum; break; } } if ((l = rb_enc_precise_mbclen(s, e, enc)) <= 0) continue; neighbor = enc_succ_alnum_char(s, l, enc, carry); switch (neighbor) { case NEIGHBOR_NOT_CHAR: continue; case NEIGHBOR_FOUND: return str; case NEIGHBOR_WRAPPED: last_alnum = s; break; } c = 1; carry_pos = s - sbeg; carry_len = l; } if (c == -1) { /* str contains no alnum */ s = e; while ((s = rb_enc_prev_char(sbeg, s, enc)) != 0) { enum neighbor_char neighbor; if ((l = rb_enc_precise_mbclen(s, e, enc)) <= 0) continue; neighbor = enc_succ_char(s, l, enc); if (neighbor == NEIGHBOR_FOUND) return str; if (rb_enc_precise_mbclen(s, s+l, enc) != l) { /* wrapped to \0...\0. search next valid char. */ enc_succ_char(s, l, enc); } if (!rb_enc_asciicompat(enc)) { MEMCPY(carry, s, char, l); carry_len = l; } carry_pos = s - sbeg; } } RESIZE_CAPA(str, RSTRING_LEN(str) + carry_len); s = RSTRING_PTR(str) + carry_pos; memmove(s + carry_len, s, RSTRING_LEN(str) - carry_pos); memmove(s, carry, carry_len); STR_SET_LEN(str, RSTRING_LEN(str) + carry_len); RSTRING_PTR(str)[RSTRING_LEN(str)] = '\0'; rb_enc_str_coderange(str); return str; } /* * call-seq: * str.succ! => str * str.next! => str * * Equivalent to <code>String#succ</code>, but modifies the receiver in * place. */ static VALUE rb_str_succ_bang(VALUE str) { rb_str_shared_replace(str, rb_str_succ(str)); return str; }
少し前にRubyの勉強をはじめてから、処理系のソースコードというのを何となく眺めるようになった。処理系といっても、メソッドの処理ぐらいだと案外ふつうに読めるプログラムなんだなということが1つの発見。頻繁に参照される構造体がどうなっているのかすらよく分かってないけど、変数名や関数名が分かりやすいから、何となく何をやっているかぐらいは見当がつく。
上のコードをみて思ったのは、桁の繰り上がり処理で文字列のメモリ領域をリサイズしたりしているところが、やたらと面倒そうだなということ。こういうのを勝手にやってくれるところが抽象度の高い言語の楽ちんさなのだなと改めて思った。
もう1つ、ちょっと驚いたのはString#succに対応する破壊的なメソッドのString#succ!の関数定義が、たった1行しかないということ。当たり前といえば当たり前だけど、こういうのって見たことなかったから新鮮だった。