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.
  • 恐らく一番洗練されたコードであったと思う。プログラミングの考え方について示していただきためになった。