広告 Flutter Dart Unity アプリ開発

Dartの文字列操作と正規表現(ほぼコピペ)

下記URLからのコピペです。
https://zenn.dev/tris/articles/bf623e5e65fac3
https://flutter.salon/dart/string/
https://zenn.dev/joo_hashi/articles/60927dae9e980a
https://flutter.salon/dart/regexp/

文字列の基本

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パッケージを使う。

文字列の長さ等を得る

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文字」としてカウントされている。

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

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

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)); // (エラー)

文字列の分割

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')); // ''
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出力時に   で揃えるような場合に便利。
print(s1.padLeft(7, ' ')); // '  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, ' ')); // 'short  '

大文字/小文字の変換

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

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

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

型の変換

文字列操作ではないけど、よく使う似たようなものということで。各型の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で使える正規表現に関する説明。以下のURLのコピペです。
https://flutter.salon/dart/regexp/

正規表現の作成

final reg = RegExp(r'A');

正規表現の該当箇所があるか確認

final regA = RegExp(r'A');
expect(regA.hasMatch('A'), true);
expect(regA.hasMatch('aA'), true);
expect(regA.hasMatch('Aa'), true);
expect(regA.hasMatch('a'), false);;

正規表現で該当箇所を取得

firstMatchは最初の該当箇所ひとつだけを取得する。
一方で、allMatchesで該当箇所を全てIteratableとして取得する。そのため、for文かtoList()にして、処理をする。
実際の該当の文字列はgroup(0)で取得する。

final matchOne = regA.firstMatch('A');
expect(matchOne!.groupCount, 0);
expect(matchOne.group(0), 'A');
final matchNone = regA.firstMatch('a');
expect(matchNone, null);
 
final matchOne2 = regA.firstMatch('AaA');
expect(matchOne2!.groupCount, 0);
expect(matchOne2.group(0), 'A');
 
final matchSome = regA.allMatches('AaA');
expect(matchSome.length, 2);
expect(matchSome.toList()[0].group(0), 'A');
expect(matchSome.toList()[1].group(0), 'A');

正規表現で一致箇所を置換

置換はString型の、replaceFirst/replaceAllを使用する。

final regA = RegExp(r'A');
expect('A'.replaceFirst(regA, 'B'), 'B');
expect('AaA'.replaceFirst(regA, 'B'), 'BaA');
expect('AaA'.replaceAll(regA, 'B'), 'BaB');

大文字・小文字の区別をする・しないの選択

大文字小文字を区別しないための正規表現(\i)があるが、Dartでは使えない。単にRegExpのcaseSensitiveを使う。true:区別する false:区別しない

final regDefault = RegExp(r'A');
final regSensitive = RegExp(r'A', caseSensitive: true);
final regNotSensitive = RegExp(r'A', caseSensitive: false);
 
expect(regDefault.isCaseSensitive, true);
expect(regSensitive.isCaseSensitive, true);
expect(regNotSensitive.isCaseSensitive, false);
 
expect(regDefault.hasMatch('a'), false);
expect(regSensitive.hasMatch('a'), false);
expect(regNotSensitive.hasMatch('a'), true);

複数回の一致している場合の検索

final reg = RegExp(r'AB.');
final matches = reg.allMatches('AB1aAB2aAB3');
expect(matches.length, 3);
 
final matchWords = matches.map((e) => e.group(0)).toList();
expect(matchWords.length, 3);
expect(matchWords[0], 'AB1');
expect(matchWords[1], 'AB2');
expect(matchWords[2], 'AB3');

stringMatchでマッチ部分を文字列で取得

final regExp = RegExp(r'bird');
expect(regExp.stringMatch('bird'), 'bird');
expect(regExp.stringMatch('a bird'), 'bird');
expect(regExp.stringMatch('bir'), isNull);

RegExpMatchのプロパティ

const string = 'Dash is a bird.';
final regExp = RegExp(r'bird');
final match = regExp.firstMatch(string)!;
expect(match.start, 10);
expect(match.end, 14);
expect(match.pattern, regExp);

正規表現で一致する文字列前後に文字を追加(固定)

final regA = RegExp(r'A');
expect('ABC'.replaceFirst(regA, '@A'), '@ABC');
expect('ABC'.replaceFirst(regA, 'A@'), 'A@BC');

正規表現で一致する文字列前後に文字を追加(動的)

const original = 'Dash is a bird.';
const result = 'Dash is a "bird".';
final regExp = RegExp(r'bird');
final match = regExp.firstMatch(original)!;
final value = original.substring(0, match.start) +
    '"' +
    match.group(0)! +
    '"' +
    original.substring(match.end);
expect(value, result);

正規表現の記法

「これぞ正規表現!」的な書き方を書いていきます。

任意の文字列 「.」

