Fortranで文字のペアの片割れを簡単に取得する方法

# 概要
ある文字から，それに対応する文字を取得する方法を紹介します．以降，ある文字とそれに対応する文字の組をペアと呼びます．
対象となる文字を文字列定数として定義しておき，`index`手続を用いて文字列中の位置を取得し，対応する文字列定数の中から組となる文字を抽出します．

```Fortran
program main
    use, intrinsic :: iso_fortran_env
    implicit none

    character(*), parameter :: upper_case = "ABC"
    character(*), parameter :: lower_case = "abc"

    integer(int32) :: pos
    pos = index(upper_case, "C")
    print *, lower_case(pos:pos) ! c
end program main
```

# 環境
- Windows 10
- gfortran 10.3.0
- intel fortran classic 2021.5.0
- nag fortran 7.1

# 文字ペアの片割れの取得
## 大文字と小文字を変換する例題
プログラムを初めてから学習がそう進まないうちに，英字を大文字から小文字，あるいはその逆に変換する例題に出会います．この例題の目的は，プログラム内では文字も数字として（文字コードとして）扱われていることを学習することです．

このとき，文字コードには著者の知る限り必ずASCIIコードが用いられます．`"A"`のASCIIコードは10進数で`65`, `"a"`のASCIIコードは10進数で`97`ですから，大文字英字に`97-65`を足せば小文字英字に変換できると学習するわけです．Fortranでは，半角英数記号のASCIIコードを返す手続`iachar`と，数字をASCIIコードとして文字に変換する手続`achar`を用いて次のように書けます．

```Fortran
achar(iachar(大文字) + iachar("a") - iachar("A"))
```

関数にするのであれば，下記のようになるでしょうか．
```Fortran
    !>大文字英字を受け取り，対応する小文字を返す．
    !>
    !>### Example
    !>
    !>```Fortran
    !>print *, to_lower("C") ! c
    !>```
    function to_lower(upper_char) result(lower_char)
        implicit none
        character, intent(in) :: upper_char
            !! 半角大文字英字（ASCII）
        character :: lower_char
            !! 対応する半角小文字英字（ASCII）

        lower_char = achar(iachar(upper_char) + iachar("a") - iachar("A"))
    end function to_lower
```

これは大して難しくない問題です．

FORTRANが開発された当初はASCIIなんてものはなかったので，ASCIIを仮定しない手続`ichar`, `char`もあります．これらは，実行しているシステムに規定された文字コードセットと整数の変換を行います．現状では，ASCII以外の文字コードセットを用いている環境で実行する方が難しいでしょうが，システム規定の文字コードセットで，必ずアルファベット順に並んでいるという保証もありません．ASCIIコードを仮定せずに，大文字小文字の変換をしてみましょうなんて意地悪な問題を作ることも可能です．

見方を変えると，これは大文字と小文字のペアのうち，大文字に対応する小文字を取得していると考えられます．

## 開き括弧と閉じ括弧の対応
文字のペアの例として，大文字小文字を挙げました．文字のペアとして馴染みやすそうな開き括弧と閉じ括弧の例を考えてみます．

開き括弧から閉じ括弧を得たいという要望は，多くないでしょうが存在します．このとき，大文字と小文字の変換のアルゴリズムを応用しようと考えます．

- `<`のASCIIコードは10進数で`60`，`>`は`62`です．
- `[`のASCIIコードは10進数で`91`，`]`は`93`です．
- `{`のASCIIコードは10進数で`123`，`}`は`125`です．

この対応をみると，開き括弧のASCIIコードに`+2`すれば閉じ括弧が得られそうです．しかし，そうでない対応をもつ括弧もあります．

- `(`のASCIIコードは10進数で`40`，`)`は`41`です．

