Fortran
ModernFortran
FortranDay 3

Fortranにおける文字型変数の宣言方法と関連機能

概要

この記事では,文字型変数および文字列型についてまとめます.ユーザ定義派生型については三度目の先送りです.

使用環境

コンパイラ バージョン
intel Parallel Studio XE Composer Edition for Fortran 17.0.4.210
PGI Visual Fortran for Windows 18.7
gfortran 7.3.0 (for Ubuntu on WSL)

文字型変数

Fortranで文字あるいは文字列取り扱うために,character型が用意されています.Fortranにおいて文字を扱う変数と文字列を扱う変数は明確に区別する必要があります

character([len=]文字数[,kind=文字種別])[,属性,属性,...] :: 変数名[(要素数[,要素数,...])]

数値を取り扱う型では,型名の後の()に書いた数字は種別を表していました.character型の場合は文字列の長さを表します.character型の標準の種別は,調査したコンパイラでは1でした.数値を取り扱う型と同様に,kind=で種別を指定することもできますが,IntelおよびPGIコンパイラではcharacter型の種別は1しかなく,gfortranのみ1と4が利用できます.どのような種別を持っているかは,iso_fortran_envモジュールに定義されているcharacter_kindsで確認できます.ただし,PGIコンパイラはこの変数が定義されていません.プログラム中でkind=を用いて文字種別を指定し,コンパイルエラーが出るかどうかで利用の可否を判別しました.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    character(10)  :: a ! len=は省略可能
    character(5,1) :: b ! lenを指定している場合は,kind=も省略可能
    character(5,4) :: c ! kind=4はgfortranしかコンパイルが通らない

    print *,character_kinds
    ! I 1
    ! P 変数未定義のコンパイルエラー
    ! g 1 4

end program main

文字列の長さを省略すると,長さは1に設定されます.他の型と同様に,変数名の後ろに要素数を書いて配列を宣言できますが,文字の配列と文字列は全くの別物です

program main
    implicit none

    character    :: a    ! 1文字
    character    :: b(5) ! 文字の配列(要素数5)
    character(5) :: c    ! 長さ5の文字列
    character(5) :: d(5) ! 長さ5の文字列の配列

end program main

文字リテラル

文字または文字列リテラルは,数字・アルファベット・記号をダブルクオート"もしくはシングルクオート'で囲むことによって作ります.文字は',文字列は"といった違いはありません.

文字列の中にダブルクオート"あるいはシングルクオート'を置きたい場合には,文字の囲みにシングルクオート(' " ')あるいはダブルクオート(" ' ")を使用します.シングルクオートで囲んでいる時はシングルクオート2個(' '' '),ダブルクオートで囲んでいる時はダブルクオート2個(" "" ")連続して書くという方法もあるようです1

program main
    implicit none
    print *,"You'will be a good Fortran programmer"  ! You'will be a good Fortran programmer
    print *,'You''will be a good Fortran programmer' ! You'will be a good Fortran programmer
end program main

character型変数への代入

character型変数(文字,文字列)への代入には,他の型と同じように代入演算子=が利用できます.

代入する文字列リテラルが変数の長さよりも長い場合には,変数の長さを超えた分は打ち切られます.文字列リテラルが短い場合は,文字列の末尾は空白となります.

program main
    implicit none

    character(5) :: a ! 長さ5の文字列

    a = "abcdefg"
    print *,a         ! abcde

    a = "z"
    print *,a         ! z␣␣␣␣
end program main

上のプログラムでは,長さ5の文字列に長さ7の文字列リテラル"abcdefg"を代入していますが,配列外参照などは起こらず,"abcde"の5文字分だけが代入されています.その後,"z"1文字だけを代入すると,出力は"zbcde"とはならず,"z␣␣␣␣"になっています.

文字の配列への代入にも代入演算子が利用できますが,おそらく予想とは異なる結果になるでしょう.詳しくは文字の配列と文字列の違いを参照してください.

文字列の定数

parameter属性を付与することで,character型定数を宣言できます.

program main
    implicit none

    character(7),parameter :: lang = "Fortran"
    print *,lang     ! Fortran
    !lang = "Python" ! コンパイルエラー
end program main

文字列の結合

文字列を結合する演算子//が利用できます.変数でも文字列リテラルでも結合できます.

