PerlでExcelアドレスA1タイプの列表記を扱う
2024/06/01
ExcelのA1アドレス表記はややこしい
  • Excelのアドレス表記には2種類ありますが、通常はA1表記(列=英文字, 行=数字)を使っているでしょう。

  • VBAだとCellsのAddressプロパティを使って列/行の10進値とA1表記を変換するのであまり困っていないと思います。ですが、Perlでやる場合はどうでしょう...この変換規則を自分で作らなければなりません

  • 行は1から始まる数値なので良いとして、Aから始まる列アドレス、これが厄介です。最初は「26進数として考えれば良いかな...」と思ったりもしますが、次を見ると「あっ...違う...」と気付かされます。
    • 列26 : Z
    • 列27 : AA

  • 26進数で扱えた場合、'A'は'0'又は'1'といった特定の数字と関連付けられるはずですが、桁上りの段階で'00'又は'11'になることはあり得ません。つまり26進数としては扱えないのです。

  • これに気付いた時「あれ? これ...どういう変換規則?」と悩むことになりました。今回は悩んだ結果をレポートします。方法は3種類用意しました。下記がまとめです。
  • 手法 1〜16384
    変換時間[sec]
    コメント
    ExcelをOLEで利用 014.03 OLEでExcelを使用する場合は選択できる
    Aから順番に力技変換 107.32 アドレス変換が数回なら使えるが
    検討した変換ルール適用 000.11 今回の推奨方法

方法1: 餅は餅屋: Excel自身に変換してもらう
  • 現行Excelの[仕様]では、列数の最大は16384です。ざっくり3桁の英文字で表せることになります。この後変換規則を考えるにしても、正解がわからないと検証できないので、まずはExcel自身に変換してもらうことにします。

  • OLEでPerlからExcelを使用している場合は正確かつ有効な方法だと考えています。適当なワークシートの1行目Cellで列番号を指定し、そのA1表記アドレスをAddressプロパティからもらうというVBAと同じ方法です。
  • #!perl -w
    use Win32::OLE;
    use strict;
    
    {
      # OLEでExcel起動
        my $excel = Win32::OLE->new("Excel.Application");
      
      # Bookオブジェクト作成
        my $book =$excel->Workbooks->Add();
      
      # 列1から最大の16384までA1表記アドレスを取得
        for (my $col_dec=1; $col_dec<=16384; $col_dec++) {
        
          # Cell(1行目,指定列)のAddressプロパティを取得
            my $addr_A1 = $book->Worksheets(1)->Cells(1,$col_dec)->{Address};
            
          # 戻り値は '$A$1' なので列表記部を抽出
            if ($addr_A1 =~ /\$(\w+)\$/) {
                my $col_A1 = $1; # A1タイプ列表記抽出
                print "$col_dec\t$col_A1\n";
            }
        }
      
      # Excelクローズ
        $excel->Quit();
    }
    
  • この方法で作成した列番号(10進値)と英数字アドレス表記対応正解データが[test0_address.zip]です。
  • 1	A
    2	B
    3	C
    ...中略...
    16383	XFC
    16384	XFD
  • Excelを実行できるPCならこれも良いですが、内部的にExcelを実行するので立ち上がりに時間を要します。それにExcelと同じレンジを扱っているからと言っても、利用しているアプリケーションがExcelとは限らない(Calcとか)ので、その場合上記の方法は使えません。

方法2: 力こそパワー: Aから順番に変換する
  • ここからはExcelの力を借りずに、自分で列の10進数値をA1表記に変換する方法となります。尚1〜16384の変換結果が、方法1の出力と一致していることは確認済みです。以降説明する方法全て確認しています。

  • さて、変換規則はわからなくても、アドレス間変化ルールはわかりますね。Zならば上位の桁を上げ、自分をAに戻すというルールです。そこでコンピュータで1(A)から該当列までカウントして順番に文字割当させて(*1)しまいます。

  • カウントは数字で行うため、各桁インデックス値の英文字変換にASCコード化+chr()関数を使っています。変換は正しくできますが、カウントしまくっているので時間かかります。アドレス処理数が多い場合はちょっと使えません。
  • #!perl -w
    use strict;
    
    {
        for (my $idx=1; $idx<=16384; $idx++) {
            print "$idx\t",convert_dec_to_A1($idx),"\n";
        }
    }
    
    sub convert_dec_to_A1 {
        my ($col_dec) = @_;
        
        
      # 各桁のカウントステータス保持配列(列の最大値は16384→最大桁3)
        my @digits = (0,0,0); # 桁0,1,2のカウント値保持
      # 桁上げ判定
        my @carry; 
        
      # 列1から最大の変換対象列までA1表記アドレスを取得
        for (my $col_num=1; $col_num<=$col_dec; $col_num++) {
        
          # 桁上げ判定初期化(最下位桁は常にカウント)
            @carry = (1,0,0);
            
          # 第1桁:Carry判定
            $carry[1]=1 if ($digits[0]==26);
          
          # 第2桁:Carry判定
            $carry[2]=1 if (($digits[1]==26) and ($carry[1]>0));
            
          # 各桁のカウントアップ
            for (my $i=0; $i<@digits; $i++) {
                $digits[$i]++ if ($carry[$i]>0);
                $digits[$i]=1 if ($digits[$i]>26); # 26(Z)を超えたら'A'に戻す
            }
        }
        
      # カウント終了後数値を文字表記に変換する。1をAとすれば、AのASCコードが65なので
      # オフセットとして64を足し、chr関数で文字変換
        my $col_A1 = '';
        for (my $i=0; $i<@digits; $i++) {
            if ($digits[$i] > 0) { # 各桁の数値が0より大きければ文字変換して結合
                $col_A1 = chr($digits[$i]+64).$col_A1;
            }
        }
        
      # 変換結果を戻す
        return($col_A1);
    }
    