任意の一文字は「.」で示します。ただし、改行にはヒットしない。

final reg = RegExp(r'c.t');
expect(reg.hasMatch('cat'), true);
expect(reg.hasMatch('cut'), true);
expect(reg.hasMatch('cutboard'), true);
 
expect(reg.hasMatch('ct'), false);
expect(reg.hasMatch('cuT'), false);
 
// 改行にはヒットしない
expect(reg.hasMatch('c{t'), true);
expect(reg.hasMatch('c\nt'), false);
expect(reg.hasMatch('c\tt'), true);

文字列の開始・終了への一致 「開始^ 終了$」

文字列の最初からの場合、正規表現の最初に「^」、文字列の最後の場合、正規表現の最後に「$」をつけます。
否定の表現と間違えやすいです。( [^文字列]⇒否定は括弧が付く )
「この正規表現でこの文字列にはヒットしないはずだが、、」と思ったら、まずこちらを確認すると良いかも知れません。想定外のところでヒットしてたりします。

final regStartWithA = RegExp(r'^A');
final regEndWithA = RegExp(r'A$');
final regOnlyA = RegExp(r'^A$');
 
expect(regStartWithA.hasMatch('A'), true);
expect(regEndWithA.hasMatch('A'), true);
expect(regOnlyA.hasMatch('A'), true);
 
expect(regStartWithA.hasMatch('Aab'), true);
expect(regEndWithA.hasMatch('Aab'), false);
expect(regOnlyA.hasMatch('Aab'), false);
 
expect(regStartWithA.hasMatch('abA'), false);
expect(regEndWithA.hasMatch('abA'), true);
expect(regOnlyA.hasMatch('abA'), false);

複数行への検索

$で文字列の最後にヒットさせる方法を紹介しました。文字列の最後は基本「文字列の最後」です。しかしDartの場合、multiLineのフラグをtrueにすることによって「各行の最後」の扱いにすることが可能です。以下の例では、引数のmultiLineが

  • 未設定時とfalseの場合、文字列の最後だけのため1カ所
  • trueの場合、改行毎のため3カ所

ヒットすることが分かります。また改行コードが「\n」「\r\n」のどちらでも同じように動作することを確認してます。

multiLineをtrueにすれば、CSVファイルなどでファイル内容全てを一つの文字列として取得した場合でも、一つの正規表現で処理することができます。

const lineN = '1END\n2END\n3END';
const lineRn = '1END\r\n2END\r\n3END';
 
final regDefault = RegExp(r'END$');
final regSingleLine = RegExp(r'END$', multiLine: false);
final regMultiLine = RegExp(r'END$', multiLine: true);
 
expect(regDefault.allMatches(lineN).length, 1);
expect(regDefault.allMatches(lineRn).length, 1);
 
expect(regSingleLine.allMatches(lineN).length, 1);
expect(regSingleLine.allMatches(lineRn).length, 1);
expect(regMultiLine.allMatches(lineN).length, 3);
expect(regMultiLine.allMatches(lineRn).length, 3);

連続した文字列

正規表現が、指定の文字列内にあれば、マッチします。
最初や最後のみにマッチさせたい場合は、「文字列の開始・終了への一致」を使います。

final reg = RegExp(r'ABC');
expect(reg.hasMatch('ABC'), true);
expect(reg.hasMatch('aABC'), true);
expect(reg.hasMatch('ABCc'), true);
 
expect(reg.hasMatch('AbC'), false);

指定文字列のどれかの文字 「[]」

[]内の指定の文字のどれかにマッチします。

final reg = RegExp(r'[ABC]');
expect(reg.hasMatch('A'), true);
expect(reg.hasMatch('B'), true);
expect(reg.hasMatch('C'), true);
 
expect(reg.hasMatch('Abc'), true);
expect(reg.hasMatch('aBc'), true);
expect(reg.hasMatch('abC'), true);
 
expect(reg.hasMatch('abc'), false);

12のメタ文字には、\をつける

メタ文字は、以下の12個: $()*+.?[\^{|
]}はメタ文字ではない。

final regDot = RegExp(r'c\.t');
expect(regDot.hasMatch('cat'), false);
expect(regDot.hasMatch('c.t'), true);
 
// RegExp(r'[');
// ⇒] がないので、実行エラー(FormatException: Unterminated character class[)
final regOpen = RegExp(r'\[');
final regClose = RegExp(r']');
 
expect(regOpen.hasMatch('[]'), true);
expect(regClose.hasMatch('[]'), true);

文字の連続 「0以上:* 1以上:+ 0か1:? 数指定:{}」

連続する数を指定するときは、

  • 0以上の連続: *
  • 1以上の連続: +
  • ないか一つだけ: ?
  • 数を指定{最小,最大}
  • 最小を指定:{最小,}
  • 最大と最小を指定{最小,最大}
  • 最大を指定:指定不可
