3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FortranAdvent Calendar 2022

Day 9

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

Last updated at Posted at 2022-12-10

概要

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

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を用いて次のように書けます.

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

関数にするのであれば,下記のようになるでしょうか.

    !>大文字英字を受け取り,対応する小文字を返す.
    !>
    !>### 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です.

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

定数を用いたペアの取得

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

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

書式 機能
index(被検索文字列, 検索文字列 [,back={.false.,.true.}, kind=整数型種別]) 被検索文字列から検索文字列を検索し,その文字の位置をinteger(整数型種別)で返す.back=.true.とすると,後方から検索する
                                     !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で検索し,小文字の定数から対応する文字を取得します.

    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を返すので,pos0かどうかのチェックは必要です.

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

    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_charclose_charallocatableにした上で,文字の先頭に連結していけば括弧の対応が取れます.また,index0を返すことを利用して,"に対応する閉じ括弧として"を設定することも可能です.

    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には文字列に対する組込手続が多くあって,意外に便利です.

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?