program main
    character(7),parameter :: lang="Fortran"
    character(15) :: langver

    langver = lang//" 2008"

    print *,langver ! Fortran 2008␣␣␣
end program main

文字の配列と文字列の違い

要素の取り扱い

文字型変数において,文字の配列と文字列は全くの別物であると述べました.それを確認するために,色々な方法で宣言したcharacter型変数に同じ文字列リテラルを代入した結果をみてみましょう.

program main
    implicit none

    character    :: a    ! 文字
    character    :: b(5) ! 文字の配列(要素数5)
    character(5) :: c    ! 長さ5の文字列
    character(5) :: d(5) ! 長さ5の文字列の配列

    a = "abcdefg"
    b = "abcdefg"
    c = "abcdefg"
    d = "abcdefg"

    print *,a ! a 
    print *,b ! aaaaa
    print *,c ! abcde
    print *,d ! abcdeabcdeabcdeabcdeabcde
end program main

変数aの長さは1なので,文字列"abcdefg"を代入すると,最初の1文字だけが代入されています.

bは文字の配列で,要素数は5です.表示すると,aaaaaと出力されました.この挙動から,bb(1), b(2), b(3), b(4), b(5)が長さ1の文字列であって,すべての要素b(:)に対してabcdefgを代入しようとして,最初の1文字だけが代入されていると判断できます.

一方,長さ5の文字列cではabcdeと出力されており,文字列を取り扱うにはcharacter(len=)で長さを指定する必要があることが確認できます.

dは長さ5の文字列の配列であり,dの要素d(1), d(2), d(3), d(4), d(5)がそれぞれ長さ5の文字列です.

bの要素それぞれに個別の文字を代入するには,他の型の配列と同様に,配列構成子を使います.このとき,配列構成子で構成する配列がbより長くても短くても,コンパイルエラーが出ます.

program main
    implicit none

    character    :: b(5) ! 文字の配列(要素数5)

    b = ['a','b','c','d','e']
    print *,b ! abcde
end program main

同様に,dの要素それぞれに異なる文字列を代入する際も,配列構成子を利用します.

program main
    implicit none

    character(5) :: d(5) ! 長さ5の文字列の配列

    d = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"]
    print *,d ! AlphaBeta GammaDeltaEpsil
end program main

IntelおよびPGIコンパイラを用いると,文字列リテラルの長さが短い場合は空白が補われ,文字列リテラルの長さが長い場合には打ち切られます.gfortranではエラーになるので,"Beta ""Epsil"としなけれなりません.

    d = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"]
                1
Error: Different CHARACTER lengths (5/4) in array constructor at (1)

範囲の指定

文字の配列および文字列ともに,その一部だけを参照することができます.この参照の方法も,文字の配列と文字列で異なります.

文字の配列から1要素だけを参照するには,他の型の配列と同様に変数名(要素番号)とします.範囲を指定するには,部分配列と同様にコロンの左右に配列要素番号を書きます.始点:終点:増分というようにして増分を指定することもできます.部分配列の範囲が配列要素の下限あるいは上限と一致する場合は,記述を省略できます.文字列を逆順に並べるには,上限:下限:-1と参照します.

program main
    implicit none

    character :: b(5) ! 文字の配列(要素数5)

    b = ['a','b','c','d','e']
    print *,b         ! abcde
    print *,b(2)      ! b
    print *,b(2:4)    ! bcd
    print *,b(::2)    ! ace
    print *,b(5:1:-1) ! edcba
end program main

文字列の場合,文字列のある特性の範囲(便宜上,部分文字列とよぶことにします)の参照にはコロンを使う必要があります.1文字だけ参照する場合でも,コロンを用いて範囲を明示します.増分を指定することはできません.

program main
    implicit none

    character(5) :: c  ! 長さ5の文字列

    c = "abcdefg"
    print *,c          ! abcde
    !print *,c(2)      ! コンパイルエラー
    print *,c(2:2)     ! b
    print *,c(2:4)     ! bcd
    !print *,c(::2)    ! コンパイルエラー
    !print *,c(5:1:-1) ! コンパイルエラー
end program main