final regZeroOrMore = RegExp(r'A*');
final regOneOrMore = RegExp(r'A+');
 
expect(regZeroOrMore.hasMatch('a'), true);
expect(regOneOrMore.hasMatch('a'), false);
 
expect(regZeroOrMore.hasMatch('Aa'), true);
expect(regOneOrMore.hasMatch('Aa'), true);
 
// ?: 0か1の連続
final regZeroOrOne = RegExp(r'cou?lor');
expect(regZeroOrOne.hasMatch('color'), true);
expect(regZeroOrOne.hasMatch('coulor'), true);
expect(regZeroOrOne.hasMatch('c_lor'), false);
expect(regZeroOrOne.hasMatch('c_ulor'), false);
 
final regTwo = RegExp(r'^A{2}$');
final regTwoOrMore = RegExp(r'^A{2,}$');
final regBetweenTwoAndFour = RegExp(r'^A{2,4}$');
 
expect(regTwo.hasMatch('A'), false);
expect(regTwoOrMore.hasMatch('A'), false);
expect(regBetweenTwoAndFour.hasMatch('A'), false);
 
expect(regTwo.hasMatch('AA'), true);
expect(regTwoOrMore.hasMatch('AA'), true);
expect(regBetweenTwoAndFour.hasMatch('AA'), true);
 
expect(regTwo.hasMatch('AAA'), false);
expect(regTwoOrMore.hasMatch('AAA'), true);
expect(regBetweenTwoAndFour.hasMatch('AAA'), true);
 
expect(regTwo.hasMatch('AAAA'), false);
expect(regTwoOrMore.hasMatch('AAAA'), true);
expect(regBetweenTwoAndFour.hasMatch('AAAA'), true);
 
expect(regTwo.hasMatch('AAAAA'), false);
expect(regTwoOrMore.hasMatch('AAAAA'), true);
expect(regBetweenTwoAndFour.hasMatch('AAAAA'), false);
 
// 最大回数の指定はできないと思われる
final regBetweenFourOrLess = RegExp(r'^A{,4}$');
expect(regBetweenFourOrLess.hasMatch('A'), false);

単語の区切りは\bを使う

final reg = RegExp(r'\bcat\b');
expect(reg.hasMatch('cat'), true);
expect(reg.hasMatch('cats'), false);
 
expect(reg.hasMatch('animal cat dog'), true);
expect(reg.hasMatch('animal cats dog'), false);

数字・数字以外 \d \D

final regNumber = RegExp(r'\d');
final regNotNumber = RegExp(r'\D');
 
expect(regNumber.hasMatch('0'), true);
expect(regNotNumber.hasMatch('0'), false);
 
expect(regNumber.hasMatch('A'), false);
expect(regNotNumber.hasMatch('A'), true);

アルファベット&アンダーバー&数字・それ以外 \w \W

final regWord = RegExp(r'\w');
final regNotWord = RegExp(r'\W');
 
expect(regWord.hasMatch('0'), true);
expect(regNotWord.hasMatch('0'), false);
 
expect(regWord.hasMatch('A'), true);
expect(regNotWord.hasMatch('A'), false);
 
expect(regWord.hasMatch('_'), true);
expect(regNotWord.hasMatch('_'), false);
 
expect(regWord.hasMatch('.'), false);
expect(regNotWord.hasMatch('.'), true);
 
expect(regWord.hasMatch('!@#%^&*()+='), false);
expect(regNotWord.hasMatch('!@#%^&*()+='), true);

空白・空白以外 \s \S

final regSpace = RegExp(r'\s');
final regNotSpace = RegExp(r'\S');
 
// 空白
expect(regSpace.hasMatch(' '), true);
expect(regNotSpace.hasMatch(' '), false);
 
// 全角空白
expect(regSpace.hasMatch(' '), true);
expect(regNotSpace.hasMatch(' '), false);
 
// フォームフィード文字(改ページ)
expect(regSpace.hasMatch('\f'), true);
expect(regNotSpace.hasMatch('\f'), false);
 
//改行文字
expect(regSpace.hasMatch('\n'), true);
expect(regNotSpace.hasMatch('\n'), false);
 
// 復帰文字
expect(regSpace.hasMatch('\r'), true);
expect(regNotSpace.hasMatch('\r'), false);
 
// タブ文字
expect(regSpace.hasMatch('\t'), true);
expect(regNotSpace.hasMatch('\t'), false);
 
// 垂直タブ文字
expect(regSpace.hasMatch('\v'), true);
expect(regNotSpace.hasMatch('\v'), false);
 
// 文字 →マッチせず
expect(regSpace.hasMatch('A'), false);
expect(regNotSpace.hasMatch('A'), true);
 
