k3kaimu
10/14/2013 - 9:56 AM

d-manual::string.md

d-manual::string.md

文字列

文字とは'a'とか'@'のことです。この文字が連なり文字列となります。 配列が操れるあなたにとってD言語の文字列操作はすごく簡単です。

文字コード

あなたがいつもプログラムを打ち込んでいるテキストエディタはどうやって文字を区別しているでしょうか? つまり、「文字に対してどのように値を割り当ててるのでしょうか?」ということです。 実はこれが文字型の値です。

実際、文字は文字コードと呼ばれる符号によって1バイトから4バイト程度まで(連結文字を含めるとそれ以上の)数値が割振られています。 文字コードにはいろいろあって、例えばASCIIやShift JIS, UTF-8, UTF-16, UTF-32などがそれです。

D言語では、文字がUTF-8やUTF-16, UTF-32でエンコーディングされていることを前提に設計されています。 それぞれchar, wchar, dcharという型に対応します。 例として、'a', 'A', '@', '&'の文字それぞれのUTF-8での値は10進数表記で以下のようになっています。

'a' => 97
'A' => 65
'@' => 64
'&' => 38

では、様々な文字のUTF-8, 16, 32でのエンコーディングを見てみましょう。

// example00402.d
// このプログラムを理解する必要はありません。
import std.stdio;
import std.typetuple;

void main()
{
    foreach(S; TypeTuple!(string, wstring, dstring)){
        S[] ss = ["a", "A", "@", "&", "あ", "ア", "阿"];

        writeln(S.stringof);
        foreach(s; ss)
            writefln("\t"`%(%s%) => %(%02X%)`, [s], (cast(immutable(ubyte)[])s).dup.reverse);
        writeln();
    }
}
string
    "a" => 61
    "A" => 41
    "@" => 40
    "&" => 26
    "あ" => 8281E3
    "ア" => A282E3
    "阿" => BF98E9

immutable(wchar)[]
    "a" => 0061
    "A" => 0041
    "@" => 0040
    "&" => 0026
    "あ" => 3042
    "ア" => 30A2
    "阿" => 963F

immutable(dchar)[]
    "a" => 00000061
    "A" => 00000041
    "@" => 00000040
    "&" => 00000026
    "あ" => 00003042
    "ア" => 000030A2
    "阿" => 0000963F

結果を見ればわかりますが、UTF-8は最低1byte, UTF-16は最低2byte, UTF-32は最低4byteになっています。 また、文字コードが可変長であることがわかりました。 UnicodeについてはTDPLがわかりやすいので参考にしましょう。

文字リテラルと文字列リテラルと型

説明するよりも以下のコードを見たほうがわかりやすいでしょう。

char c = 'a';               // 文字リテラル
                            // ''の中に1文字

wchar wc = 'あ';             // wcharはUTF-16
dchar dc = 'う';             // dcharはUTF-32

string str = "ふぉおお";        // UTF-8
wstring wstr = "ばああ";       // UTF-16
dstring dstr = "ほげええ";      // UTF-32

auto _str = "foo";          // string型
auto _wstr = "foo"w;        // wstring型
auto _dstr = "foo"d;        // dstring型

str = [c];                  // string == immutable(char)[]なので
wstr = [wc];                // wstring == immutable(wchar)[]なので
dstr = [dc];                // dstring == immutable(dchar)[]なので

str = `これも文字列リテラル(WYSIWYG: What You See Is What You Get, (訳)見たものが手に入るものである)`;
wstr = `この中では、エスケープシーケンス(後述)は使えない`;
dstr = r"これもWYSIWYG文字列リテラルなので、エスケープシーケンスを使えない";

各文字コード間で変換するにはstd.conv.toが便利です。 std.utf.toUTF8, std.utf.toUTF16, std.utf.toUTF32でも同様に変換可能です。

import std.conv;

void main()
{
    string str = "ふうううう";
    wstring wstr = str.to!wstring();
    dstring dstr = str.to!dstring();
    str = dstring.to!string();
}