コロンを使わずに文字列の要素を参照しようとしたとき(例えばc(2)と書いたとき)の,各コンパイラのエラーメッセージの要約を表に示します.エラーメッセージは三者三様ですが,PGIコンパイラが一番的確でしょうか.文字列と文字の配列が異なることを知っていないと,これらのメッセージから何が悪いかを判断するのはかなり難易度が高いと思います.

コンパイラ エラーメッセージ
Intel cは配列あるいは関数として宣言されていません.
PGI cに対する不正な部分文字列表現です.
gfortran 文法エラーです.

文字列の配列の参照

文字列の配列に保持したある文字列の範囲を参照するのは,あまり直感的ではありません.
例えば,下のプログラムでは,d(2)を表示するとBetaが出力されます.その一部etaを表示しようとd(2,:)と書いても,配列の添字の数が違うというコンパイルエラーが出ます.

program main
    implicit none

    character(5) :: d(5) ! 長さ5の文字列の配列

    d = ["Alpha", "Beta ", "Gamma", "Delta", "Epsil"]
    print *,d(2)   ! Beta␣
    print *,d(2,:) ! コンパイルエラー
end program main

この場合は,d()()というようにカッコを二つ書き,一つ目のカッコで配列要素,二つ目のカッコで文字列の範囲を指定します.

program main
    implicit none

    character(5) :: d(5) ! 長さ5の文字列の配列

    d = ["Alpha", "Beta ", "Gamma", "Delta", "Epsil"]
    print *,d(2)      ! Beta␣
    print *,d(2)(2:4) ! eta
end program main

文字列と文字の結合

文字の配列と文字列を//によって結合すると,配列の各要素と文字列全体が結合されます.
文字の配列同士を//によって結合すると,配列の各要素同士が結合されます.そのため,結合する配列の要素数が一致している必要があります.

program main
    implicit none

    character(7) :: lang="Fortran"
    character    :: ver(4)

    ver = ["2","0","0","8"]
    print *,lang//ver              ! Fortran2Fortran0Fortran0Fortran8
    print *,["a","b","c","d"]//ver ! a2b0c0d8
end program main

文字列に関する組込関数

文字列の長さ

文字列の長さを取得する関数や空白を調整する関数が用意されています.

関数 機能
len(文字列) 空白を含む文字列の長さを返す
trim(文字列) 文字列の末尾の空白を削除した文字列を返す
len_trim(文字列) 末尾の空白を削除した文字列の長さを返す
adjustl(文字列) 文字列の長さを変えず,左に寄せた文字列を返す(先頭の空白を末尾に詰める)
adjustr(文字列) 文字列の長さを変えず,右に寄せた文字列を返す(末尾の空白を先頭に詰める)

使う頻度はlen()trim()が高いでしょう.先頭にある空白を削除するには,一度adjustl()で空白を末尾に詰めて,trim()で削除します.関数を知った当初,先頭に空白のある文字列なんてどこから出てくるのだろうと思っていましたが,文字列と整数・実数の相互変換を行うと出てくるこ場合があることが確認できました.

program main
    implicit none

                        !1234567890123456789012345
    character(27) :: str = "  functions for character"

    print *,str      ! ␣␣functions for character␣␣
    print *,len(str) ! 27

    print *,trim(str)     ! ␣␣functions for character
    print *,len_trim(str) ! 25

    print *,adjustl(str) ! functions for character␣␣␣␣
    print *,adjustr(str) ! ␣␣␣␣functions for character

    print *,len_trim(adjustl(str)) !23

end program main

文字列の検索

文字列の中から特定の文字列や複数の文字を検索するための関数が用意されています.含まれない文字の検索もできます.

関数 機能
index(被検索文字列, 検索文字列 [,back={.false.,.true.}, kind=整数型種別]) 被検索文字列から検索文字列を検索し,その文字の位置をinteger(整数型種別)で返す
back=.true.とすると,後方から検索する
scan(被検索文字列, 検索文字集合 [,back={.false.,.true.}, kind=整数型種別]) 被検索文字列から検索文字集合に含まれるいずれかの文字を検索し,その文字の位置をinteger(整数型種別)で返す
verify(被検索文字列, 検索文字集合 [,back={.false.,.true.}, kind=整数型種別]) 被検索文字列から検索文字集合に含まれない文字を検索し,その文字の位置をinteger(整数型種別)で返す
program main
    implicit none

                        !1234567890123456789012345
    character(27) :: str = "  functions for character"

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

    print *,scan(str, "for")             !  3 ! 文字の集合f,o,rのうち,fがfunctionのfと一致したので,その位置
    print *,scan(str, "for",back=.true.) ! 25 ! 後ろから検索し,rがcharacterのrと一致したので,その位置

    print *,verify(     str , "acefhiorstu",back=.true.) ! 27 ! 文字の集合に含まれない文字(空白)がstr末尾の空白に存在するので,その位置
    print *,verify(trim(str), "acefhiorstu",back=.true.) ! 16 ! trimによって末尾の空白を削除したので,forとcharacter間の空白の位置