// アラート文字(ベル)、バックスペース文字、行末の改行を抑止する →マッチせず
expect(regSpace.hasMatch('\a\b\c'), false);
expect(regNotSpace.hasMatch('\a\b\c'), true);

否定 [^~]

final regNotOne = RegExp(r'[^A]');
final regNotSome = RegExp(r'[^ABC]');
 
expect(regNotOne.hasMatch('A'), false);
expect(regNotSome.hasMatch('A'), false);
 
expect(regNotOne.hasMatch('ABC'), true);
expect(regNotSome.hasMatch('ABC'), false);
 
expect(regNotOne.hasMatch('BCA'), true);
expect(regNotSome.hasMatch('BCA'), false);
 
expect(regNotOne.hasMatch('ABCD'), true);
expect(regNotSome.hasMatch('ABCD'), true);

連続した文字

final regZeroToFive = RegExp(r'[0-5]');
expect(regZeroToFive.hasMatch('0'), true);
expect(regZeroToFive.hasMatch('1'), true);
expect(regZeroToFive.hasMatch('3'), true);
expect(regZeroToFive.hasMatch('5'), true);
 
expect(regZeroToFive.hasMatch('6'), false);
expect(regZeroToFive.hasMatch('9'), false);
 
final regAtoE = RegExp(r'[A-E]');
expect(regAtoE.hasMatch('A'), true);
expect(regAtoE.hasMatch('C'), true);
expect(regAtoE.hasMatch('E'), true);
 
expect(regAtoE.hasMatch('F'), false);
expect(regAtoE.hasMatch('a'), false);

範囲を表さないマイナスは隅に寄せる

final reg1_9 = RegExp(r'[1-9]');
final reg19_ = RegExp(r'[19-]');
final reg_19 = RegExp(r'[-19]');
expect(reg1_9.hasMatch('8'), true);
expect(reg_19.hasMatch('8'), false);
expect(reg19_.hasMatch('8'), false);
expect(reg1_9.hasMatch('-'), false);
expect(reg_19.hasMatch('-'), true);
expect(reg19_.hasMatch('-'), true);

]を[]で使用するときは、\をつける

final reg = RegExp(r'[AB\]]');
final regNot = RegExp(r'[^\]AB]');

expect(reg.hasMatch('A'), true);
expect(reg.hasMatch(']'), true);
expect(reg.hasMatch('C'), false);

expect(regNot.hasMatch('A'), false);
expect(regNot.hasMatch(']'), false);
expect(regNot.hasMatch('C'), true);

文字列内の一致箇所をいくつかに分けてグループとして取得

final regGroup = RegExp(r'^http://([^/]+)/(.+)$');
final matches =
    regGroup.allMatches('http://domain.com/path1/path2').toList();
expect(matches.length, 1);
 
var match = matches[0];
expect(match.groupCount, 2);
expect(match.group(0), 'http://domain.com/path1/path2');
expect(match.group(1), 'domain.com');
expect(match.group(2), 'path1/path2');
 
// リストでまとめてグループを取得
final groups = match.groups([1, 2]);
expect(groups.length, 2);
expect(groups[0], 'domain.com');
expect(groups[1], 'path1/path2');

グループ名で取得

final regGroup = RegExp(r'^http://(?<domain>[^/]+)/(?<path>.+)$');
final matches =
    regGroup.allMatches('http://domain.com/path1/path2').toList();
expect(matches.length, 1);
 
var match = matches[0];
expect(match.groupCount, 2);
 
final groupNames = match.groupNames.toList();
expect(groupNames.length, 2);
expect(groupNames[0], 'domain');
expect(groupNames[1], 'path');
 
expect(match.group(0), 'http://domain.com/path1/path2');
expect(match.group(1), 'domain.com');
expect(match.namedGroup('domain'), 'domain.com');
expect(match.group(2), 'path1/path2');
expect(match.namedGroup('path'), 'path1/path2');
 
// グループ名を間違えると、ArgumentError が発行される
expect(() => match.namedGroup('wrongName'),
    throwsA(isInstanceOf<ArgumentError>()));

パターンのグループ化

final reg = RegExp(r'c(?:at|ut)');
expect(reg.hasMatch('cat'), true);
expect(reg.hasMatch('cut'), true);
expect(reg.hasMatch('cute'), true);
 
expect(reg.hasMatch('c'), false);
expect(reg.hasMatch('cote'), false);
 
final regName = RegExp(r'c(?<name>at|ut)');
expect(regName.firstMatch('cat')!.namedGroup('name'), 'at');
expect(regName.firstMatch('cut')!.namedGroup('name'), 'ut');

グループでリピート

^と$で全体で判定させる(一部でも合えばよし、ではなく)。cから始まり、その後がatutのどちらかの連続にのみヒットする。cが間に入ると、ヒットしない。