また，`"`や<code>`</code>を開きと閉じの両方に使いたいという要望が出たとすると，それらを考慮すると，対応を得る関数が当初の想定よりも複雑になってしまいます．

## 定数を用いたペアの取得
上記のような，制約の強い前提に基づいて関数を作るのではなく，統一的なアルゴリズムで，文字のペアの片割れを取得する方法があります．

検査対象となる文字列の配列定数と，そのペアとなる文字列の配列定数を用意しておき，被検索文字列内から部分文字列を検索する手続`index`を利用して位置を取得し，ペアとなる文字列定数の中から目当ての文字を抽出します．

|書式|機能|
|:--|:--|
|`index(被検索文字列, 検索文字列 [,back={.false.,.true.}, kind=整数型種別])`|被検索文字列から検索文字列を検索し，その文字の位置を`integer(整数型種別)`で返す．`back=.true.`とすると，後方から検索する|

```Fortran
                                     !1234567890123456789012345
    character(*), parameter :: str = "  functions for character"

    print *,index(str, "ct")             !  6 <- functionに含まれるctのcの位置
    print *,index(str, "ct",back=.true.) ! 22 <- characterに含まれるctのcの位置
```

大文字と小文字の変換については，それぞれ大文字のみの文字列定数と，それに対応する順番で並べられた小文字のみの文字列定数を用意した後，大文字の定数から目当ての文字の場所を`index`で検索し，小文字の定数から対応する文字を取得します．

```Fortran
    character(*), parameter :: upper_case = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        !! 大文字
    character(*), parameter :: lower_case = "abcdefghijklmnopqrstuvwxyz"
        !! 小文字

    integer(int32) :: pos
    character :: upper_char, lower_char

    ! upper_charに文字を代入する．

    pos = index(upper_case, upper_char)
    lower_char = lower_case(pos:pos)

    ! lower_charに文字を代入する．

    pos = index(lower_case, lower_char)
    upper_char = upper_case(pos:pos)
```

`index`は，検査文字列が被検索文字列にない場合に`0`を返すので，`pos`が`0`かどうかのチェックは必要です．

同じように，開き括弧と閉じ括弧を文字列定数として定義しておけば，同じ方法で開き括弧に対応した閉じ括弧を取得できます．

```Fortran
    character(*), parameter :: bracket_open  = "(<[{)>]}"
        !! 開き括弧
    character(*), parameter :: bracket_close = ")>]}(<[{"
        !! 閉じ括弧

    integer(int32) :: pos
    character :: open_char, close_char

    open_char = "("
    pos = index(bracket_open, open_char)
    close_char = bracket_close(pos:pos)
    print *, open_char, close_char ! ()
```

括弧が複数の場合は，`open_char`や`close_char`を`allocatable`にした上で，文字の先頭に連結していけば括弧の対応が取れます．また，`index`が`0`を返すことを利用して，`"`に対応する閉じ括弧として`"`を設定することも可能です．

```Fortran
    integer(int32) :: pos, p
    character(:), allocatable :: open_char, close_char

    open_char = '({|"'
    close_char = ""

    do p = 1, len(open_char)
        pos = index(bracket_open, open_char(p:p))
        if (pos >= 1) then  ! indexの戻り値が1以上なら文字列に開き括弧が含まれているので，同じ位置にある閉じ括弧を取得する
            close_char = bracket_close(pos:pos)//close_char 
        else                ! indexの戻り値が1以上でないなら，文字列に開き括弧が含まれていないので，閉じ括弧に開き括弧と同じ文字を用いる
            close_char = open_char(p:p)//close_char
        end if
    end do
    print *, open_char, close_char ! ({|""|})
```

## 補足
こういう話を書くと，実行速度が遅い（から価値がない）という意見を頂くことがあります．確かに，ASCIIコードを仮定して整数演算と整数－文字変換のみで処理する方が高速なのは間違いありません．しかし，著者自身は，何を重要視しているかによって複数の書き方が選択できる方が重要であり，遅い＝価値がないとは考えていません．

著者が開発している流体シミュレーションソフトウェアでは，文字列走査よりも流体シミュレーションの方が桁違いに重たいので，文字列走査を高速化したところで効果は全くありません．それよりも，様々な知識背景を持つ開発者やユーザがソースを見て意図を把握できること，長い間（長くて4半世紀）保守管理でき，その間に変化するであろう計算機環境の変化に対応できることの方が重要です．

一方で，文字列の操作を主としたソフトウェアで，価値が高速な動作にあるのだとしたら，ここで紹介した方法は向かないでしょう．

# まとめ
Fortranには文字列に対する組込手続が多くあって，意外に便利です．