end program main

文字コード関連

文字と文字コードの相互変換や,文字コードを基準に文字列の大小を比較する関数もあります.

関数 機能
char(i=整数[,kind=文字型種別]) 整数をシステム既定の文字セットに沿って変換した文字(character(len=1))を返す
kindは文字型の種別
ichar(c=文字[,kind=整数型種別]) 文字をシステム既定の文字セットに沿って変換した数字を返す
kindは戻り値の整数型の種別
achar(i=整数[,kind=文字型種別]) 整数をASCIIコードに沿って変換した文字(character(len=1))を返す2
iachar(c=文字[,kind=整数型種別]) 文字をASCIIコードに沿って変換した数字を返す2
lgt(string_a=文字列1, string_b=文字列2) 文字列1と文字列2を先頭から比較していき,文字列1のASCIIコードが文字列2より大きければ.true.を返す
文字列の長さが異なる場合は,短い文字列の末尾に空白が追加される
lge(string_a=文字列1, string_b=文字列2) 文字列1のASCIIコードが文字列2より大きいか,文字列1と文字列2が等しければ.true.を返す
llt(string_a=文字列1, string_b=文字列2) 文字列1のASCIIコードが文字列2より小さければ.true.を返す
lle(string_a=文字列1, string_b=文字列2) 文字列1のASCIIコードが文字列2より小さいか,文字列1と文字列2が等しければ.true.を返
program main
    implicit none

    print *, char(Z'41')  ! A
    print *, char(i=65)   ! A
    print *,ichar(c="A")  ! 65
    print *, achar(i=97)  ! a
    print *,iachar(c="a") ! 97

    print *,lge(string_a="aa",string_b="aa"),lle(string_a="aa",string_b="aa") ! T T 
    print *,lgt(string_a="aa",string_b="aa"),llt(string_a="aa",string_b="aa") ! F F

    print *,lge(string_a="ab",string_b="aa"),lle(string_a="aa",string_b="ab") ! T T ! 1文字目は"a"で等しいので,2文字目のASCIIコードの大小で比較
    print *,lgt(string_a="ab",string_b="aa"),llt(string_a="aa",string_b="ab") ! T T
end program main

エスケープシーケンス

FortranでC言語スタイルのエスケープ文字を使うには,コンパイルオプションを付与する必要があります.当該オプションをつけないと,エスケープ文字に用いるバックスラッシュ\が通常の文字として解釈されるからです.以下のコンパイルオプションによってそれを変更します.

コンパイラ オプション
Intel /assume:bscc
PGI -Mnobackslash
gfortran -fbackslash

確認できたエスケープシーケンスを表に示します.コンパイラによって対応状況が異なりますが,基本的なエスケープシーケンスは利用できます.

