give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

Excel列名変換問題で第2回社内プログラミングコンテストを開催してみた(後編)

はじめに

すいません、なかなかブログを書く時間が無くて前編からかなり間が空いてしまいました。
第2回社内プログラミングコンテストの後編をご紹介します。

前回までのあらすじ

前回までのあらすじは大体以下のような感じです。

  • Excelの列名変換問題で2回目の社内プログラミングコンテストを開催した。
  • アルファベットから数字、数字からアルファベットの2題を出題した。
  • 予想以上に難しく、時間内に2問目(数字からアルファベット)まで解けた人はいなかった。
  • そこで2問目は各自の宿題にして、説明会は後日行うことにした。


詳しくはこちらのエントリをご覧下さい。

Excel列名変換問題で第2回社内プログラミングコンテストを開催してみた(前編) - give IT a try


また、第2問の仕様はこんな感じです。

第2問の仕様
  • 入力された数字をアルファベットに変換する。
    • ただし、問題1で作ったプログラムを拡張すること。
  • 起動時引数
    • [0] 0=数字へ変換、1=アルファベットへ変換
    • [1] 変換する数字またはアルファベット(どちらも上限なし)
  • 実行例
    • ExcelColConv.pl 0 AA → 27
    • ExcelColConv.pl 1 27 → AA
ちょっとした趣向を加えてみた

今回はちょっと説明会に面白い趣向を加えてみました。
これまでは僕は出題者という立場だったので、ランキング投票の対象者に含めていなかったのですが、今回は僕もランキング対象に含めてもらうことにしました。
時間制限なしで答えを見ずに問題を解く、という意味では他のメンバーと同じ条件になるから、というのがその理由です。
これまでは他のメンバーが評価されるのを上から見下ろしていたわけですが、今回は僕も評価されるわけです。
さてさて、このプログラミングコンテストの発起人は見事上位にランクされるのでしょうか?それとも・・・??

動作確認&説明タイム

今回の回答者は全部で12人です。
時間制限なしはなかったので、全員が見事に正解!!・・・と思ったのですが、残念ながら一名だけバグありのプログラムでした。
彼のプログラムでは26の変換結果が「Z」ではなく「AZ」に・・・。う〜ん、残念!
Zの前後はバグが出やすいので要注意なんですよね〜。実は僕も最初はバグってました。はい。。。


他のメンバーの解答を見ると、パッと見た感じの書き方は大きく違っていても、説明を聞くとロジック自体はそれほど大きく変わらないような印象を受けました。
しかし、一部には「えっ?そんなやり方でやったの!?」というような少々風変わりなロジックを組んでたメンバーもいました。


ちなみに今回は時間短縮のため、動作確認用のシェルスクリプト/バッチプログラムも作ってもらいました。
しかし、シェルやバッチを書くぐらいなら、次回からはいっそのことユニットテストも必須にしてしまった方が良いのかも、と思ったりもしました。

投票&開票結果

さて、今回もまた投票でベストプログラマを選出する時間がやってきました。
基本的に負けず嫌いなので、なんとか1位になりたいと思ってたのですが、結果はどうだったんでしょうか・・・??


はい、1位の人は前回、前々回も1位だったAさんでした!見事V3です!
2位も前回、前々回と2位だったMさんでした!このお二方は不動のポジションなのでしょうか??
そして、僕は・・・なんとか3位でした!
ちょっと悔しいですが、なんとかベスト3に食い込めたので、ほっとしました。(でも悔しい・・・)


ちなみに4位は前回3位だったKさんでした。3位の僕とは1点差です。ほんと、ギリギリの3位です。


ただ、前回と前々回はAさんとMさんが3位の人に10点以上の差を付けていたのですが、今回は比較的票が分散していました。
1位:17点、2位:15点、3位:12点、4位:11点、という感じです。
じっくり時間をかけると、みんなのコードの品質はある程度均衡してくるのかもしれません。

後編のまとめ

第1回と第2回前編は時間制限があったので、スピードが重視されました。
一方、今回は時間制限なしだったので、メンバーはロジックをじっくり考えたり、リファクタリングしたりする時間を確保できたはずです。
先ほども述べたように、今回は特定の人に票が偏らず、比較的分散したのは時間制限をなくしたのが原因の一つになってるんじゃないかと思います。


実際の業務ではスピードと質の両方が求められるのは確かですが、コンテストとしては質をじっくり追求できる要素があっても良いのかな〜と思いました。
今回時間制限がなくなったのはたまたま全員が時間内に解答できなかったのが原因でした。しかし、コンテストは時間制限なしにしても面白い、ということが発見できたのは怪我の功名だったかもしれません。

参考資料

今回も上位3人のプログラムを載せておきます。
また、各メンバーに投票した他のメンバーからのコメントも載せておきます。

1位の人が書いたプログラム

最初の配列の定義が行数を食っていますが、メインロジック自体は比較的シンプルです。