final regRepease = RegExp(r'^c(at|ut)+$');
expect(regRepease.hasMatch('cat'), true);
expect(regRepease.hasMatch('catut'), true);
 
expect(regRepease.hasMatch('catcut'), false);

先読みと後読みを使ったパターン

正規表現でマッチする文字列が欲しいが、マッチさせたい文字列の前後に特定の文字列がある場合のみ取得したい、もしくは、特定の文字が合った場合は除外したい場合に使用する。

final reg = RegExp(r'(?<=<b>)\w*(?=</b>)');
 
expect(reg.firstMatch('<b>cat</b>')!.group(0), 'cat');
 
final matches = reg.allMatches('<b>cat</b>1234<b>dog</b>5678<b>fox</b>90');
expect(matches.length, 3);
final result = <String>[];
for (final match in matches) {
  result.add(match.group(0)!);
}
 
expect(result[0], 'cat');
expect(result[1], 'dog');
expect(result[2], 'fox');

肯定先読み: マッチ箇所(?=後の一致条件)

否定先読み: マッチ箇所(?!後の不一致条件)

肯定後読み:(?<=前の一致条件)マッチ箇所

否定後読み:(?<!前の一致条件)マッチ箇所

final dragonQuest = 'DragonQuest';
final dragonBall = 'DragonBall';
final fotuneQuest = 'FortuneQuest';
 
//肯定先読み
final regBefore = RegExp(r'Dragon(?=Quest)');
expect(regBefore.hasMatch(dragonQuest), true);
expect(regBefore.firstMatch(dragonQuest)!.group(0), 'Dragon');
expect(regBefore.hasMatch(dragonBall), false);
 
// 否定先読み
final regNotBefore = RegExp(r'Dragon(?!Quest)');
expect(regNotBefore.hasMatch(dragonQuest), false);
expect(regNotBefore.hasMatch(dragonBall), true);
expect(regNotBefore.firstMatch(dragonBall)!.group(0), 'Dragon');
 
//肯定後読み
final regAfter = RegExp(r'(?<=Dragon)Quest');
expect(regAfter.hasMatch(dragonQuest), true);
expect(regAfter.firstMatch(dragonQuest)!.group(0), 'Quest');
expect(regAfter.hasMatch(fotuneQuest), false);
 
//否定後読み
final regNotAfter = RegExp(r'(?<!Dragon)Quest');
expect(regNotAfter.hasMatch(dragonQuest), false);
expect(regNotAfter.hasMatch(fotuneQuest), true);
expect(regNotAfter.firstMatch(fotuneQuest)!.group(0), 'Quest');

最大量指定子(デフォルト) と 最小量指定子(?)

最大量指定子は欲張りな(Greedy)なマッチで、なるべく多くの文字をマッチさせようとする。
最小量指定子は控えめな(Reluctant または Non-greedy)なマッチで、少ない文字をマッチさせようとする。

// 0回以上の繰り返し
final regMoreGreedy = RegExp(r'A*');
final regMore = RegExp(r'A*?');
expect(regMoreGreedy.firstMatch('AA')!.group(0), 'AA');
expect(regMore.firstMatch('AA')!.group(0), '');
 
// 1回以上の繰り返し
final regOneAndMoreGreedy = RegExp(r'A+');
final regOneAndMore = RegExp(r'A+?');
expect(regOneAndMoreGreedy.firstMatch('AA')!.group(0), 'AA');
expect(regOneAndMore.firstMatch('AA')!.group(0), 'A');
 
// 0回か1回
final regZeroOrOneGreedy = RegExp(r'A?');
final regZeroOrOne = RegExp(r'A??');
expect(regZeroOrOneGreedy.firstMatch('AA')!.group(0), 'A');
expect(regZeroOrOne.firstMatch('AA')!.group(0), '');
 
// 最小、最大の指定
final regMinMaxGreedy = RegExp(r'A{2,4}');
final regMinMax = RegExp(r'A{2,4}?');
expect(regMinMaxGreedy.firstMatch('AAAAA')!.group(0), 'AAAA');
expect(regMinMax.firstMatch('AAAAA')!.group(0), 'AA');

後方参照 \数字

後方参照とは、正規表現で対象の文字列内でマッチさせた箇所の特定の文字列を、対象の文字列内の後の方で特定の文字列を使用しているか、を確認するときに使用する方法です。標準の正規表現では9カ所までマッチできれば良いですが、Dartではそれ以上でもマッチします(12までは確認。reg12 参照)。

括弧を二重にした場合、前の括弧の位置が早いほうが、グループとして前の番号は割り振られます(regDouble参照)

final reg1 = RegExp(r'(aaa).*\1');
expect(reg1.hasMatch('aaa123aaa'), true);
expect(reg1.hasMatch('aaa123bbb'), false);
 