エスケープシーケンス 意味
\0 NULL文字
\a ビープ音
\b バックスペース
\f フォームフィード(改ページ)
\n 改行
\r キャリッジリターン(行頭復帰)
\t 水平タブ
\v 垂直タブ3
\o 8進数で書かれたASCIIコードoを文字に変換4
\xh 16進数で書かれたASCIIコードhを文字に変換56
\uhhhh 16進数で書かれたunicodehhhhを文字に変換7
\Uhhhhhhhh 16進数で書かれたunicodehhhhhhhhを文字に変換7
\\ バックスラッシュ(\)
\' シングルクオート(')48
\" ダブルクオート(")49
\? クエスチョンマーク(?)10
program main
    implicit none

    print *,"line 1 line 2"      ! line 01 line 2
    print *,"line 1 \nline 2"    ! line 01         ! 書式なしで出力すると左に1文字スペースが入る
                                 !line 2           ! 改行後はそのスペースが入らないので,2行目は1文字左に寄る
    print *,"line 01 \rline 2"   !line 201         ! ␣line 01を表示後に行頭に復帰し,line 2を表示したので上書きされる
    print *,"line 01 \0line 2"   ! line 01  line 2
    print *,"line 01 \bline 2"   ! line 01line 2
    print *,"line 01 \fline 2"   ! line 01  line 2
    print *,"line 01 \tline 2"   ! line 01        line 2
    print *,"line 01 \\line 2"   ! line 01 \line 2
    print *,"line 01 \'line 2"   ! line 01 'line 2
    print *,"line 01 \"line 2"   ! line 01 "line 2 ! Intelコンパイラ,gfortranではコンパイルエラー
    print *,'line 01 \"line 2'   ! line 01 "line 2
    print *,'line 01 \x27line 2' ! line 01 'line 2 ! gfortranではコンパイルエラー
    print *,"line 01 \x22line 2" ! line 01 "line 2 ! gfortranではコンパイルエラー
    print *,'line 01 \47line 2'  ! line 01 'line 2
    print *,"line 01 \42line 2"  ! line 01 "line 2
    print *,"line 01 \u0026line 2"     ! line 01 &line 2 ! gfortranのみ対応
    print *,"line 01 \U0000007eline 2" ! line 01 ~line 2 ! gfortranのみ対応
end program main

文字列がNULL文字で終わるC言語とは異なり,Fortranの文字列はNULL文字で終わりません.この違いから,Fortranでは文字列の途中にNULL文字があっても,文字列はすべて出力されます.コンソール出力ではスペースが一つ追加されただけのように見えますが,ここにはNULL文字のコードが存在しています.そのため,上記プログラムのコンソール出力をコピーしてどこかに貼り付けようとしても,NULL文字の手前までしか貼り付けられません.

コンパイルオプションを利用しない場合は,ASCIIコードをachar()で文字に変換し,//で結合します.

エスケープシーケンス ASCIIコード 意味
\0 00 NULL
\a 07 ビープ音
\b 08 バックスペース
\f 0c フォームフィード(改ページ)
\n 0a 改行
\r 0d キャリッジリターン(行頭復帰)
\t 09 水平タブ
\v 0B 垂直タブ3
\\ 5c バックスラッシュ(\)
\' 27 シングルクオート(')
\" 22 ダブルクオート(")
program main
    implicit none
    print *,"line 01 line 2"                   ! line 01 line 2
    print *,"line 01 "//achar(z'0a')//"line 2" ! line 01
                                                !line 2
    print *,"line 01 "//achar(z'0d')//"line 2" !line 201
    print *,"line 01 "//achar(z'00')//"line 2" ! line 01  line 2
    print *,"line 01 "//achar(z'08')//"line 2" ! line 01line 2
    print *,"line 01 "//achar(z'0c')//"line 2" ! line 01  line 2
    print *,"line 01 "//achar(z'09')//"line 2" ! line 01        line 2
    print *,"line 01 "//achar(z'5c')//"line 2" ! line 01 \line 2
    print *,"line 01 "//achar(z'27')//"line 2" ! line 01 'line 2
    print *,"line 01 "//achar(z'22')//"line 2" ! line 01 "line 2
end program main

文字,文字列の動的割付

配列と同様に,変数にallocatable属性を付与して,文字列の長さあるいは文字の配列要素数を動的に決めることができます.allocatable属性を付与するときは,文字の長さ,あるいは要素数の代わりに:を用いて型宣言を行います.

allocatable属性をつけた配列の割り付けには関数allocateを用い,解放にはdeallocateを用います.配列や文字列が既に割り付けられているかどうかは,関数allocated()を利用して確認します.

文字列を割り付ける際,source指定子でクローンを作らないのであれば,型を明示して割り付ける必要があります.

allocate(character(長さ) :: 文字列変数) !型付き割付
program main
    implicit none

    character(:),allocatable :: str
    character,allocatable :: array(:)

    allocate(str, source = "dynamic string")
    allocate(array, source = ["d","y","n","a","m","i","c"," ","a","r","r","a","y"])

    print *,str   ! dynamic string
    print *,array ! dynamic array

    print *,allocated(str),allocated(array) ! T T

    deallocate(str)
    deallocate(array)

    allocate(character(14) :: str) !型付き割付
    print *,len(str) ! 14  
    deallocate(str)
