冬の未完成なDIY

毎年のことながら、最近は秋というものがなくなった。なくなったというのは正確ではないかもしれない。暑さが続き、急に温度が下がってしまうと、寒さを感じてしまい「もう冬だな」と思ってしまうのだろう。本当は、紅葉が見られるので、短いながらも秋は存在するのだ。

現実に戻ろう。前回で「なければ作ろう」という話をした。今回は実際に未完成ながら作ってみたというお話である。Fortranは海外製でありレガシー言語であるため、日本語文字列操作にとても弱いというかできない。そりゃあそうだ。Fortranがおぎゃーとした年は日本語入力なんてなかったし、海外製である以上、1バイト文字さえ扱えれば良かったわけだからね。Fortranを販売しているメーカーはあるが、メーカー独自の日本語対応の拡張をしているところもあるようだ。残念ながら、オープンソースとして開発が続けられているGNU Fortranでは難しいだろう。

まあ、そんなこと言ってられないので、日本語の文字数を返すmbstrlen関数という簡単なものを自作してみた。そのためにはFortranで扱う日本語について知る必要がある。現在、文字コードはUTF-8である。UTF-8は基本的に文字を1〜4バイトの可変長コードで表しており、現在では世界標準の仕様といっていいほどである。日本ではShift-JISというコードもあるが、今ではもう使われないといっていいほどだ。UTF-8にはUTF-8 BOMというのもあるが、話すのもおぞましいので割愛する。

さてFortranでは日本語1文字を3バイトで表現している。これはFortranだけに限らない。日本語の“あ”はE38182となる。これをLen関数で文字列の長さを取得してみると、3という数字が返ってくる。Pythonでは1だ。これはバグでも何でもなく日本語文字列の取り扱いの差である。FortanのLen関数は文字を1バイトとして扱うために、日本語の1文字は3バイトと返しているだけである。

まだまだ、知っておかなければならないことがある。文字の型宣言である。前回でも書いたように、characterは文字型宣言である。character(128)とすれば、128文字までの文字を扱うことができる。これが、問題となってしまうことがある。例えば、次のように型宣言したとしよう。

character(128) :: str

この変数に文字を代入して、その長さをLen関数を使って表示させてみる。

program strlen
    implicit none

    character(128) :: str

    str = 'ABCDEFG'

    print *, len(str)
end program strlen

「7文字だから、長さは7だろう」と思ったアナタ。ぶー、不正解。正解は128である。Fortranでは128文字の領域を定義したら、そこに何文字代入されようが長さは128なのである。自分で関数を作ろうとすると、この長さの定義は文字列を操作する上でいろいろと障害になることが多い。しかし、これでは困ったことになるので、回避策として次のようにする。

trim(adjustl(str))

adjustl関数で左寄せにしてtrim関数で末尾の空白を削除する。adjustl関数を使うのは、数字を文字に変換したとき、右寄せされたままになるので、それを避けるためである。これで正しい文字数を返すことができる。

んで、そんなこんなで書いたコードが以下のとおりである。

    program Test
        implicit none

    ! 関数の型宣言と合わせること
        character(:), allocatable :: str

        str = 'あいうえお'

        print *, mbstrlen(str)
    stop
    contains
    ! mbstrlen(strparam)
    ! 機能      : 日本語文字数を返す
    ! 使用法    : 例  number = mbstrlen('あいうえお')
    ! 注意点:
    ! ASCIIと日本語を区別していないため、ASCII文字と混在させないこと。
    ! 
    ! 変数:
    ! c         : 文字数カウント
    ! i         : ループ用
    ! n         : 動的配列変数領域
    ! strparam  : 引数(日本語文字列)
    ! strm      : 退避用
    ! s         : 日本語文字を格納する動的配列変数
    integer function mbstrlen(strparam)
        implicit none

        character(:), allocatable :: strparam
        character(3), dimension(len(trim(adjustl(strparam)))) :: strm
        character(3), allocatable :: s(:) ! 動的配列として宣言
        integer :: i, c, n

        ! 領域を確保
        n = len(trim(adjustl(strparam)))
        allocate(s(n))

        ! 文字数のカウント
        c = 0
        do i = 1, n - 2, 3
            s(i) = strparam(i:i + 2)
            c = c + 1
        end do

        ! 解放する前に別の変数へ代入
        strm = s

        ! 領域を解放
        deallocate(s)
        deallocate(strparam)

        ! 文字数を返す
        mbstrlen = c
    end function mbstrlen
end program Test

いくつか解説しておこう。文字の型宣言は同時にその長さを指定するが、allocatableを使って動的に扱うようにしている。変数strmとsは配列宣言しているが、これはあとから文字列を抽出する関数を作るための覚書のようなものなのであまり気にする必要はない。ちなみにstrmとsのサイズを同じにしておかないと、ゴミが混じってしまい、あまり好ましくない結果となる。

do文(for文と同じ)で文字数をカウントしているが、少し変わった記述をしている。本来なら、

do i = 1, n, 3

と記述するかもしれない。どちらでも同じ結果になるので問題ないのだが、チキンなので予防策みたいにしている。これは以下の理由による。

“あいうえお”から一文字ずつ取り出そうとすると、日本語は1文字3バイトなので次のようになる。

01 02 03  04 05 06   07 08 09   10 11 12   13 14 15
   あ       い       う       え       お

1文字目から3文字目までが「あ」となる。見ればわかると思うが、取り出し開始位置は1文字目、4文字目というようになる。最終は13文字目なのでnをー2して13文字目としているわけである。実際の取り出す処理をstrparam(i:i + 2)で行っている。この(i:i+2)という書き方は、文字列を取り出すときに使われる書き方である。

(p1:p2)
p1:取り出し開始位置
p2:取り出し終了位置

となる。たとえば、(1:3)と記述した場合は、1文字目から3文字目までという意味になる。決して1文字目から2文字取り出すという意味ではない。

 

長くなってしまった。未完成ではあるが一応形としてはできあがった。注意点にも書いてあるとおり、ASCII文字(0ー9、a-z、A-Z、各記号文字)が混在した場合、正しい長さを返すことができない。解決策の一応の目星としては、文字コードのチェックなのだが、うまくいくかどうかわからないので今後の課題としておくことにした。

mbstrlen()は機能的にはシンプルだが、決してスムーズに作れたわけではない。ネットで調べたが、それにしても情報が少なすぎた。多くの時間は試行錯誤の繰り返しに使った時間である。

最後に身もフタもない言い方をすれば、「Fortranで文字列操作するのは間違ってるだろ」。そのとおりである。でもじじぃは頑張ったのだ。自分を褒めて終わることにする。