final reg12 = RegExp(
    r'(1)(2)(3)(4)(5)(6)(7)(8)(9)(0)(11)(12)\1\2\3\4\5\6\7\8\9\10\11\12');
expect(reg12.hasMatch('1234567890111212345678901112'), true);
expect(reg12.hasMatch('1234567890111212345678901110'), false);
 
final regDouble = RegExp(r'(ab(c|d))_\1\2');
expect(regDouble.hasMatch('abc_abcc'), true);
expect(regDouble.hasMatch('abd_abdd'), true);
expect(regDouble.hasMatch('abc_abcd'), false);
 
final regHtml = RegExp(r'<(p|pre)>.*<(p|pre)>.*</\2>.*</\1>');
expect(regHtml.hasMatch('<pre>aaa<p>sdaa</p>ksdfs</pre>'), true);
expect(regHtml.hasMatch('<p>aaa<pre>sdaa</pre>ksdfs</p>'), true);
expect(regHtml.hasMatch('<p>aaa<p>sdaa</p>ksdfs</pre>'), false);

Dartで正規表現のコンテキストはサポートされていない

正規表現の置換に「マッチコンテキスト」というものがある。現在のDart(2.18.5)ではサポートしていない、と結論づけた。コンテキストを指定しても、そのまま表示される(ご存じの方、いらっしゃったらご教授ください。「$記号」は、Perl, .Netの置換テキスト方言を書いてます)

  • $_:対象テキスト全体
  • $&:マッチ
  • $`:前コンテキスト(正規表現がマッチする部分の前)
  • $':後コンテキスト(正規表現がマッチする部分の後)
final reg = RegExp(r'B');
expect('ABC'.replaceFirst(reg, r'$_'), r'A$_C');
expect('ABC'.replaceFirst(reg, r'\&'), r'A\&C');
expect('ABC'.replaceFirst(reg, r'$&'), r'A$&C');

正規表現の実用的な使い方

郵便番号の確認

final regPostalCode = RegExp(r'^\d{3}-?\d{4}$');
 
expect(regPostalCode.hasMatch('1234567'), true);
expect(regPostalCode.hasMatch('123-4567'), true);
 
expect(regPostalCode.hasMatch('123--4567'), false);
expect(regPostalCode.hasMatch('123456'), false);
expect(regPostalCode.hasMatch('12345678'), false);
expect(regPostalCode.hasMatch('1234-567'), false);
expect(regPostalCode.hasMatch('123-456'), false);
expect(regPostalCode.hasMatch('123-45678'), false);

郵便番号を変換

final regPostalCode = RegExp(r'^(\d{3})-?(\d{4})$');
 
final match = regPostalCode.firstMatch('1234567')!;
expect('${match.group(1)}${match.group(2)}', '1234567');
expect('${match.group(1)}-${match.group(2)}', '123-4567');
 
final matchMinus = regPostalCode.firstMatch('123-4567')!;
expect('${matchMinus.group(1)}${matchMinus.group(2)}', '1234567');
expect('${matchMinus.group(1)}-${matchMinus.group(2)}', '123-4567');

メールアドレスの確認

final regEmail = RegExp(
  caseSensitive: false,
  r"^[\w!#$%&'*+/=?`{|}~^-]+(\.[\w!#$%&'*+/=?`{|}~^-]+)*@([A-Z0-9-]{2,6})\.(?:\w{3}|\w{2}\.\w{2})$",
);
 
expect(regEmail.hasMatch('aaaa@aaaa.com'), true);
expect(regEmail.hasMatch('!.{}@aaaa.com'), true);
expect(regEmail.hasMatch('aaaa@aaaa.co.jp'), true);
 
expect(regEmail.hasMatch('aaaa.com'), false);
expect(regEmail.hasMatch('aa@aaaaaaa.com'), false);
expect(regEmail.hasMatch('.aaaa@aaaa.com'), false);

日付の確認

final regText = r'^(?<year>20[0-3][0-9])/(?:' +
    r'(?<month1>0?2)/(?<day1>[12][0-9]|0?[1-9])|' +
    r'(?<month2>0?[469]|11)/(?<day2>30|[12][0-9]|0?[1-9])|' + // 30日の月
    r'(?<month3>0?[13578]|1[02])/(?<day3>3[01]|[12][0-9]|0?[1-9])' + //31日の月
    r')$';
final regDay = RegExp(regText);
 
expect(regDay.hasMatch('1999/01/01'), false);
expect(regDay.hasMatch('2000/01/01'), true);
expect(regDay.hasMatch('2039/01/01'), true);
expect(regDay.hasMatch('2040/01/01'), false);
 
expect(regDay.hasMatch('2000/1/1'), true);
expect(regDay.hasMatch('2000/1/01'), true);
expect(regDay.hasMatch('2000/01/1'), true);
 
expect(regDay.hasMatch('20000/01/1'), false);
expect(regDay.hasMatch('02000/01/1'), false);
expect(regDay.hasMatch('2000/001/1'), false);
expect(regDay.hasMatch('2000/1/001'), false);
 
expect(regDay.hasMatch('2022/02/01'), true);
expect(regDay.hasMatch('2022/02/02'), true);
expect(regDay.hasMatch('2022/02/09'), true);
expect(regDay.hasMatch('2022/02/10'), true);
expect(regDay.hasMatch('2022/02/29'), true);
expect(regDay.hasMatch('2022/02/30'), false);
 
expect(regDay.hasMatch('2022/04/01'), true);
expect(regDay.hasMatch('2022/04/09'), true);
expect(regDay.hasMatch('2022/04/10'), true);
expect(regDay.hasMatch('2022/04/15'), true);
expect(regDay.hasMatch('2022/04/19'), true);
expect(regDay.hasMatch('2022/04/20'), true);
expect(regDay.hasMatch('2022/04/21'), true);
expect(regDay.hasMatch('2022/04/29'), true);
expect(regDay.hasMatch('2022/04/30'), true);
expect(regDay.hasMatch('2022/04/31'), false);
expect(regDay.hasMatch('2022/04/32'), false);
 
expect(regDay.hasMatch('2022/04/31'), false);
expect(regDay.hasMatch('2022/06/31'), false);
expect(regDay.hasMatch('2022/09/31'), false);
 
expect(regDay.hasMatch('2022/01/01'), true);
expect(regDay.hasMatch('2022/01/09'), true);
expect(regDay.hasMatch('2022/01/10'), true);
expect(regDay.hasMatch('2022/01/15'), true);
expect(regDay.hasMatch('2022/01/19'), true);
expect(regDay.hasMatch('2022/01/20'), true);
expect(regDay.hasMatch('2022/01/21'), true);
expect(regDay.hasMatch('2022/01/29'), true);
expect(regDay.hasMatch('2022/01/30'), true);
expect(regDay.hasMatch('2022/01/31'), true);
expect(regDay.hasMatch('2022/01/32'), false);
 
final match = regDay.firstMatch('2022/01/01');
expect(match!.groupNames.contains('month1'), true);
expect(match.groupNames.contains('month2'), true);
expect(match.groupNames.contains('month3'), true);
 
expect(match.namedGroup('month1'), null);
expect(match.namedGroup('month2'), null);
expect(match.namedGroup('month3'), '01');

日付に変換

DateTime? getDate(String targetDay) {
// final static で定義する?
final regText = r'^(?<year>20[0-3][0-9])/(?:' +
    r'(?<month0>0?2)/(?<day0>[12][0-9]|0?[1-9])|' +
    r'(?<month1>0?[469]|11)/(?<day1>30|[12][0-9]|0?[1-9])|' + // 30日の月
    r'(?<month2>0?[13578]|1[02])/(?<day2>3[01]|[12][0-9]|0?[1-9])' + //31日の月
    r')$';
final regDay = RegExp(regText);
if (!regDay.hasMatch(targetDay)) {
  return null;
}
 
final match = regDay.firstMatch(targetDay)!;
for (int i = 0; i < 3; i++) {
  if (match.namedGroup('month$i') != null) {
    return DateTime(
      int.parse(match.namedGroup('year')!),
      int.parse(match.namedGroup('month$i')!),
      int.parse(match.namedGroup('day$i')!),
    );
  }
}
}
 
test('getDate', () {
	expect(getDate('2022/01/01'), DateTime(2022, 1, 1));
	expect(getDate('2022/01/31'), DateTime(2022, 1, 31));
	expect(getDate('2022/02/29'), DateTime(2022, 2, 29));
 
	expect(getDate('20220229'), null);
	expect(getDate('2022/04/31'), null);
	expect(getDate('AAAA/04/31'), null);
});

HTMLのパースなど複数行にまたがってマッチさせる

改行を含んだ複数行にまたがる対象を検索

\sが改行も含んだ空白で、\Sはそれ以外なので、\s\Sで改行と全ての文字が含まれるみたいです。詳しくは「正規表現の例文-複数行にまたがる処理」まで。

const data = '''
<div>
  データ1
</div>  
  <div>
データ2
</div>
''';

final reg = RegExp(r'<div>[\s\S]*?</div>');
final matchers = reg.allMatches(data).toList();
expect(matchers.length, 2);
 
expect(matchers[0].group(0), '''
<div>
  データ1
</div>''')
 
    expect(matchers[1].group(0), '''
<div>
  データ2
</div>''');

日本語

final regHiragana = RegExp('[\u3040-\u309F]');
expect(regHiragana.hasMatch('あ'), true);
expect(regHiragana.hasMatch('を'), true);
expect(regHiragana.hasMatch('ぁ'), true);
expect(regHiragana.hasMatch('だ'), true);
 
expect(regHiragana.hasMatch('A'), false);
expect(regHiragana.hasMatch('ア'), false);
 
final regKatakana = RegExp('[\u30A0-\u30FF]');
expect(regKatakana.hasMatch('ア'), true);
expect(regKatakana.hasMatch('ヲ'), true);
expect(regKatakana.hasMatch('ァ'), true);
expect(regKatakana.hasMatch('ダ'), true);
expect(regKatakana.hasMatch('A'), false);
expect(regKatakana.hasMatch('あ'), false);
 
final regKanji = RegExp('[\u4E00-\u9FFF]');
expect(regKanji.hasMatch('亜'), true);
expect(regKanji.hasMatch('正'), true);
 
expect(regKanji.hasMatch('A'), false);
expect(regKanji.hasMatch('あ'), false);
expect(regKanji.hasMatch('ア'), false);

中括弧{}の中身を取り出す

final reg = RegExp(r'id=\{[^}]*}');
expect(reg.hasMatch('id={123}'), true);
expect(reg.firstMatch('id={123}')?.group(0), 'id={123}');
final regGroup = RegExp(r'id=\{([^}]*)}');
expect(regGroup.hasMatch('id={123}'), true);
expect(regGroup.firstMatch('id={123}')?.group(0), 'id={123}');
expect(regGroup.firstMatch('id={123}')?.group(1), '123');

Excelのシート名・セル名を抽出

ポイント

  • (?<name>):nameで名前をつけてグループ化
  • *?: 最小量指定子
  • '?:'の0か1個

シート名の前後に「'」があってもなくても、最小指定子と?を使うことで、シート名のみ抽出できるようにしている。

final reg = RegExp(r"'?(?<sheet>.*?)'?!(?<cell>.*)");
 
final matchWithQuote = reg.firstMatch("'シート名'!A12")!;
expect(matchWithQuote.namedGroup('sheet'), 'シート名');
expect(matchWithQuote.namedGroup('cell'), 'A12');
 
final matchWithoutQuote = reg.firstMatch('sheet!A12')!;
expect(matchWithoutQuote.namedGroup('sheet'), 'sheet');
expect(matchWithoutQuote.namedGroup('cell'), 'A12');

CSVファイルの一行をグループで読み込む

const line = 'ナイトパレード,ハリウッドエリア,19:00';
 
final regNo = RegExp(r'^([^,]*),([^,]*),(\d+):(\d{2})$');
final matchNo = regNo.firstMatch(line);
expect(matchNo, isNotNull);
expect(matchNo!.group(1), 'ナイトパレード');
expect(matchNo.group(2), 'ハリウッドエリア');
expect(matchNo.group(3), '19');
expect(matchNo.group(4), '00');
 
final regNamed = RegExp(
    r'^(?[^,]*),(?[^,]*),(?\d+):(?\d{2})$');
final matchNamed = regNamed.firstMatch(line);
expect(matchNamed, isNotNull);
expect(matchNamed!.namedGroup('name'), 'ナイトパレード');
expect(matchNamed.namedGroup('place'), 'ハリウッドエリア');
expect(matchNamed.namedGroup('hour'), '19');
expect(matchNamed.namedGroup('minute'), '00');

ひらがな から カタカナ への変換

ひらがなとカタカナの文字コードの差が0x60なので、マッチした箇所に対して差分を加算してカタカナにします。

String convert(String value) => value.replaceAllMapped(
    RegExp('[ぁ-ゔ]'),
    (Match match) =>
        String.fromCharCode(match.group(0)!.codeUnitAt(0) + 0x60));
expect(convert('あいを'), 'アイヲ');

片仮名 から 平仮名 への変換

String convert(String value) => value.replaceAllMapped(
    RegExp('[ァ-ン]'),
    (Match match) =>
        String.fromCharCode(match.group(0)!.codeUnitAt(0) - 0x60));
expect(convert('アイヲ'), 'あいを');

正規表現で$がある場合はRawStringは使わない

$がある場合はRawStringは使わない方がよい。
RawStringを使わないと、$の後は変数名になるので、エスケープ文字が必要。
hasMatchのところもRawStringだと想定通りに動かない。

expect(RegExp(r'$test').hasMatch('test'), false);
expect(RegExp(r'$test').hasMatch('\$test'), false);
expect(RegExp(r'$test').hasMatch('\\\$test'), false);

const test = 'TEST';
expect(RegExp('$test').hasMatch('TEST'), true);
expect(RegExp('\\$test').hasMatch('\\$test'), true);
expect(RegExp('\\$test').hasMatch(r'$test'), false);

-Flutter Dart Unity アプリ開発