end program main

自動再割付文字列

先日の記事で,allocatableな配列に大きさ(要素数)の異なる配列を代入すると,自動で再割付されることを説明しました.この挙動は文字列でも同じで,allocatableな文字列変数に文字列リテラルあるいは変数が代入されると,文字列の長さに応じて自動で割り付けられます
この自動再割付文字列は非常に便利です.文字列の適切な長さを考える,あるいはtrim()で空白を除去する手間を省くことができます.ただし,型宣言時に代入することはできません.

program main
    implicit none

    character(:),allocatable :: str !="initialize" 宣言時の初期化は不可能

    print *,len(str)       ! 0  ! gfortranは32767
    str = "dynamic string"
    print *, str, len(str) ! dynamic string          14

    str = "string is not an array of characters"
    print *, str, len(str) ! string is not an array of characters          36
end program main

自動再割付文字列strの長さは最初0(gfortranのみ32767)ですが,文字列リテラルを代入するとその長さに応じてstrの長さも変わっています.また,deallocateせずに別の文字列リテラルを代入しても正しく代入が行われていることが確認できます.

文字列の反転

文字の配列と文字列は似ているようで大きく違いますが,ビット列としてみると同じです.そのためtranfer関数を用いれば,文字列と文字の配列を相互に変換できます.この性質と自動再割付配列を利用して,Reverse a stringという文字列を反転する問題を解いてみました11.繰り返し処理を書くことなく,任意の長さの文字列を反転できます.

program main
    implicit none

    character,allocatable :: a(:)
    character(:),allocatable :: b
    integer :: nb

    b = "abcdefg" ! 文字列を動的に割り付け
    nb = len(b)   ! 文字列の長さ

    allocate(a(nb))             ! 文字の配列を割り付け
    a = transfer(b, a, size=nb) ! 文字列を文字の配列に変換
    b = transfer(a(nb:1:-1),b)  ! 逆順に参照した文字の配列を文字列に戻す
    print *,b                   ! gfedcba
end program main

もう少し行数を減らすことは可能ですが,このプログラムは,PGIコンパイラではコンパイルできません.

program main
    implicit none

    character,allocatable :: a(:)
    character(:),allocatable :: b

    b = "abcdefg" ! 文字列を動的に割り付け

    allocate(a(len(b)),source = transfer(b, ["a"], size=len(b))) ! 文字列から変換された文字の配列のクローンを作成
    b = transfer(a(len(b):1:-1), b)                              ! 逆順に参照した文字の配列を文字列に戻す
    print *,b                                                    ! gfedcba
end program main

さらにallocate()で割り付ける際にa(len(b))の配列要素数を省略できそうですが,そうすると正しく動作する(仕様的に正しいかどうかではなく,反転した文字列を返す)のはIntelコンパイラだけになります.

文字列と整数・実数の相互変換

この記事の上の方で,ASCIIコードと文字の相互変換の関数を紹介しました.Fortranには整数あるいは実数と文字列の相互変換を行う関数は存在していないようです.ですが,非常に簡単な処理で変換できます.

整数・実数から文字列への変換にはwrite文,文字列から整数・実数への変換にはread文を用います.それぞれ出力と入力を行うための文(statement)です.write文,read文は共に,装置番号と書式を指定し,その情報を基に変数を出力または入力します.

write(装置番号,書式) 変数あるいはリテラル
read(装置番号,書式) 変数

その他,数多くのパラメータがありますが,それらが省略可能であるのに対し,装置番号と書式は省略できません.変数に適した書式で画面出力をする場合,あるいはキーボード入力を適した書式で変数に保持する場合には,アスタリスク*を用います.

write(*,*) 変数 ! 変数の値を画面に出力する
read(*,*) 変数  ! キーボードからの入力を変数に代入する

装置番号という呼称は,おそらく過去の状況を引き継いでいるだけで,現在はファイル等と装置番号を対応づけて利用します.例えば,多くの処理系では,標準入力に5番,標準出力に6番が充てられています12.この装置番号に変数を与えることで,整数・実数と文字列の変換が行われます.このような変数の使い方を内部ファイルとよび,内部ファイルへデータを出力するwrite文を内部write文,内部ファイルからデータを入力するread文を内部read文とよぶようです13

