エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

いかにして表計算の列名をつくるか

「列名って、なに?」

これです。このA, B, ..., Z, AA, AB, ... という文字列です。

仕事でちょっとゴルフネタ的ななにかになったので、記録しておきます。

みんな大好き、直積

列名は最初は1桁の A〜Z の26個、次に2桁の AA〜ZZ の676個、次に3桁の AAA〜ZZZ の17,576個、…と続きます。

AA〜ZZ の並びは A〜Z と A〜Z との直積で得られます。Ruby では product メソッドを使います。
AAA〜ZZZ の並びも同様に AA〜ZZ と A〜Z との直積で求めることができます。
これをふまえて。文字列の配列を一挙に作ってしまおうという作戦。


実装。

AtoZ = [*?A..?Z]
column_names = AtoZ + AtoZ.product(AtoZ).map(&:join) + AtoZ.product(AtoZ).product(AtoZ).map(&:join)

column_names[200] # => "GS"


3桁あれば18,278列までまかなえるので充分なのですが、この数を配列で用意しておくと、メモリ効率がよくない感じ。


桁上がりで1文字戻す

特定の番号の列名が欲しいとき、上記のように配列に用意しておいた値を引くという手もありますが、できれば算出してしまいたいもの。
26文字を使うので一瞬26進数のように錯覚したのですが、そうではありません。
これは 0〜9 の文字を使うとわかりやすくなると思います。

0, 1, ..., 8, 9, 00, 01, 02, ..., 98, 99, 000, ...

つまり

1桁のばあい: 0〜9
2桁のばあい: 00〜99
3桁のばあい: 000〜999

という並びになることになります。
9の次が10でなく00になるようにするためには、桁上がりがおこったときに、桁上がりした桁の値を1小さくしてやる必要があります。


実装。

def column_names(n)
  result = ''
  begin
    result = (n % 26 + 65).chr + result
    n = n / 26 - 1
  end while n >= 0
  result
end

回さない方法はないのか

ループでぐるぐる回すのがRubyっぽくないと、その後丸1日ぐらい考え続けていました。

もう一度、値と桁数の関係を考えてみると、

0〜25: 1桁の26進数
26〜701: 2桁の26進数
701〜18,277: 3桁の26進数

という関係になっていることがわかります。

つまり、たとえば200であれば2桁になるので、26を引いて26進数変換すればもとめる文字列になりそうです。

irb> (200 - 26).to_s(26)
=> "6i"

はい、なりません。
これは考え方がまずいのではなく、使う文字の問題です。
各桁の値を表す文字が 0〜9 および a〜z となっているため、7番目の文字が G ではなく 6 に、18番目の文字が S でなく i になるためです。
ですので文字 A で下駄を履かせることで期待する文字が得られることになります。

irb> ((200 - 26).to_s(26)[0].to_i(26) + 'A'.ord).chr
=> "G"
irb> ((200 - 26).to_s(26)[1].to_i(26) + 'A'.ord).chr
=> "S"

桁ごとに下駄を履かせるのは面倒なので、26進数で文字列にしたものを一旦36進数として解釈し、各桁の下駄の値を足し合わせた値を加えて36進数として文字列に変換します。
最後に、列名は大文字で欲しいのでupcaseメソッドで大文字にしています。

irb> ((200 - 26).to_s(26).to_i(36) + 'AA'.to_i(36)).to_s(36).upcase
=> "GS"

そうすると最後に残るのが桁数の取得です。

1桁で表せる列名の数: 26
2桁以内で表せる列名の数: 1桁で表せる列名の数 + 2桁で表せる列名の数 = 26 + 262 = 702
3桁以内で表せる列名の数: 1桁で表せる列名の数 + 2桁で表せる列名の数 + 3桁で表せる列名の数 = 26 + 262 + 263 = 18,278
途中省略
n桁以内で表せる列名の数: 26 * (26n - 1) / (26 - 1)

ここから式を変形して、列の番号 m から次の式で桁数 n を取得することができるようになります。

floor(log26(25 * m + 26))

ここで floor は床関数です。


これらをふまえて。実装。

def column_names(n)
  c = Math.log(25 * n + 26, 26).floor
  ((n - 26 * (26**(c - 1) - 1) / 25).to_s(26).to_i(36) + ('A' * c).to_i(36)).to_s(36).upcase
end


26進数変換と36進数変換を3回やっていて効率が悪そうに見えますが…気にしてはいけません。

それ succ でできるよ

Ruby には「次の値」を返すsuccという便利なメソッドがあります。

1.succ # => 2 : 数値 1 の次の値
'1'.succ # => '2' : 文字列 '1' の次の値
'A'.succ # => 'B' : 文字列 'A' の次の値

三村さんに指摘されて知ったのですが、'Z'の次の値というのが:

'Z'.succ # => 'AA' : 文字列 'Z' の次の値
'ZZ'.succ # => 'AAA' : 文字列 'ZZ' の次の値


200番目の列名を取得したいときは200回succすれば求めたい値になります。

irb> 200.times.inject('A') {|c| c.succ }
=> "GS"

名前を求めるたびに何回もsuccするのは効率が悪そうですが、普通は列名は連続して生成するばあいが多いと思います。そのばあい、succで進めるたびに値を取り出すようにすれば、効率もよくなりそうです。





…。


結論

succ メソッドを使うといいと思うよ!