改行文字と制御文字

テキストデータではどのようにして改行の情報を保持していると思いますか?

実は改行の情報も1文字(Windowsでは2文字)と数えられているのです。 改行文字(列)はLF(Line Feed)と呼ばれたり、CRLF(Carriage Return, Line Feed)と呼ばれます。 Windowsでは通常CRLFで表されますが、LinuxやMacではLFです。

LFは"\n"、CRLFは"\r\n"で表します。 writelnを使えば改行有りで表示しましたが、writeを使えば改行されません。 また、stdstdio.write系関数ではWindowsでも"\n""\r\n"へと内部で変換されるので、write("\n");と書けば改行されます。

import std.stdio;

void main()
{
    write("改行なし");
    write("\n");          // 改行文字の出力
    write("改行\nあり");
}
改行なし
改行
あり

つまり文字列リテラルでは、改行したい位置に\nを入れておけばよいのです。 このような\から始まる文字列をエスケープシーケンスといい、制御文字などを表します。 文字リテラル''もしくは文字列リテラル""の中でのみエスケープシーケンスは有効です。

代表的なエスケープシーケンスを以下に示しておきます。

\r          => CR
\n          => LF
\t          => タブ
\\          => 文字としての`\`
\"          => 文字としての`"`
\'          => 文字としての`'`
\0          => ヌル文字
\xHH        => UTF-8の16進数表記
\uHHHH      => UTF-16の16進数表記
\UHHHHHHHH  => UTF-32の16進数表記
import std.stdio;

void main()
{
    writeln("\"");                  // " <- の表示
    writeln(`"\n\r\n\0\\x00`);      // ``で囲まれた文字列リテラル内ではエスケープシーケンスは無効
    writeln(x"31 23 44");           // "\x31\x23\x44"と同じ
}
"
"\n\r\n\0\\x00
1#D

基本的な文字列操作

文字列と配列

文字列は、「文字の列」という名前の通り文字が順番に並んだものです。 string, wstring, dstringは実際にはそれぞれimmutable(char)[], immutable(wchar)[], immutable(dchar)[]に付けられた別名です。 長い型を注意深く観察すれば、文字列型は文字型の配列であることがわかりますね。 つまり、文字列もint[]などの配列と同様に扱うことができます。

文字列の先頭の文字を得るには?

文字列は実は配列だというのは先ほど分かりましたが、配列の先頭要素はarr[0]で取得できましたね。 dstringはそれでも問題無いのですが、stringwstringはそれではダメなのです。

// example00404.d
import std.stdio;

void main()
{
    string str =   "山田太郎";
    wstring wstr = "鈴木次郎"w;
    dstring dstr = "斎藤三郎"d;

    writeln(str[0]);
    writeln(wstr[0]);
    writeln(dstr[0]);
}
$ rdmd exmaple00404
?
鈴
斎

上記例ではstringのみ失敗しましたが、wstringでも実はwstr[0]は安全ではありません。 通常は以下のようにstd.array.frontを使います。

// example00405.d
import std.array, std.stdio;

void main()
{
    string str =   "山田太郎";
    wstring wstr = "鈴木次郎"w;
    dstring dstr = "斎藤三郎"d;

    writeln(str.front);
    writeln(wstr.front);
    writeln(dstr.front);
}
$ rdmd example00405
山
鈴
斎

ちゃんと表示できましたね。 文字列型に対するstd.array.frontが返す値の型はstring, wstring, dstring関係なくdchar型であることに注意しなければいけません。

あと注目して欲しいのは、front(str)でなくてstr.frontなことです。 これはUFCS(Uniform Function Call Syntax)といい、「関数呼び出しがfoo(a, b, c, ...)などの場合に、a.foo(b, c, ...)と書ける」記法です。 またfoo(a)はUFCSでa.foo()となりますが、D言語の特徴で()は外してもいいのでa.fooとなります。 以前はaが配列(stringは配列と言いましたね)の場合だけに許された記法でしたが、dmd 2.059でUFCSとしてどのような型でも可能になりました。