方法3: ようやく真面目に: 各桁独立の変換
  • とりあえず「やっつけ」たところで、ようやく考える余裕がでてきました。恐らくですが下記ルールのようです。
    1. 各n桁(n=0,1,2, ...)は、26^n 単位のカウント値を示している
    2. ただし下位桁の最低値以下は、上位桁のカウントに入らない

  • ルール1だけなら普通の26進数ですが、ルール2で変わってきます。アドレス初期値1の影響ですね。

  • この想定ルールに基づいて作ったのが下記です。全列アドレス(1〜16384)は正しく変換されました。
  • #!perl -w
    use strict;
    
    {
        for (my $idx=1; $idx<=16384; $idx++) {
            print "$idx\t",convert_dec_to_A1($idx),"\n";
        }
    }
    
    sub convert_dec_to_A1 {
        my ($col_dec) = @_;
        
      # 各桁の値初期化
        my @digits = (0,0,0); # 計算値保持
        
      # 第0桁
      # - 26除算した剰余
      #   - 0のときは26とする
        $digits[0] = $col_dec % 26;
        $digits[0] = 26 if ($digits[0] < 1);
        
      # 第1桁
      # - 第0桁の開始値(26**0)を引いてから26除算の商
      #   - 商が26を超えた場合は
      #     - 更に26除算した剰余
      #       - 0のときは26とする
        $digits[1] = int(($col_dec - 26**0) / 26**1);
        if ($digits[1] > 26) {
            $digits[1] = $digits[1] % 26;
            $digits[1] = 26 if ($digits[1] < 1);
        }
        
      # 第2桁
      # - 第1桁の開始値(27=26**0 + 26**1)を引いてから26**2除算の商
      #   - 商が26を超えた場合は
      #     - 更に26除算した剰余
      #       - 0のときは26とする
        $digits[2] = int(($col_dec - (26**0 + 26**1)) / (26**2));
        if ($digits[2] > 26) {
            $digits[2] = $digits[2] % 26;
            $digits[2] = 26 if ($digits[2] < 1);
        }
        
      # 数値を文字表記に変換する。1をAとすれば、AのASCコードが65なので
      # オフセットとして64を足し、chr関数で文字変換
        my $col_A1 = '';
        for (my $i=0; $i<@digits; $i++) {
            if ($digits[$i] > 0) { # 各桁の数値が0より大きければ文字変換して結合
                $col_A1 = chr($digits[$i]+64).$col_A1;
            }
        }
        
      # 変換結果を戻す
        return($col_A1);
    }
    
  • 条件や考え方は上記のコードを見るのが把握しやすいと思います。あとは記述の類似性をまとめ、更に桁数対応を汎用化したのが次のコードになります。

  • 第-1桁を仮想的に入れるのでわかりにくいかなと思っています。何をしているのかわかりにくいコードはメンテナンスで困るため避けた方が良いかもです。
  • #!perl -w
    use strict;
    
    {
        for (my $idx=1; $idx<=16384; $idx++) {
            print "$idx\t",convert_dec_to_A1($idx),"\n";
        }
    }
    
    sub convert_dec_to_A1 {
        my ($col_dec) = @_;
        
      # 各桁の値初期化
        my $digits = []; # 計算値保持
        my $init   = 0;  # 各桁の計算オフセット初期値
        my $div    = 0;  # 除算分母初期値
        
      # 引数値の最大桁見積もり
        my $maxdigit = 0;
        $maxdigit++ while($col_dec/(26**$maxdigit) > 26);
        
      # 各桁の計算
        for (my $i=0; $i<=$maxdigit; $i++) {
            $div = 26**$i;      # 計算桁除算分母
            $digits->[$i] = 0;  # 計算桁初期値
            $init += int(26**($i-1));
            $digits->[$i] = int(($col_dec - $init) / $div);
            if ($digits->[$i] > 26) {
                $digits->[$i] = $digits->[$i] % 26;
                $digits->[$i] = 26 if ($digits->[$i] < 1);
            }
        }
        
      # 数値を文字表記に変換する。1をAとすれば、AのASCコードが65なので
      # オフセットとして64を足し、chr関数で文字変換
        my $col_A1 = '';
        for (my $i=0; $i<@{$digits}; $i++) {
            if ($digits->[$i] > 0) { # 各桁の数値が0より大きければ文字変換して結合
                $col_A1 = chr($digits->[$i]+64).$col_A1;
            }
        }
        
      # 変換結果を戻す
        return($col_A1);
    }
    
  • これでどうにかできると思っていますが、大体こうやって考えた後に、今まで見つけられなかった「正しい考え方」が急に見つかったりするのは「あるある」です...。
Notes
  • 悩んでいる時間が取れない場合には良く使う手です。やっつければいいと思うよ。
2024/06/02: 表記及び変数名修正、実行時間追記
2024/06/01: 初版
Copyright(C) 2024 Altmo
本HPについて