下記プログラムでは,整数と文字列の相互変換,実数と文字列の相互変換を行っています.整数の桁に対して文字列が長いように感じると思います.例えば整数100という3桁の数を文字列にするなら,文字列の長さは3で十分なように思われます.しかし,書式を指定しないと実行時にオーバーフローするので,変数の型の最長桁数を保持できる程度の長さを持たせておき,必要に応じてadjustl()trim()で空白を削除します.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    character(18)  :: str
    integer(int32) :: i
    real(real32)   :: a

    i = 100
    write(str,*) i    ! 内部write文で整数を文字列に変換
    print *,trim(str) ! 100

    str = "65535"
    read(str,*) i     ! 内部read文で文字列を整数に変換
    print *,i         ! 65535

    a = 3.1415926
    write(str,*) a    ! 内部write文で実数を文字列に変換
    print *,trim(str) ! 3.141593

    str = "0.62831852e1" ! 指数表記でも変換可能
    read(str,*) a        ! 内部read文で文字列を実数に変換
    print *,a            ! 6.283185
end program main

この程度で変換できるのであれば,関数を用意しないのも納得できる気がします.ただ変換するだけでなく,write文,read文の書式を指定することで,さらに柔軟に変換できます.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    character(8)  :: str
    integer(int32) :: i
    real(real32)   :: a

    i = 100
    write(str,'(I8.7)') i ! iを8桁で表示し,上位7桁まで0を詰める
    print *,str           ! ␣0000100

    a = 3.1415926
    write(str,'(E8.1)') a      ! aを指数表記し,小数点以下は1桁だけ表示する
    print *,str                ! ␣0.3E+01
    print *,trim(adjustl(str)) ! 0.3E+01

end program main

ただし,内部ファイルに自動再割付文字列を用いたとしても,残念ながら自動で割り付けられません

書式は,()の間に書式指定文字を並べて表現します.書式は文字列でなければならないので,(書式指定)全体を'または"で囲みます.書式指定文字の後の数字で表示する桁数を調整します.書式指定子に大文字小文字の区別はありませんが,著者はほとんどの場合大文字で書いています.

書式指定文字 表示幅(w)の指定 表示内容
I Iw
Iw.d14
整数(10進数)
B Bw
Bw.d14
整数(2進数)
O Ow
Ow.d14
整数(8進数)
Z Zw
Zw.d14
整数(16進数)
F Fw.d15 実数
E Ew.d15
Ew.dEe16
実数(指数表記)
A Aw 文字
L Lw 論理値
X wX 水平方向文字送り
$ - 書式末尾に置いて改行を抑制17

計算ステップを含んだファイル名の設定

数値計算では,計算した結果をファイルに出力して結果を可視化し,計算が正しく行われているかを確認します.計算している現象が時間的に変化する場合,同一ファイルに結果を上書きすると,時間変化の過程を確認することができません.そのため,ファイル名に時間の情報を含めて,個別のファイルとして出力します.時間の情報は実数でも構いませんが,小数点が入るとWindowsでは面倒なことになるので,繰り返し計算の回数(時間ステップ)に基づいてファイル名を決定します.

物性値名//時間ステップ//拡張子という名前を付けようとすると,時間ステップを整数から文字列に変換する必要があります.下のプログラムでは,時間ステップの桁digit_stepを一度文字str_digitに変換し,書式指定文字や括弧などと結合して書式fmt='(I8.8)'を作っています.少し手間のかかるやり方ですが,時間ステップの桁が変わっても(10桁未満であれば)プログラムを変更する必要はありません.

時間ステップの桁を繰返し回数から自動的に取得するには,繰返し回数の最大値が例えばNmaxであれば,int(log10(Nmax))+1を計算します.