「じゃあ、front(str)と書くのはいけないのか?」という話ですが、結論からいうとD言語のお作法的にダメです。 次の項で説明するpopFrontemptyもUFCSを使って書くのがお作法です。 これはRangeという考え方に沿っていますが、このRangeについて説明するのはかなり後になるでしょう。

2文字目以降を得る

山田さんの"山"だけ取得できてもそんなに嬉しくないので、"田"も取ってみたいところです。 すこし変更して、"田"以降の各文字も出力できるようにしてみましょう。

// example.d
import std.array, std.stdio;

void main()
{
    string str = "山田太郎";

    writefln("%s : %s", str.front, str);
    str.popFront();
    writefln("%s : %s", str.front, str);
    str.popFront();
    writefln("%s : %s", str.front, str);
    str.popFront();
    writefln("%s : %s", str.front, str);

    writeln(str.empty);     // 文字列が空か? => false
    str.popFront();
    writeln(str.empty);     // 文字列が空か? => true
}
$ rdmd example
山 : 山田太郎
田 : 田太郎
太 : 太郎
郎 : 郎
false
true

山田太郎さんの4文字すべて列挙できました!

std.array.popFrontを使えば、文字列の先頭から1文字削除することができます。 frontの場合とは違い、popFrontstr.popFront()という風に最後に()を付けるのがお作法です。

std.array.emptyを使えば文字列が空かどうか判定できます。 文字列に文字が一切含まれていないなら、この関数はtrueを返します。

文字列からある部分を取り出す

実はstring型の"山田太郎"から、真ん中の"田太"を抜き出すのは多少手間がかかります。 もしdstring型であれば、配列のスライス演算子によってa[1 .. 3]とできるのですが、string型ではそのようなことができません。 非常に残念ですね。

え?スライス演算子を忘れた? そんな方は、配列の章まで戻って勉強しましょう!

さて話を戻して、もしstring型に入っている文字が半角英数字だと仮定できるなら、a[1 .. 3]としても大丈夫です。 つまり、"yamada taro""da ta"を取り出すことは簡単なのです。 なぜなら、半角英数字であれば各文字は1byte、つまりstringの要素であるcharに収まり、「1文字 == 配列の一要素」となるからです。 2バイト文字と呼ばれる日本語などはUTF-8では1byte以上で表されることは、この章の最初のほうで説明しましたね。 つまり、「1文字 == 配列の一要素」という規則は通用しません。

では簡単なyamada taroda taを取得することをしてみましょう。 yamadaのdは先頭から5文字目で、taroaは先頭から9文字目です。 なので次のソースコードを動かせばda taが手に入ります。

import std.stdio;

void main()
{
    string str = "yamada taro";

    writeln(str[5-1 .. 9]); // da ta
}

5-1としている理由や、str[5-1 .. 9]はわかりますよね? 分からない場合は、「配列の章まで戻って勉強し直しの刑」に処されるべきです

しかし、この方法は2byte文字などが使われないという保証がある場合にのみ使うべきです。 保証できない場合には、やはりto!dstringdstringに変換するか、Rangeインターフェースを用いるのがよいでしょう。

// example.d
import std.stdio;

import std.range    : take, drop;
import std.conv     : to;


void main()
{
    string str = "山田太郎";
    writeln(str.drop(1).take(2));       // 先頭から1文字削って2文字取得した文字列
    writeln(str.to!dstring()[1 .. 3]);  // dstringに変換してからスライス演算子で取得
}
$ rdmd example
田太
田太

文字列を結合する

文字列は配列なので、配列と同様にa ~ bという結合演算子によって結合を行います。

// example.d
import std.stdio;


void main()
{
    string str = "foo";

    writeln(str ~ str ~ str);

    str ~= str[1 .. $] ~ str[0 .. 2];

    writeln(str);

    writeln("foo" "bar");       // 実は、文字列リテラルの場合には結合演算子ナシで結合できる
    writeln(`fooo`"\n"`bar`);   // ``の中ではエスケープシーケンスが使えないが""と併用することで使用可能
}
$ rdmd example
foofoofoo
foooofo
foobar
fooo
bar

