give IT a try

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

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

はじめに

一つ前のエントリでExcel列名変換問題の色々な解答例を紹介しました。
RubyPerlC#は普通に手続き型プログラミングが分かっていれば、大体理解できると思うのですが、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# 関数型プログラミング入門