🎻

[Flutter] Dartの文字列操作チートシート

2021/05/11に公開2

手軽に調べられるようにまとめてみた。使用したDartのバージョンは2.12.4

文字列の基本

Dartの文字列はStringクラス

Dartの文字列は不変(immutable)なので、何らかの処理を行う場合は元の文字列自体が変更されるのではなく、新しい文字列が返される。

String(文字列)はPatternを実装しており、これはRegExp(正規表現)も同様。文字列に関する諸々のメソッドの中には引数としてPatternを渡せるものも多く、これらでは正規表現が使用できる。

Dartに文字型(char的なもの)はなく、全てString(文字列)となる。Stringの中身と言えるUnicodeコードポイント(1文字ごとの整数値)は扱えるものの、使うことはほとんどないと思う。

文字列の定義

文字列には単一行と複数行がある。

文字列の定義にはシングルクォートとダブルクォートの両方が使えるが、一応シングルクォートが標準っぽい。

final s1 = 'シングルクォート';
final s2 = "ダブルクォート"; // (シングルクォートが標準ということで、Lintで警告が出ることがある)
final s3 = 'シングル"ダブル"クォート';
final s4 = "ダブル'シングル'クォート"; // (こういう必要な場合にはダブルクォートでもLintの警告は出ない)

// 複数行の文字列は、3連続クォートを使用する。
final m1 = '''複数行
の
文字列''';

// 複数行の文字列では、開始直後の改行だけは無視される。
final m2 = """
複数行
の
文字列""";
print(m1 == m2); // true

// \n や \t といったエスケープシーケンスが使える。
final m3 = '複数行\nの\n文字列';
print(m1 == m3); // true

// クォートに r を付けることで、エスケープシーケンスを適用しない「生文字列」にできる。
// 正規表現を書くようなときに便利。
final s5 = r'複数行\nの\n文字列ではない'; // 複数行\nの\n文字列ではない

==で、文字列の中身を比較できる。

文字列の連結(結合)と値の埋め込み

単純な文字列の連結は+を使う。

// + で文字列同士を連結できる。
final s1 = 'Dart' + 'は' + 'ダート'; // Dartはダート

// 文字列を並べるだけでも連結できるが、あまり使わない気がする。
final s2 = 'Dart' 'は' 'ダート'; // Dartはダート