文字列の比較

配列の比較や順序付けは辞書順でしたので、文字列型についてもその法則が成り立ちます。

"foo" == "bar"      =>      false
"foo" == "foo"      =>      false
"abc" < "bbc"       =>      true
"aaa" < "aab"       =>      true
"aaa" < "aaa"       =>      false
"bar" > "foo"       =>      false
"aaaa" < "aaa"      =>      false

boolへの変換

配列同様に未割り当ての文字列はfalseとなりますが、実際に使う場合には文字列が空かどうかを調べたいので、str.length != 0か、より安全なRangeインターフェースのstd.array.emptyを使って!str.emptyという風に文字列が空かどうかチェックするのがよいでしょう。

import std.stdio;
import std.array;


void main()
{
    string foo;

    writeln(!!foo);             // false
                                // 未割り当てなので

    foo = "foo";
    writeln(!!foo);             // true
                                // 割り当ているので

    foo = foo[0 .. 0];
    writeln(!!foo);             // true
                                // 空でも問題なし

    foo = null;
    writeln(!!foo);             // false
                                // 未割り当て状態

    foo = "bar";

    writeln(foo.empty);         // false
                                // fooは空でない

    writeln(foo[0 .. 0].empty); // true
                                // [0 .. 0]なので空
}

少しレベルアップした文字列操作

Phobosは素晴らしい機能がたくさん搭載された標準ライブラリなので、文字列もある程度簡単に行うことができます。 文字列に関する便利な関数を探す場合には、std.algorithm, std.ascii, std.range, std.string, std.uni, std.utfを覗いてみましょう。 また、正規表現(Regular Expression)という素晴らしい機能についてはstd.regexを使いましょう。

もし、文字列からある型の値などへ変換したい場合、あるいは様々な型の値から文字列へ変換したいのであればstd.conv.toを使用しましょう。 std.conv.toは、あなたが思っている以上の変換を行ってくれるでしょう。

問題 -> 解答

  • 問題1

ASCIIコードという文字を表す符号系列があります。 ASCIIコードは2進数7bit、つまりたった128文字だけしか表現できません。 しかし、英文だけを表現する場合には128文字だけで十分なのです。

D言語の言語仕様として、char型に入る文字はUTF-8でエンコードされていると仮定されると説明しましたが、実はUTF-8はASCIIコードと互換性を持っています。 つまり、ASCIIコードで表すことができる文字列はUTF-8と解釈することも可能です。 逆にUTF-8で書かれた文字列をASCIIとして読み込んだとしても、正常には表示できません。 (このような関係を「UTF-8はASCIIに対して上位互換性がある」といいます。)

さて、問題へ移りましょう。 あなたが行うべきことは簡単です。 ASCIIコードのうち、表示可能な文字、つまり画面上に文字として表示できるアルファベットか数字, もしくは記号の一覧をプログラムを書くことによって求めてください。

あなたが特殊な知識を持っていない限り、この問題を解くためにはPhobosの力が必要でしょう。 さあ、プログラムの見通しが立ったのであれば、Phobosから目的の関数を探してみましょう。

  • 問題2

標準入力から1行取得したいのであれば、std.stdio.readlnを使うのがもっとも便利です。 しかし気をつけなければいけないのは、readlnが返す文字列は終端に改行文字が存在することです。

この問題でもあなたがすべきことは問1よりも明確に理解できるでしょうが、油断してはいけません。 Phobosをあまり使ったことのないD言語初心者にとっては少し難しいかもしれません。 というのも、この問題も解くためにはPhobosのたくさんのモジュール達から適切な関数を探さなければいけないからです。

これからあなたに作ってもらうプログラムは、ユーザーからのたった2行の入力を処理するプログラムです。 1行目は数値、と言っても整数値でint型に収まる大きさとしましょう。 2行目についてはより簡単、int型のリテラルです。