program main
    use,intrinsic :: iso_fortran_env
    implicit none

    integer(int32),parameter :: digit_step = 8        ! 時間ステップの桁

    character(16),parameter  :: variable = "pressure" ! 出力する物性値
    character( 4),parameter  :: extension = ".dat"    ! ファイル拡張子
    character(digit_step)    :: step                  ! 時間ステップ
    character(:),allocatable :: filename              ! ファイル名

    character(:),allocatable :: fmt                   ! 時間ステップを文字列に変換する時の書式
    character(1)             :: str_digit             ! 桁数(文字型)

    integer(int32),allocatable :: n                   ! 時間ステップ用カウンタ変数

    write(str_digit,'(I1)') digit_step                ! 桁数を文字に変換
    fmt = "(I"//str_digit//"."//str_digit//")"        ! 書式(I8.8)を構成

        !例えばn = 234500ステップ目の計算

        : !
        : ! この辺で色々な計算を行う
        : !

        !ファイル出力のためにファイル名を定める
        write(step,fmt) n                             ! 00234500
        filename = trim(variable)//step//extension    ! 物性値,時間ステップ,拡張子を結合
        print *, filename                             ! pressure00234500.dat

        !この辺でファイル出力

end program main

書式は文字列なので,表示幅に変数を利用することはできません.古い拡張では,<>で変数を挟むことで,表示幅に変数を利用できました.

        write(step,"(I<digit_step>.<digit_step>)") n  ! 00234500

仮引数

文字の配列を手続(サブルーチン,関数)に渡す場合,仮引数の定義は他の型の配列と同様に形状引継配列を使っておけば問題ありません.

文字列を手続に渡す場合,仮引数の型の定義,正確には文字列の長さにコロンを用いることはできません.character(:)は必ずpointer属性かallocatable属性とともに宣言しなければなりません.コロンの代わりに用いる文字の長さには,実引数の文字列の長さが固定ならその文字長さを,長さの異なる様々な文字列を実引数に取る可能性があるならアスタリスク*を用います.

program main
    implicit none

    character(:),allocatable :: filename

    filename = "pressure00234500.dat"
    print *,filename                            ! pressure00234500.dat

    call changeExtension(filename,"txt")
    print *,filename                            ! pressure00234500.txt

    contains
    subroutine changeExtension(str,extension)
        implicit none
        character(*) :: str                     ! 文字列の長さが決まっていなければ*を用いる
        character(3) :: extension               ! 文字列の長さが決まっていればその長さを用いる

        str(len(str)-2:len(str)) = extension(:) ! 末尾3文字を上書き
    end subroutine changeExtension
end program main

まとめ

Fortranにおける文字の配列と文字列は,宣言方法がわずかに異なるだけですが,挙動は大きく異なります.文字列はcharacter型から分離して,string型など専用の型にした方がよいのではないかと思います.


  1. http://www.nag-j.co.jp/fortran/FI_4.html 

  2. システム既定の文字セットがASCIIコードであれば,characharichariacherは同じ. 

  3. テストした環境では機能しませんでした. 

  4. gfortranは未対応.\も含めて文字列がそのまま出力される. 

  5. gfortranは未対応.コンパイルエラーになる. 

  6. PGIコンパイラは未対応.\が消えるだけで,以降の文字列がそのまま出力される. 

  7. gfortranのみ対応しているが,ASCIIコード範囲しか対応していない.IntelとPGIコンパイラでは\が消えるだけで,以降の文字列がそのまま出力される. 

  8. Intelコンパイラおよびgfortranでは,シングルクオートで囲んで文字を作っている場合はコンパイルエラーになる. 

  9. Intelコンパイラおよびgfortranでは,ダブルクオートで囲んで文字を作っている場合はコンパイルエラーになる. 

  10. 文字列内に?を直接書いた場合と同じ. 

  11. Reverse a stringという名前に厳格に従っています.Reverse an array of charactersを許容してくれるのであれば,a(len(a):1:-1)で終わりです. 

  12. iso_fortran_envモジュールでは,標準入力の装置番号としてinput_unit,標準出力の装置番号としてoutput_unitという名前の定数を定義しています.装置番号を指定する際のユーザー側の任意性が排除されつつあります. 

  13. これらのことから,write文,read文の役割は,メモリの内容を装置へ転送する,装置の内容をメモリへ転送すると表現した方が適切かもしれません. 

  14. dは0を詰める桁数. 

  15. dは小数点以下の桁数. 

  16. eは指数の桁数. 

  17. non-advancing I/O(write(*,*,advance="no"))を使うのがイマドキのやり方です.ですが,Intelコンパイラはadvance="no"の出力を随時出力せず,改行が現れた時に一気に出力します.write文が実行されたタイミングで改行なしで表示するには,$を使うしかありません.また,print文で改行を抑制するにも,$を使うしかありません.