my %table = (
        ('A', 1),
        ('B', 2),
        ('C', 3),
        ('D', 4),
        ('E', 5),
        ('F', 6),
        ('G', 7),
        ('H', 8),
        ('I', 9),
        ('J', 10),
        ('K', 11),
        ('L', 12),
        ('M', 13),
        ('N', 14),
        ('O', 15),
        ('P', 16),
        ('Q', 17),
        ('R', 18),
        ('S', 19),
        ('T', 20),
        ('U', 21),
        ('V', 22),
        ('W', 23),
        ('X', 24),
        ('Y', 25),
        ('Z', 26),
);

my @list = (
        'Z',    #
        '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',
);

if ($ARGV[0] eq '0') {          # 自動判定は -> $ARGV[1] =~ /^[A-Z]*$/

    my @input = split(//, $ARGV[1]);
    my $answer = 0;
    my $n;

    for (my $i = 0; $i < ($#input + 1); $i++) {

        $n = $table{$input[$i]};
        $answer = ($answer * 26) + $n;
    }

    print "$answer\n";

}
elsif ($ARGV[0] eq '1') {       # 自動判定は -> $ARGV[1] =~ /^\d*$/

    my $input = $ARGV[1];
    my $spl;
    my @answer;

    while ($input > 0) {

        $spl = ($input % 26);
        $input = int($input / 26);
        unshift(@answer, $list[$spl]);

        # ふつうの n進変換ならこんなコードは要らないが、A = 0 ではなく A = 1 なので
        if ($spl == 0) {
            $input -= 1;
        }
    }

    $" = '';    # ダブルクォートの中で @list を print するときのデフォルト区切り文字を変更
    print "@answer\n";
}
1位の人に投票したメンバーからのコメント
  • シンプル。引数の自動判定ができるのにしなかったところ。
  • 相変わらずシンプル。匠といったところ。時間をかければもっと洗練されるのだろうが、あまり時を間をとっている様に見えない。
  • 解りやすい。やっていることはスマート。だけどかなり精通していないと一回では理解できないかも・・・。
2位の人が書いたプログラム

メインプログラムだけでなく、ユニットテストのコードも付いています。あと、ロジックには再帰を使っています。

# メインプログラム

use strict;
use warnings;

# PBP says to use Readonly instead of use constant but I don't think we have it installed
use constant {
    TO_NUMBER    => 0,
    TO_LETTERS   => 1,
    BASE         => 26,
    ASCII_OFFSET => 64,
    BORDER_CHAR  => "Z",
};

# For testing. Call main if we were run directly from command line
my $result;
$result = main( @ARGV ) unless caller(  );
print $result . "\n";

sub main {
    # Get parameter
    my ($mode, $input) = @_;

    if ($mode == TO_NUMBER) {
        return col_name_to_number($input);
    }
    elsif ($mode == TO_LETTERS) {
        return number_to_col_name($input);
    }
    else {
        die "Invalid mode\n";
    }
}

# q = 1 r = 2 => AB (28)
# q = 1 r = 1 => AA (27)
# q = 1 r = 0 => Z (26)
# q = 2 r = 0 => AZ (52)
# q = 27 r = 0 => ZZ (702)
sub number_to_col_name {
    my ($number) = @_;
    my $quotient  = int($number / BASE);
    my $remainder = $number % BASE;
    # Non-border (Z -> A) case
    if ($quotient && ($remainder != 0)) {
        return number_to_col_name($quotient) . chr($remainder + ASCII_OFFSET);
    }
    # Border case but not end case
    elsif (($quotient > 1) && ($remainder == 0)) {
        return number_to_col_name($quotient - 1) . BORDER_CHAR;
    }
    # Border case and end case
    elsif ($remainder == 0) {
        return BORDER_CHAR;
    }
    # Non-border case and end case
    else {
        return chr($remainder + ASCII_OFFSET);
    }
}

sub col_name_to_number {
    my ($col_name) = @_;
    my $character = substr($col_name, -1, 1);
    $col_name     = substr($col_name, 0, (length($col_name) - 1));
    my $value     = ord($character) - ASCII_OFFSET;
    if (length($col_name) > 0) {
        return $value + (BASE * col_name_to_number($col_name));
    }
    else {
        return $value;
    }
}
# ユニットテスト

use strict;
use warnings;

use Test::More qw/no_plan/;

require_ok("ExcelColConv.pl");

my $result;
$result = main(0, "A");
is($result, 1, "0 A = 1");

$result = main(0, "B");
is($result, 2, "0 B = 2");

$result = main(0, "Y");
is($result, 25, "0 Y = 25");

$result = main(0, "Z");
is($result, 26, "0 Z = 26");

$result = main(0, "AA");
is($result, 27, "0 AA = 27");

$result = main(0, "AB");
is($result, 28, "0 AB = 28");

$result = main(0, "AZ");
is($result, 52, "0 AZ = 52");

$result = main(0, "BA");
is($result, 53, "0 BA = 53");

$result = main(0, "BB");
is($result, 54, "0 BB = 54");

$result = main(0, "ZY");
is($result, 701, "0 ZY = 701");

$result = main(0, "ZZ");
is($result, 702, "0 ZZ = 702");

$result = main(0, "AAA");
is($result, 703, "0 AAA = 703");

$result = main(0, "AAB");
is($result, 704, "0 AAB = 704");

$result = main(0, "XFD");
is($result, 16384, "0 XFD = 16384");

$result = main(0, "ABCDE");
is($result, 494265, "0 ABCDE = 494265");

#-------------

# 1 1 = A
$result = main(1, 1);
is($result, "A", "1 1 = A");
# 1 2 = B
$result = main(1, 2);
is($result, "B", "1 2 = B");
# 1 25 = Y
$result = main(1, 25);
is($result, "Y", "1 25 = Y");
# 1 26 = Z
$result = main(1, 26);
is($result, "Z", "1 26 = Z");
# 1 27 = AA
$result = main(1, 27);
is($result, "AA", "1 27 = AA");
# 1 28 = AB
$result = main(1, 28);
is($result, "AB", "1 28 = AB");
# 1 52 = AZ
$result = main(1, 52);
is($result, "AZ", "1 52 = AZ");
# 1 53 = BA
$result = main(1, 53);
is($result, "BA", "1 53 = BA");
# 1 54 = BB
$result = main(1, 54);
is($result, "BB", "1 54 = BB");
# 1 701 = ZY
$result = main(1, 701);
is($result, "ZY", "1 701 = ZY");
# 1 702 = ZZ
$result = main(1, 702);
is($result, "ZZ", "1 702 = ZZ");
# 1 703 = AAA
$result = main(1, 703);
is($result, "AAA", "1 703 = AAA");
# 1 704 = AAB
$result = main(1, 704);
is($result, "AAB", "1 704 = AAB");
# 1 16384 = XFD
$result = main(1, 16384);
is($result, "XFD", "1 16384 = XFD");
# 1 494265 = ABCDE
$result = main(1, 494265);
is($result, "ABCDE", "1 494265 = ABCDE");
2位の人に投票したメンバーからのコメント
  • シンプルなコードの中に技(再帰法)を含んでいてバランスが良いと思う。
  • まだ完全には理解できていませんが、再帰やCONSTANT等いろいろとチャレンジしているので評価したい。
  • Test::Moreを使用したユニットテスト。リカーシブのロジック。
3位の人が書いたプログラム

3位の人=僕です。出題者なのに1位になれませんでした(ToT)。
僕もユニットテストを書いています。再帰を使って実装しているのも2位の人と同じです。
あと、Perl::CriticやPerl::Tidyを使ってフォーマットの整形やベストプラクティスの導入を図っています。

# メインプログラム

use strict;
use warnings;
use ExcelUtil;

my $TO_NUMBER = 0;

main(@ARGV);

sub main {
    my @args = @_;
    my $mode = $args[0];
    my $val  = $args[1];
    my $ans;
    if ( $mode == $TO_NUMBER ) {
        $ans = ExcelUtil::to_col_number($val);
    }
    else {
        $ans = ExcelUtil::to_col_string($val);
    }
    print "$ans\n";

    return 1;
}
# 変換用モジュール

package ExcelUtil;
use strict;
use warnings;
use base qw( Exporter );
our @EXPORT_OK = qw( to_col_number to_col_string );

my $AZ_LENGTH  = ord('Z') - ord('A') + 1;    #26
my $OFFSET_NUM = ord('A') - 1;               #64

sub to_col_number {
    my ($col_str) = @_;
    my @chars = split //sm, $col_str;
    return convert_str( 0, \@chars );
}

sub convert_str {
    my ( $ret, $chars_ref ) = @_;

    if ( scalar( @{$chars_ref} ) == 0 ) {
        return $ret;
    }

    my $c            = shift @{$chars_ref};
    my $chars_length = scalar @{$chars_ref};
    return convert_str( calc_decimal( $c, $chars_length ) + $ret, $chars_ref );
}

sub calc_decimal {
    my ( $c, $times ) = @_;
    return $AZ_LENGTH**$times * ( ord($c) - $OFFSET_NUM );
}

sub to_col_string {
    my ($n) = @_;
    return convert_num( q{}, $n );
}

sub convert_num {
    my ( $ret, $n ) = @_;
    if ( $n == 0 ) {
        return $ret;
    }
    else {
        my ( $quo, $rem ) = excel_divmod($n);
        return convert_num( chr( $rem + $OFFSET_NUM ) . $ret, $quo );
    }
}

sub excel_divmod {
    my ($n) = @_;
    my ( $quo, $rem ) = ( int( $n / $AZ_LENGTH ), $n % $AZ_LENGTH );
    if ( $rem == 0 ) {
        return $quo - 1, $AZ_LENGTH;
    }
    else {
        return $quo, $rem;
    }
}

1;
# ユニットテスト

use strict;
use warnings;

use Test::More 'no_plan';

my @subs = qw(to_col_number to_col_string);

use_ok( 'ExcelUtil', @subs );
can_ok( __PACKAGE__, 'to_col_number' );
can_ok( __PACKAGE__, 'to_col_string' );

is( to_col_number('A'),   1,      'A' );
is( to_col_number('B'),   2,      'B' );
is( to_col_number('C'),   3,      'C' );
is( to_col_number('Y'),   25,     'Y' );
is( to_col_number('Z'),   26,     'Z' );
is( to_col_number('AA'),  27,     'AA' );
is( to_col_number('AB'),  28,     'AB' );
is( to_col_number('AY'),  51,     'AY' );
is( to_col_number('AZ'),  52,     'AZ' );
is( to_col_number('BA'),  53,     'BA' );
is( to_col_number('BB'),  54,     'BB' );
is( to_col_number('IV'),  256,    'IV' );
is( to_col_number('ZY'),  701,    'ZY' );
is( to_col_number('ZZ'),  702,    'ZZ' );
is( to_col_number('AAA'), 703,    'AAA' );
is( to_col_number('AAB'), 704,    'AAB' );
is( to_col_number('XFD'), 16_384, 'XFD' );

is( to_col_string(1),      'A',   'For 1' );
is( to_col_string(2),      'B',   'For 2' );
is( to_col_string(3),      'C',   'For 3' );
is( to_col_string(25),     'Y',   'For 25' );
is( to_col_string(26),     'Z',   'For 26' );
is( to_col_string(27),     'AA',  'For 27' );
is( to_col_string(28),     'AB',  'For 28' );
is( to_col_string(51),     'AY',  'For 51' );
is( to_col_string(52),     'AZ',  'For 52' );
is( to_col_string(53),     'BA',  'For 53' );
is( to_col_string(54),     'BB',  'For 54' );
is( to_col_string(256),    'IV',  'For 256' );
is( to_col_string(701),    'ZY',  'For 701' );
is( to_col_string(702),    'ZZ',  'For 702' );
is( to_col_string(703),    'AAA', 'For 703' );
is( to_col_string(704),    'AAB', 'For 704' );
is( to_col_string(16_384), 'XFD', 'For 16384' );
3位の人に投票したメンバーからのコメント
  • 似たようなコードを作る人がいる中、再利用性等を考慮したプログラマのお手本のようなコード。将棋で言うところの飛車。
  • Single purpose functions, used Test::More, ascii mapping.
  • 恐らく一番洗練されたコードであったと思う。プログラミングの考え方について示していただきためになった。

Excel列名変換問題をRubyとPerlとC#とF#で書いてみた

はじめに

こちらは第2回社内プログラミングコンテストの番外編です。


Excel列名変換問題で第2回社内プログラミングコンテストを開催してみた(後編) - give IT a try


実はこの問題、コンテストの前にF#とC#で解いていました。また、コンテストが終わった後でRubyでも書いてみました。
どの言語も基本的に同じロジックで書いています。
ロジックは同じでも、違う言語で書くとそれぞれの言語の特徴が出ているようで興味深いかもしれません。

問題のおさらい

Excel列名変換問題はこんな問題です。

  • Excelの列名のようにアルファベットを数字に変換する
    • A=1, Z=26, AA=27 ...
  • 反対に数字をアルファベットに変換する処理も用意する
    • 1=A, 26=Z, 27=AA

この問題を解くにあたって、僕は以下のようなロジックを考えました。

XFD(16384)の場合
  1. アルファベットは全部で26文字であることを定数で表現する。
  2. 文字列XFDを配列[X, F, D]に変換する。
  3. (再帰1)配列[X, F, D]を文字Xと配列[F, D]に分解する。
  4. アスキーコードから文字Xがアルファベットの24番目であることを割り出す。
  5. 配列[F, D]の長さが2であることを割り出す。
  6. 26の2乗 * 24 = 16224を保存する。
  7. (再帰2)次に配列[F, D]の処理に移る。
  8. 配列[F, D]を文字Fと配列[D]に分解する。
  9. アスキーコードから文字Fがアルファベットの6番目をであること割り出す。
  10. 配列[D]の長さが1であることを割り出す。
  11. 26の1乗 * 6 = 156を保存する。
  12. (再帰3)次に配列[D]の処理に移る。
  13. 配列[D]を文字Dと配列[]に分解する。
  14. アスキーコードから文字Dがアルファベットの4番目をであること割り出す。
  15. 配列[]の長さが0であることを割り出す。
  16. 26の0乗 * 4 = 4を保存する。
  17. (再帰4)次に配列[]の処理に移る。
  18. 配列の長さが0なので、これまでに計算した値の合計を求める。
  19. 16224 + 156 + 4 = 16384
  20. 16384をXFDの列番号として呼び出し元に返す。
1354(AZB)の場合
  1. アルファベットは全部で26文字であることを定数で表現する。
  2. (再帰1)1354を26で割った商とあまりを求める。
  3. 1354 / 26 = 52 あまり 2
  4. アスキーコードからあまりの2をアルファベットのBに変換する。
  5. (再帰2)商の52を26で割った商とあまりを求める。
  6. 52 / 26 = 2 あまり 0
  7. アスキーコードからあまり0をアルファベットに変換するとAより小さい@になってしまう。
  8. そこであまりが0になる場合だけ、商を一つ減らし、あまりを26と考える。
  9. 52 / 26 = 1 あまり 26
  10. アスキーコードからあまりの26をアルファベットのZに変換する。
  11. (再帰3)商の1を26で割った商とあまりを求める。
  12. 1 / 26 = 0 あまり 1
  13. アスキーコードからあまりの1をアルファベットのAに変換する。
  14. (再帰4)商が0になったので、これまでに割り出したアルファベットを連結する。
  15. A + Z + B = AZB
  16. AZBを1354の列名として呼び出し元に返す。


文章で書くとかなりまどろっこしいですね。
たぶんコードで見る方が早いと思います。
それでは各言語で書いたプログラムを見ていきましょう。

Ruby

まずはRubyで書いた場合。結構シンプルで読みやすいと思います。

class ExcelColConv
    AZ_LENGTH = 'Z'.ord - 'A'.ord + 1 #26
    OFFSET_NUM = 'A'.ord - 1 #64

    # アルファベットから数字
    def to_col_number(col_str)
       convert_str 0, col_str.split('') 
    end

    def convert_str(ret, str_array)
        if str_array.empty?
            ret
        else
            c = str_array.shift
            convert_str calc_decimal(c, str_array.length) + ret, str_array # 再帰
        end
    end
    
    def calc_decimal(c, times)
        AZ_LENGTH ** times * (c.ord - OFFSET_NUM)
    end

    # 数字からアルファベット
    def to_col_string(col_num)
        convert_num '', col_num 
    end

    def convert_num(ret, n)
        if n == 0
            ret
        else
            quo, rem = excel_divmod n
            convert_num (rem + OFFSET_NUM).chr + ret, quo # 再帰           
        end
    end    

    def excel_divmod(n)
        quo, rem = n.divmod AZ_LENGTH
        rem == 0 ? [quo - 1, AZ_LENGTH] : [quo, rem]
    end
end
Perl

次にPerlで書いた場合です。
だいたいRubyと同じように書けますが、Rubyよりもちょっと冗長な感じがします。

package ExcelUtil;
use strict;
use warnings;
use base qw( Exporter );
our @EXPORT_OK = qw( to_col_number to_col_string );

my $AZ_LENGTH  = ord('Z') - ord('A') + 1;    #26
my $OFFSET_NUM = ord('A') - 1;               #64

# アルファベットから数字
sub to_col_number {
    my ($col_str) = @_;
    my @chars = split //sm, $col_str;
    return convert_str( 0, \@chars );
}

sub convert_str {
    my ( $ret, $chars_ref ) = @_;

    if ( scalar( @{$chars_ref} ) == 0 ) {
        return $ret;
    }

    my $c            = shift @{$chars_ref};
    my $chars_length = scalar @{$chars_ref};
    return convert_str( calc_decimal( $c, $chars_length ) + $ret, $chars_ref ); # 再帰
}

sub calc_decimal {
    my ( $c, $times ) = @_;
    return $AZ_LENGTH**$times * ( ord($c) - $OFFSET_NUM );
}

# 数字からアルファベット
sub to_col_string {
    my ($n) = @_;
    return convert_num( q{}, $n );
}

sub convert_num {
    my ( $ret, $n ) = @_;
    if ( $n == 0 ) {
        return $ret;
    }
    else {
        my ( $quo, $rem ) = excel_divmod($n);
        return convert_num( chr( $rem + $OFFSET_NUM ) . $ret, $quo ); # 再帰
    }
}

sub excel_divmod {
    my ($n) = @_;
    my ( $quo, $rem ) = ( int( $n / $AZ_LENGTH ), $n % $AZ_LENGTH );
    if ( $rem == 0 ) {
        return $quo - 1, $AZ_LENGTH;
    }
    else {
        return $quo, $rem;
    }
}

1;
C#

次にC#で書いた場合です。かなり長くなりますね。
ただしこれはC# 2.0です!2世代前のバージョンなのでちょっと古いです。

using System;
using System.Collections.Generic;

public class ExcelUtil
{
    private static readonly int AzLength = (int)'Z' - (int)'A' + 1; // 26
    private static readonly int OffsetNum = (int)'A' - 1; // 64

    #region アルファベットから数字
    public int ToColNumber(string colStr)
    {
        return ConvertString(0, new Queue<char>(colStr.ToCharArray()));
    }

    private int ConvertString(int ret, Queue<char> chars)
    {
        if (chars.Count == 0)
        {
            return ret;
        }
        else
        {
            char c = chars.Dequeue();
            return ConvertString(CalcDecimal(c, chars.Count) + ret, chars); // 再帰
        }
    }

    private int CalcDecimal(char c, int times)
    {
        return (int)Math.Pow(AzLength, times) * ((int)c - OffsetNum);
    }
    #endregion

    #region 数字からアルファベット
    public string ToColString(int colNum)
    {
        return ConvertNumber(string.Empty, colNum);
    }

    private string ToChar(int n)
    {
        return ((char)(n + OffsetNum)).ToString();
    }

    private struct Tuple
    {
        public int quo;
        public int rem;
        public Tuple(int quo, int rem)
        {
            this.quo = quo;
            this.rem = rem;
        }
    }

    private Tuple ExcelDivmod(int n)
    {
        int quo = n / AzLength;
        int rem = n % AzLength;
        return (rem == 0) ? new Tuple(quo - 1, AzLength) : new Tuple(quo, rem);
    }

    private string ConvertNumber(string ret, int n)
    {
        if (n == 0)
        {
            return ret;
        }
        else
        {
            Tuple t = ExcelDivmod(n);
            return ConvertNumber(ToChar(t.rem) + ret, t.quo); // 再帰
        }
    }
    #endregion
}
F#

最後にF#で書いた場です合。
一番短くなりますが、F#の知識がないと何がなんだか分からないかもしれません。。。

namespace ExcelColConv
module ExcelUtil = 
  let AzLength = int 'Z' - int 'A' + 1 // 26
  let OffsetNum = int 'A' - 1 // 64
  
  // アルファベットから数字
  let toColNumber (colStr : string) =
    let calcDecimal c times = (pown AzLength times) * (int c - OffsetNum) 
    let rec convertStr ret = function
      | [] -> ret
      | c::chars -> ((calcDecimal c chars.Length) + ret, chars)
                    ||> convertStr // 再帰
    convertStr 0 (Seq.toList colStr)
  
  // 数字からアルファベット
  let toColString colNum =
    let toChar n = n + OffsetNum |> char |> string
    let excelDivmod n = 
      match n / AzLength, n % AzLength with
      | quo, 0 -> quo - 1, AzLength
      | t -> t
    let rec convertNum ret = function
      | 0 -> ret
      | n -> excelDivmod n 
             ||> fun quo rem -> (toChar rem) + ret, quo
             ||> convertNum // 再帰
    convertNum "" colNum
補足説明

実際に僕がプログラムを書いた順番はF#、C#PerlRubyの順です。
F#で最初にロジックを考えたり、プログラムを書いたりしたので、他の言語からするとちょっとトリッキーなところがあるかもしれません。
F#だからトリッキー、というわけではありませんが、当時はF#の機能をできるかぎり取り込んでコードを短くしようと目論んでいたので、小難しいプログラムになったかもしれません。
他の言語はそのF#のロジックに引きずられているので、「何もそんな書き方にしなくても」と思われるようなところがあるかもしれません。

いろんな言語で書いてみた感想

簡潔さと読みやすさのバランスが一番取れているのはRubyだと思いました。
個人的な好みかもしれませんが、書いていて楽しいです。


F#は関数型言語の機能を活用するとかなり短く書けるので、ある意味非常に面白いのですが、やりすぎると第三者にはさっぱり分からないプログラムができあがる危険性があるかもしれません。
自己満足なプログラムには要注意です。(僕のプログラムのように・・・)


簡潔さや読みやすさの観点からすると、Perlはちょっと中途半端な印象です。
また、"@_"みたいな記号で引数を受け取るのもPerlに不慣れな人から見ると奇妙で、僕のように慣れていない人からするとちょっと書きにくいです。
PerlにしかないCPANモジュール等を使うのでなければ、Better PerlとしてのRubyを使えばいいんじゃないかと思います。


C#はこの程度の小さなプログラムでは構文の冗長さの方が強く目立ってしまい、ある意味かわいそうでした。
コンパイル型言語はもう少し大規模なプログラムを書く時に、変数やメソッドにしょうもないスペルミスとかが混入していないことをコンパイルで確認できる、みたいなところがメリットになるんじゃないかと思います。


というわけで、個人的には書きやすくて読みやすいRubyと、黒魔術的なコードが書けて面白いF#が気に入りました。
でも仕事で使っているのはC#Perl、というのが何とも皮肉な現実なんですけどね・・・。

あわせて読みたい

Excel列名変換問題の元ネタはこちらのエントリです。

Excel列名変換問題で第2回社内プログラミングコンテストを開催してみた(前編) - give IT a try
Excel列名変換問題で第2回社内プログラミングコンテストを開催してみた(後編) - give IT a try


動的型付け言語(スクリプト言語)と静的型付け言語(コンパイル言語)を自分なり比較してみたエントリもあります。

「動的型付言語は使い物にならない」か? - give IT a try


このエントリで紹介したF#のプログラムを詳しく掘り下げて解説してみました。

F#で解いたExcel列名変換問題を解説してみる - give IT a try


F#の本も書いている、いげ太さんの解答例。
さすがプロ。僕なんかよりもずっとエレガントなF#なプログラムです。


igeta's
gist: 1339173 — Gist

F#で解いたExcel列名変換問題を解説してみる

はじめに

一つ前のエントリでExcel列名変換問題の色々な解答例を紹介しました。
Ruby、Perl、C#は普通に手続き型プログラミングが分かっていれば、大体理解できると思うのですが、F#だけはかなり特殊な処理になっているので、もう少し深く掘り下げて解説してみようと思います。


ちなみに一つ前のエントリはこちらです。


Excel列名変換問題をRubyとPerlとC#とF#で書いてみた


なお、僕はそれほどF#に深く精通しているわけではありません。
解説の中にはF#として正しくない説明の仕方が紛れ込んでいる可能性もあるので、そのあたりは予めご了承ください。(^_^;

問題のおさらい

Excel列名変換問題はこんな問題です。

  • Excelの列名のようにアルファベットを数字に変換する
    • A=1, Z=26, AA=27 ...
  • 反対に数字をアルファベットに変換する処理も用意する
    • 1=A, 26=Z, 27=AA

F#で書いたプログラム

とりあえず、素のプログラムをそのまま載せます。

namespace ExcelColConv
module ExcelUtil = 
  let AzLength = int 'Z' - int 'A' + 1 // 26
  let OffsetNum = int 'A' - 1 // 64
  
  // アルファベットから数字
  let toColNumber (colStr : string) =
    let calcDecimal c times = (pown AzLength times) * (int c - OffsetNum) 
    let rec convertStr ret = function
      | [] -> ret
      | c::chars -> ((calcDecimal c chars.Length) + ret, chars)
                    ||> convertStr // 再帰
    convertStr 0 (Seq.toList colStr)
  
  // 数字からアルファベット
  let toColString colNum =
    let toChar n = n + OffsetNum |> char |> string
    let excelDivmod n = 
      match n / AzLength, n % AzLength with
      | quo, 0 -> quo - 1, AzLength
      | t -> t
    let rec convertNum ret = function
      | 0 -> ret
      | n -> excelDivmod n 
             ||> fun quo rem -> (toChar rem) + ret, quo
             ||> convertNum // 再帰
    convertNum "" colNum


次にアルファベットから数値、数値からアルファベットの変換ロジックをそれぞれ別々に説明していきます。

アルファベットを数値に変換する場合

まず、ロジックを文章で表現するとこうなります。

XFD(16384)の場合
  1. アルファベットは全部で26文字であることを定数で表現する。
  2. 文字列XFDを配列[X, F, D]に変換する。
  3. (再帰1)配列[X, F, D]を文字Xと配列[F, D]に分解する。
  4. アスキーコードから文字Xがアルファベットの24番目であることを割り出す。
  5. 配列[F, D]の長さが2であることを割り出す。
  6. 26の2乗 * 24 = 16224を保存する。
  7. (再帰2)次に配列[F, D]の処理に移る。
  8. 配列[F, D]を文字Fと配列[D]に分解する。
  9. アスキーコードから文字Fがアルファベットの6番目をであること割り出す。
  10. 配列[D]の長さが1であることを割り出す。
  11. 26の1乗 * 6 = 156を保存する。
  12. (再帰3)次に配列[D]の処理に移る。
  13. 配列[D]を文字Dと配列[]に分解する。
  14. アスキーコードから文字Dがアルファベットの4番目をであること割り出す。
  15. 配列[]の長さが0であることを割り出す。
  16. 26の0乗 * 4 = 4を保存する。
  17. (再帰4)次に配列[]の処理に移る。
  18. 配列の長さが0なので、これまでに計算した値の合計を求める。
  19. 16224 + 156 + 4 = 16384
  20. 16384をXFDの列番号として呼び出し元に返す。


次に、アルファベットを数値に変換するロジックだけを抜粋し、細かいコメントを付けてみます。

// namespaceの宣言。
namespace ExcelColConv

// moduleの定義。
// F#はPythonのようにインデントがブロックになる。
module ExcelUtil =

  // 定数の定義(といってもF#は再代入できないから全部定数みたいなものだけど)。
  // letは変数(常に定数?)や関数を定義するためのキーワード。
  let AzLength = int 'Z' - int 'A' + 1 // 26
  let OffsetNum = int 'A' - 1 // 64

  // アルファベットを数字に変換する関数の定義。
  // colStrは引数。
  // 型推論がうまくいかなかったので、明示的にstring型であることを指定している。
  let toColNumber (colStr : string) =

    // ローカル変数ならぬ、ローカル関数の定義。
    // cとtimesが引数。
    // この関数では与えられた文字と数値(配列の長さ)から、その文字を数値に変換する。
    // (pown AzLength times)はpown関数の引数にAzLengthとtimesを与えている。
    // int cは文字列をアスキーコードに変換している。
    // cがB(アスキーコードは66)、timesが2であれば。
    //   26の2乗 * (66 - 64) = 1352
    // となる。
    let calcDecimal c times = (pown AzLength times) * (int c - OffsetNum) 

    // こちらもローカル関数。
    // recは再帰呼び出しを可能にするためのキーワード。
    // 引数は数値であるretと文字の配列の2つ。
    // ただし、2つ目の引数はfunctionキーワードを使ったパターンマッチで
    // 利用されるので明示的には現れていない。
    let rec convertStr ret = function

      // 渡された配列が空なら一つ目の引数である数値を返す(再帰処理の終わり)。
      | [] -> ret

      // 配列が空でなければ、配列を文字(c)と残りの配列(chars)に分解する。
      // (calcDecimal c chars.Length)上で定義したcalcDecimal関数に文字cと残りの配列の長さを渡す。
      // 例えばcがB、charsの長さが2であれば1352が返る。
      // (calcDecimal c chars.Length) + retでここまでの数値の合計値が計算される。
      | c::chars -> ((calcDecimal c chars.Length) + ret, chars)

                    // convertStr関数にこれまでの合計値と残りの配列を渡す。
                    // ||> は1つ前で作られたタプルを関数の引数として使うための演算子。
                    // convertStrは自分自身なので再帰処理となる。
                    ||> convertStr

    // toColNumberを呼び出すとここから処理が実行される。
    // (ここから上のコードは全部ローカル関数の定義)
    // 1つ目は合計値の初期値である0。
    // 2つ目は文字列から変換された配列(厳密にいうとリスト)。
    convertStr 0 (Seq.toList colStr)

  // (以下省略)

数値をアルファベットに変換する場合

先ほどと同様にロジックを文章で表現してみます。

1354(AZB)の場合
  1. アルファベットは全部で26文字であることを定数で表現する。
  2. (再帰1)1354を26で割った商とあまりを求める。
  3. 1354 / 26 = 52 あまり 2
  4. アスキーコードからあまりの2をアルファベットのBに変換する。
  5. (再帰2)商の52を26で割った商とあまりを求める。
  6. 52 / 26 = 2 あまり 0
  7. アスキーコードからあまり0をアルファベットに変換するとAより小さい@になってしまう。
  8. そこであまりが0になる場合だけ、商を一つ減らし、あまりを26と考える。
  9. 52 / 26 = 1 あまり 26
  10. アスキーコードからあまりの26をアルファベットのZに変換する。
  11. (再帰3)商の1を26で割った商とあまりを求める。
  12. 1 / 26 = 0 あまり 1
  13. アスキーコードからあまりの1をアルファベットのAに変換する。
  14. (再帰4)商が0になったので、これまでに割り出したアルファベットを連結する。
  15. A + Z + B = AZB
  16. AZBを1354の列名として呼び出し元に返す。


次に、数値をアルファベットに変換するロジックだけを抜粋し、細かいコメントを付けてみます。

  // (省略)
  
  // 数字をアルファベットに変換する関数の定義。
  // 引数はcolNum。
  let toColString colNum =

    // 数値を文字(厳密にいうと長さ1の文字列)に変換するローカル関数。
    // nは引数。
    // |> は前の値を次の関数の引数にするパイプ演算子。
    // 例えばnが2であれば
    //   n + OffsetNum = 2 + 64 = 66
    //   char 66 = 'B'
    //   string 'B' = "B"
    // となる。
    let toChar n = n + OffsetNum |> char |> string

    // Excel用に商とあまりを求めるローカル関数。
    // nは引数。
    let excelDivmod n =

      // nを26で割った商とあまりを求める。 
      match n / AzLength, n % AzLength with

      // 計算結果によって処理を分岐させる(パターンマッチ)。
      // あまりが0であれば商を一つ減らし、26をあまりと見なす(そのまま戻り値になる)。
      | quo, 0 -> quo - 1, AzLength

      // それ以外の場合は商とあまりをそのまま戻り値と見なす。
      | t -> t

    // こちらもローカル関数。
    // recは再帰呼び出しを可能にするためのキーワード。
    // 引数は文字列であるretと数値の2つ。
    // ただし、2つ目の引数はfunctionキーワードを使ったパターンマッチで
    // 利用されるので明示的には現れていない。
    let rec convertNum ret = function

      // 2つ目の引数が0なら処理を終了し、1つ目の引数である文字列をそのまま返す(再帰処理の終わり)。
      | 0 -> ret

      // それ以外であれば、まず商とあまりを求める。
      | n -> excelDivmod n 

             // fun はアドホックな関数を定義するためのキーワード。
             // "fun x y z -> ..."のように書くと、引数x, y, zを使った関数を定義できる。
             // ここでは商とあまりを引数として受け取る関数を定義している。
             // (toChar rem)であまりを文字に変換している。
             // (toChar rem) + retで、その文字をこれまでに変換してきた文字列と連結している。
             // あまりのquoはそのまま保持する。
             ||> fun quo rem -> (toChar rem) + ret, quo

             // 1つ前の行で作った文字列とあまりのタプルをconvertNum関数に渡す。
             // convertNumは自分自身なので再帰処理となる。           
             ||> convertNum

    // toColStringを呼び出すとここから処理が実行される。
    // (ここから上のコードは全部ローカル関数の定義)
    // 1つ目は文字列の初期値である空文字列。
    // 2つ目は最初に渡された数値。
    convertNum "" colNum

おわりに

え〜っと・・・思ったほど分かりやすくならないですね。ごめんなさい。


言い訳じゃないですけど、自分で書いてみるのが一番分かりやすいと思います。
そのとき、できるだけで手続き型っぽくならないように関数型らしく書こうと努力すると、そのうちに関数型言語のパワフルさと面白さが見えてくるんじゃないかと思います。


僕もネットや技術書を読むだけではF#や関数型言語の魅力がよく分かりませんでしたが、自分で手と頭を動かすと最近流行している理由が分かってきたような気がしました。
F#や関数型言語に慣れていない人はきっと、このエントリを見ても魅力は伝わっていないと思うので、是非自分の手と頭を動かして関数型言語の魅力を味わってください!!


・・・と少々強引な結論で、このエントリを終了します。(^o^;;

あわせて読みたい

F#の概要をつかむのであれば、bleisさんのこちらのエントリが分かりやすいと思います。


デブサミ 2011 で F# について話してきました! - ぐるぐる~


僕がF#を勉強したのはこちらの2冊です。
詳細な内容を学習するなら、やはり技術書で!

Programming F# (Animal Guide)

Programming F# (Animal Guide)

実践 F# 関数型プログラミング入門

実践 F# 関数型プログラミング入門