…ええ、つまり2行ともint型のリテラルを要求します。 あなたは、この2行の入力に与えられたint型リテラルをintの数値へ変換し、その合計値を出力すればよいのです。

  • 問題3

正規表現というのは、かの偉大なWikipediaから言葉を借りれば以下の様なものです。

正規表現(せいきひょうげん、regular expression)とは、文字列の集合を一つの文字列で表現する方法の一つである。正則表現(せいそくひょうげん)とも呼ばれ、形式言語理論の分野では比較的こちらの訳語の方が使われる。まれに正規式と呼ばれることもある。

Wikipedia:正規表現より引用

つまり、正規表現というのは文字列によってある程度のパターンを持った文字列を表す方法のことです。 D言語で正規表現を使いたい場合にはstd.regexというモジュールを使います。

この第三問目は正規表現を使って解いてもらうわけですが、std.regexの使い方は説明しません。 しかし、正規表現はD言語に限らず様々な言語で実装されているのでネット上に大量に資料が転がっています。 たとえば「ruby 正規表現」とGoogleなどで検索すれば良いサイトが多数ヒットするでしょう。

肝心の問題ですが、以下の様な仕様です。

入力として文章が与えられる。 この文章中には複数の数値が書かれている。 数値のフォーマットを詳しく書けば次のリストの通りである。

  • 数値は10進数で書かれており、もちろん使用される文字は0123456789である。
  • 負の数を表す場合には先頭に-をつける。また、明示的に正の数を表すために+を付加する可能性がある。
  • 桁区切りとして,一文字を使用できる。区切る桁数は1桁でも2桁でも3桁でもよいが、一桁も桁を区切らないようなカンマの打ち方は禁止されている。
  • 小数点として.を使用している。もちろん文章中に現れる数値らしき文字列1つあたりに小数点が2つ以上存在することはない。つまり、ドキュメント中に1.2.3のような文字列は絶対に出現しないことは保証されている。
  • 小数点以下では,を使って桁を区切ることはない。
  • 小数点以上には、1桁でも数字が無ければいけない。

今回あなたがしなければいけない課題は、ドキュメント中に現れるこのような数値の合計を、小数点以下3桁の精度で精確に表示することである。

以下に文章の例と、その場合の解を示しておく。

  • 例1

    foobarhogehoge123oooxxy3.1415+5-3*123,455,1,0,,123
    

    この場合、123, 3.1415, +5, -3, 123,455,1,0, 123が仕様に適合する。 *は今回は関係なく、また,,は「一桁も桁を区切らないカンマの打ち方」になり、 よってその合計である12345761.1415から±0.001以下の精度で出力すればよい。

  • 例2

    1;2;3;;4.412411:4214221.1412 +.41241
    

    1, 2, 3, 4.412411, 4214221.1412, 41241が数値であり、その合計である4255472.553611の±0.001以下の誤差を含んだ解が正解である。

  • 例3

    +++3-4.34,23,4.5
    

    +3, -4.34, 23,4.5が数値であり、その合計は233.16である。

  • 例4

    fooo1.1.1bar
    

    このような文章は決して現れないので、考慮しなくてよい。

おわりに

D言語の文字列操作の素晴らしさが感じれましたか?

最後の問題は少し難しすぎたかもしれませんが、それでもC言語で実装する場合よりも明らかにソースコードは簡単になるでしょう。 std.algorithmstd.range, std.stringなどのPhobosについてはこれから必要になればちょっとずつドキュメントを覗いてみてください。 たくさんの便利な関数や機能があなたを待っていることでしょう。

キーワード

  • 文字(character)
  • 文字列(string)
  • 文字コード
    • ASCII
    • Unicode; UTF-8, UTF-16, UTF-32
  • 改行文字
  • 制御文字
  • エスケープシーケンス
  • UFCS(Uniform Function Call Syntax)
  • 正規表現

仕様