文字列への値の埋め込みは、文字列内で${}を使用する。この波括弧の中に変数名や関数等を書く。
波括弧は、なくても問題ないときは省略が推奨されているようで、Lintで指摘される。(Effective Dartにも省略推奨として書かれている

final s3 = 'あいうえお';
print('$s3${s3.length}文字'); // あいうえおは5文字

// 変数の後に英数字が続くようなときは、波括弧でくくる必要がある。なぜならどこまでが変数名か分からないから。
// print('$s3desu'); // (コンパイルエラー)
print('${s3}desu'); // あいうえおdesu
// 後続が記号だったり日本語だったりする場合は波括弧不要。
print('$s3 desu'); // あいうえお desu
print('$s3デス'); // あいうえおデス

// 「$」をそのまま表示したいときはバックスラッシュ(円記号)でエスケープする。
print('\$s3は\${s3.length}文字'); // $s3は${s3.length}文字

単なる値の埋め込みだけではなくフォーマットしたい場合は、sprintfパッケージを使う。

https://pub.dev/packages/sprintf

文字列の長さ等を得る

final s0 = '';
final s1 = 'あいうえお';

// 文字列が空か否かを調べる。
print(s0.isEmpty); // true
print(s1.isEmpty); // false
print(s0.isNotEmpty); // false
print(s1.isNotEmpty); // true

// 文字列の長さを調べる。
print(s0.length); // 0
print(s1.length); // 5

文字列を調べる

final s = 'うえうえしたした';

// 指定した文字列(パターン)を含むか否かを調べる。
print(s.contains('した')); // true
print(s.contains('ひだり')); // false

// 指定した文字列(パターン)で始まるか否かを調べる。
print(s.startsWith('うえ')); // true
print(s.startsWith('した')); // false

// 指定した文字列(パターン)で終わるか否かを調べる。
print(s.endsWith('した')); // true
print(s.endsWith('うえ')); // false

// 指定した文字列(パターン)が最初に出てくるインデックスを取得する
print(s.indexOf('うえ')); // 0
print(s.indexOf('した')); // 4
// 2番目の引数(start)に整数を渡すと、そのインデックスから検索を開始する。
print(s.indexOf('うえ', 1)); // 2
print(s.indexOf('した', 6)); // 6
// 見付からなかったら -1 が返ってくる。
print(s.indexOf('ひだり')); // -1
print(s.indexOf('うえ', 5)); // -1
// start が文字列の長さ(length)より大きいとエラーになる。
// print(s.indexOf('うえ', 99)); // (コンパイルエラー)

// 指定した文字列(パターン)が最後に出てくるインデックスを取得する
print(s.lastIndexOf('うえ')); // 2
print(s.lastIndexOf('した')); // 6
// 2番目の引数(start)に整数を渡すと、そのインデックスより前から検索を開始する。
print(s.lastIndexOf('うえ', 1)); // 0
print(s.lastIndexOf('した', 4)); // 4
// 見付からなかったら -1 が返ってくる。
print(s.lastIndexOf('ひだり')); // -1
print(s.lastIndexOf('した', 3)); // -1
// start が文字列の長さ(length)より大きいとエラーになる。
// print(s.indexOf('うえ', 99)); // (例外発生)

見ての通り、日本語の1文字が「1文字」としてカウントされている。本当はそこに「文字とは何か」というUnicodeのとてもややこしい話が絡んで来るのだけど、それに関しては省略。普通に使う分にはそこまで意識する必要はないはず。

文字列の一部の取得(抽出)

final s = 'あいうえお';

// 指定したインデックスの文字(n文字目)を取得する。Dartに文字型はないので、「1文字の文字列」が返ってくる。
print(s[1]); // い
// 文字列の長さより大きい値を指定すると実行時エラーとなる。
// print(s[99]); // (エラー)

// 指定したインデックス以降の文字列(末尾まで)を取得する。
print(s.substring(2)); // うえお
// 2番目の引数(end)に整数を渡すと、それ未満までの文字列を取得できる。
print(s.substring(2, 4)); // うえ
// endに不正な値(負の値、1番目の引数より小さな値、文字列の長さよりも大きな値)を指定すると実行時エラーとなる。
// print(s.substring(2, -1)); // (エラー)
// print(s.substring(2, 1)); // (エラー)
// print(s.substring(2, 99)); // (エラー)

文字列の分割

split()List<String>を返してくるので、それでループしたりイテレータで処理したりできる。

final s0 = '';
final s1 = 'Hello Dart world';
final s2 = 'あいいう';

// 引数で渡された文字列(パターン)で分割する。
print(s1.split(' ')); // ['Hello', 'Dart', 'world']
print(s2.split('い')); // ['あ', '', 'う']
// 空文字列を渡すと、すべての文字で分割するので、1文字ずつ処理できる。
print(s2.split('')); // ['あ', 'い', 'い', 'う']
// 空文字列に対してsplitすると、以下のような結果になる。
print(s0.split('')); // [] (空のリスト)
print(s0.split('').length); // 0
print(s0.split('a')); // [''] (空の文字列を1個含むリスト)
print(s0.split('a').length); // 1

文字列の置換

以降の変換系の処理では、最初に述べた通り、既存の文字列を置換するのではなく、置換された新しい文字列が返される。

final s = 'うえうえしたした';

// replaceFirst(Pattern from, String to, [int startIndex = 0]) → String
// 最初に見付かった文字列(パターン)fromを、文字列toへ置換する。
print(s.replaceFirst('した', 'ひだり')); // うえうえひだりした
// startIndexを指定すると、そのインデックス以降で最初に見つかったfromをtoへ置換する。
print(s.replaceFirst('した', 'ひだり', 5)); // うえうえしたひだり

// replaceAll(Pattern from, String replace) → String
// 見付かったすべての文字列(パターン)fromを、文字列toへ置換する。
print(s.replaceAll('した', 'ひだり')); // うえうえひだりひだり

空白の削除

削除される空白はホワイトスペース。具体的には半角スペース、改行コード、タブ等が対象。

final s = '  Dart  \n';

// 両端のホワイトスペースを削除する。
print(s.trim()); // 'Dart'

// 左側(文字列の始端側)のホワイトスペースを削除する。
print(s.trimLeft()); // 'Dart  \n'

// 右側(文字列の終端側)のホワイトスペースを削除する。
print(s.trimRight()); // '  Dart'

空白の付加

final s1 = 'short';   // 5文字
final s2 = 'LOOOONG'; // 7文字

// padLeft(int width, [String padding = ' ']) → String
// 引数widthで指定した幅になるように、左側に半角スペースを挿入する。
print(s1.padLeft(7)); // '  short'
print(s2);            // 'LOOOONG'
// widthが文字列の長さよりも短い場合は何もしない。
print(s2.padLeft(1)); // 'LOOOONG'
// 引数paddingで、埋める文字列を指定できる。(paddingを省略した場合はデフォルト値の半角スペース)
print(s1.padLeft(7, '-')); // '--short'
// paddingで埋める回数は、元の文字列の長さとwidthで指定した幅を比較したもの。
// したがってpaddingが1文字ではない場合は、結果的に文字列の長さがwidthとは異なる。
// でもこれは逆にHTML出力時に &nbsp; で揃えるような場合に便利。
print(s1.padLeft(7, '&nbsp;')); // '&nbsp;&nbsp;short'

// padRight(int width, [String padding = ' ']) → String
// 右側に挿入するだけで、動作は上記の padLeft と同様。
print(s1.padRight(7)); // 'short  '
print(s2);             // 'LOOOONG'
print(s2.padRight(1)); // 'LOOOONG'
print(s1.padRight(7, '-')); // 'short--'
print(s1.padRight(7, '&nbsp;')); // 'short&nbsp;&nbsp;'

大文字/小文字の変換

final s1 = 'Dart'; // 半角英数
final s2 = 'Dart'; // 全角英数

// 大文字にする。
print(s1.toUpperCase()); // 'DART'
// 全角英数にも効く。
print(s2.toUpperCase()); // 'DART'

// 小文字にする。
print(s1.toLowerCase()); // 'dart'
// 全角英数にも効く。
print(s2.toLowerCase()); // 'dart'

正規表現の使用

最初に書いた通り、Stringクラスの一部のメソッドには正規表現パターンを渡せる。上記の引数に「パターン」と書いているのがそれ。

final s = 'うえうえしたした';
print(s.split(RegExp(r'え.*し'))); // [う, た]
print(s.split(RegExp(r'え.*?し'))); // [う, たした] (最左最短マッチ)
print(s.replaceFirst(RegExp(r'え.{1}た'), 'で')); // うえうでした

1文字ずつ処理する

上述したようなsplit('')を使って文字ごとに分割する方法が分かりやすい。List<String>が得られるので、イテレータとして処理できる。

その他に、runesで文字のコード(rune、Unicodeコードポイント)のリストを取得する方法もある。これならintなのでそのまま文字演算できる。ただアルファベット以外で使うときは文字のコードの並びに要注意。

print('あいうえお'.split('')); // [あ, い, う, え, お]

final s = 'IBM-ibmあいびーえむ';
// マイナス1して、1文字前を得る。
print(s.runes.map((v) => String.fromCharCode(v - 1)).join()); // HAL,halぁぃひ・ぇみ

型の変換

文字列操作ではないけど、よく使う似たようなものということで。
各型のparseスタティックメソッドを使用して変換する。

intへの変換

// parse(String source, {int? radix, int onError(String source)}) → int
print(int.parse('42')); // 42
print(int.parse('-7')); // -7
print(int.parse('  77  \n')); // 77 (前後のホワイトスペースは無視される)
// 引数radixに基数(何進数か)を指定できる。省略されている場合は10進数。
print(int.parse('A', radix: 16)); // 10 (16進数)
print(int.parse('fF', radix: 16)); // 255 (16進数)
print(int.parse('1100', radix: 2)); // 12
print(int.parse('1100', radix: 8)); // 576
print(int.parse('1100', radix: 10)); // 1100
print(int.parse('1100', radix: 16)); // 4352
// 変換できない場合は例外が発生する。
// int.parse('10.0'); // (例外発生)
// int.parse('9a'); // (例外発生)
// 引数の onError は非推奨。

// tryParse(String source, {int? radix}) → int?
// パースに失敗する可能性がある場合は、parseで例外をキャッチするより、tryParseの使用がおすすめ。
final value = int.tryParse('A'); // 変換に失敗して null が返される。
if (value == null) {
  // ... 変換エラー時の処理
}
// あるいはこんな書き方でnullの場合の値を設定できる。
final value2 = int.tryParse('A') ?? 0;

doubleへの変換

// parse(String source, [double onError(String source)]) → double
// doubleは色々な表現の文字列で初期化できる。
print(double.parse('3.14')); // 3.14
print(double.parse('  3.14 \xA0')); // 3.14 (前後のホワイトスペースは無視される)
print(double.parse('0.')); // 0.0
print(double.parse('.1')); // 0.1
print(double.parse('-1.e3')); // -1000.0
print(double.parse('1234E+7')); // 12340000000.0
print(double.parse('+.12e-9')); // 1.2e-10
print(double.parse('-NaN')); // NaN
print(double.parse('Infinity')); // Infinity
// 変換できない場合は例外が発生する。
// double.parse('A'); // (例外発生)
// 引数の onError は非推奨。

// tryParse(String source) → double?
// パースに失敗する可能性がある場合は、parseで例外をキャッチするより、tryParseの使用がおすすめ。
final value = double.tryParse('A'); // 変換に失敗して null が返される。
if (value == null) {
  // ... 変換エラー時の処理
}

他の型からStringへの変換

文字列への変換は、それぞれのクラスで実装されているtoString()メソッドを呼び出すか、文字列へ${}で埋め込む。これはどちらも同じ処理。

あとがき

色々な言語を触っていると、それぞれ書き方が違って混乱してしまうため、良い情報が見付からなかったDartを整理してみた。

しかしチートシートと題するならもっとコンパクトにまとまっていた方が良い気もするけど、具体例を書いているから仕方ない、ということでひとつ。

Discussion

Misako TateiwaMisako Tateiwa

突然のコメント失礼致します。

文字列の分割で

final s2 = 'あいいう';

// 引数で渡された文字列(パターン)で分割する。
print(s2.split('い')); // ['あ', '', 'う']

と記載されていたのですが、正確には

final s2 = 'あいいう';

// 引数で渡された文字列(パターン)で分割する。
print(s2.split('い')); // ['あ', '', '', 'う']

なのかなと思いました。
https://api.flutter.dev/flutter/dart-core/String/split.html

もし、バージョン違いでこのようになっているのであれば無視していただいて構いません

tristris

どもども。
DartPadで実行してみましたが、記事の記載の通りの動作となりました。

ドキュメントを読んでも、特に違和感のある動作ではありません。

出力されているのは『い』での分割結果で、以下のものだからですね。

  • 「『い(1)』の前」
  • 「『い(1)』と『い(2)』の間」
  • 「『い(2)』の後」

CSVファイルをカンマで分割するときのことを考えると分かりやすいかもしれません。