【Flutter】Dartでの文字列操作の基本と応用

  • 2024年10月26日
  • 2024年10月26日
  • Dart

Table of Contents

対象者

  • Dartを使ったアプリケーション開発に携わるソフトウェアエンジニア
  • 文字列操作に関心がある開発者
  • Stringで知らない機能があるんじゃないかと気になる人

はじめに

プログラミングにおいて文字列操作は避けて通れない重要な要素です。Dart言語も他の言語と同様、文字列の操作方法を正しく理解しておくことで、効率的なコードを書くことができます。しかし、文字列操作の方法に悩むことはありませんか?初めてDartに触れる方にとっては、他の言語との違いに戸惑うこともあるでしょう。

この記事では、具体的なコード例を交えながら、Dartにおける文字列操作の基本から応用までを丁寧に解説します。これにより、文字列操作の理解を深め、より質の高いコードを書く手助けとなるでしょう。

この記事を読んで、初めての機能を知ったり、アプリ作成時に役にたっていただけると嬉しいです!

この記事の範囲

この記事では、文字列、文字列操作の機能、数値との変換を取り扱ってます。Fileやバイナリとの変換、正規表現は以下のブログ記事を参照してください。

【Dart】バイナリのデータ変換: Uint8List, List<int>, ByteData, ByteBuffer, Base64, File, String

【Flutter/Dart】で 正規表現リファレンス RegExpで文字列チェック、置換などを一通り

定義

文字列の定義と宣言

シングルクォートとダブルクォートを使った文字列の定義方法と、それらにクォートを含める方法を説明します。Dartではシングルクォートを使うことが推奨されています。しかし文字列内にシングルクォートを使用したい場合、ダブルクォートを使うと、楽に書けます。

  • シングルクォート ' とダブルクォート " の使い分け
  • 文字列内にクォートを含める方法:
String greeting = 'こんにちは';
String name = "Dart";
expect('$greeting、$name!', equals('こんにちは、Dart!'));

String singleQuote = "He said, 'Hello!'";
String doubleQuote = 'She replied, "Hi!"';
expect(singleQuote, equals("He said, 'Hello!'"));
expect(doubleQuote, equals('She replied, "Hi!"'));

マルチライン文字列

複数行にわたる文字列を定義する方法を示します。

  • 三重のシングルクォート ''' を使用してマルチライン文字列を定義
String multiLine = '''
これは
複数行の
文字列です。
''';
expect(multiLine, equals('これは\n複数行の\n文字列です。\n'));

生の文字列(Raw String)

エスケープシーケンスを無視した文字列の定義方法を説明します。

  • r プレフィックスを使用して生の文字列(エスケープシーケンスを無視)を定義
String rawString = r'これは\nエスケープされません。';
expect(rawString, equals('これは\\nエスケープされません。'));

連結

このグループでは、文字列の連結方法についてテストしています。演算子や文字列補間、繰り返し、StringBuffer の使用方法を確認します。

文字列の連結 – +演算子を使用

+ 演算子を使って文字列を連結する方法を示します。

String hello = 'Hello';
String world = 'World';
String greetingConcat = hello + ' ' + world + '!';
expect(greetingConcat, equals('Hello World!'));

文字列の連結 – 文字列をそのまま並べる

文字列を隣接して配置することで連結する方法を説明します。

  • 文字列をそのまま並べて連結
String greetingAdjacent = 'Hello' ' ' 'World' '!';
expect(greetingAdjacent, equals('Hello World!'));

文字列補間(String Interpolation)

文字列補間を使って変数や式を文字列に埋め込む方法を示します。

  • $variable を使用して変数を埋め込む
  • ${expression} を使用して式を埋め込む
String flutter = 'Flutter';
expect('$flutterの長さは${flutter.length}', equals('Flutterの長さは7'));

文字列の繰り返し

* 演算子を使って文字列を繰り返す方法を説明します。

  • * 演算子による文字列の繰り返し
String repeated = 'abc' * 3;
expect(repeated, equals('abcabcabc'));

StringBuffer(何回も文字列連結をするときは早くなる)

StringBuffer を使って効率的に文字列を連結する方法を示します。

  • StringBuffer の使用
  • write, writeAll, writeln メソッドの利用
final buffer = StringBuffer();
buffer.write('a');
buffer.writeAll(['b', 1]);
buffer.writeln('2');
buffer.write(3);

expect(buffer.toString(), 'ab12\n3');

長さ取得

このグループでは、文字列やリストの長さを取得する方法、空文字列の確認方法をテストしています。

文字列の長さを取得

文字列の長さを取得する方法を示します。

  • 文字列の length プロパティを使用
String text = 'こんにちは';
expect(text.length, equals(5));

配列やリストの長さを取得

リストの長さを取得する方法を説明します。

  • リストの length プロパティを使用
List<String> languages = ['Dart', 'Flutter', 'JavaScript'];
expect(languages.length, equals(3));

空文字列の確認

文字列が空かどうかを確認する方法を示します。

  • isEmpty プロパティを使用
  • isNotEmpty プロパティを使用
String emptyText = '';
expect(emptyText.isEmpty, isTrue);
expect(emptyText.isNotEmpty, isFalse);

文字列の検索

このグループでは、文字列の部分取得や検索、置換方法をテストしています。

部分文字列の取得

文字列から特定の部分を抜き出す方法を示します。

  • substring メソッドを使用
String programming = 'Dart Programming';
String sub = programming.substring(5, 16);
expect(sub, equals('Programming'));

expect(programming.substring(5), equals('Programming'));
expect(programming.substring(5, 5), equals(''));
expect(programming.substring(5, 6), equals('P'));

文字列の検索 – 特定の文字列が含まれているか確認

文字列内に特定の文字列が含まれているかを確認します。

  • contains メソッドを使用
String searchText = 'Flutter is awesome';
expect(searchText.contains('awesome'), isTrue);
expect(searchText.contains('bad'), isFalse);

文字列の検索 – 指定した文字列で始まるか・終わるか確認

文字列が特定の文字列で始まるか、終わるかを確認します。

  • startsWith メソッドを使用
  • endsWith メソッドを使用
String searchText = 'Flutter is awesome';
expect(searchText.startsWith('Flutter'), isTrue);
expect(searchText.endsWith('awesome'), isTrue);

文字列の位置を取得

文字列内の特定の文字列の位置を取得します。

  • indexOf メソッドを使用
  • lastIndexOf メソッドを使用
String searchText = 'Flutter is awesome';
int index = searchText.indexOf('is');
expect(index, equals(8));

// (10-1)文字目以降から検索
expect(searchText.indexOf('e', 10), 13);

// 見つからない場合は-1
expect(searchText.indexOf('Dart'), -1);

expect(
  () => searchText.indexOf(searchText, 20),
  throwsA(isA<RangeError>()),
);

expect(searchText.lastIndexOf('e'), equals(17));
expect(searchText.lastIndexOf('e', 15), equals(13));
expect(
  () => searchText.lastIndexOf(searchText, 20),
  throwsA(isA<RangeError>()),
);

文字列の置換 – 全ての一致を置換

文字列内の特定の文字列をすべて置換します。

  • replaceAll メソッドを使用
String replaceText = 'Dart is fun. Dart is easy.';
String replaced = replaceText.replaceAll('Dart', 'Flutter');
expect(replaced, equals('Flutter is fun. Flutter is easy.'));

文字列の置換 – 最初の一致を置換

文字列内の最初の一致を置換します。

  • replaceFirst メソッドを使用
String replaceText = 'Dart is fun. Dart is easy.';
String replacedFirst = replaceText.replaceFirst('Dart', 'Flutter');
expect(replacedFirst, equals('Flutter is fun. Dart is easy.'));

文字列の置換 – 特定の範囲を置換

文字列の特定の範囲を置換します。

  • replaceRange メソッドを使用
String replaceRangeText = 'Hello, Dart!';
String replacedRange = replaceRangeText.replaceRange(7, 11, 'Flutter');
expect(replacedRange, equals('Hello, Flutter!'));

文字列の処理

このグループでは、文字列の分割やトリミング、文字列の付加、大文字・小文字の変換をテストしています。

文字列の分割

文字列を特定の区切り文字で分割します。

  • split メソッドを使用
String csv = 'apple,banana,orange';
List<String> fruits = csv.split(',');
expect(fruits, equals(['apple', 'banana', 'orange']));

文字列のトリミング – 両端の空白を削除

文字列の前後の空白を削除します。

  • trim メソッドを使用
String trimText = '   Dart   ';
expect(trimText.trim(), equals('Dart'));

文字列のトリミング – 左端または右端の空白を削除

文字列の左端または右端の空白を削除します。

  • trimLeft メソッドを使用
  • trimRight メソッドを使用
String trimText = '   Dart   ';
expect(trimText.trimLeft(), equals('Dart   '));
expect(trimText.trimRight(), equals('   Dart'));

文字列の付加

文字列に指定した文字や空白を付加します。

  • padLeft メソッドを使用
  • padRight メソッドを使用
expect('12'.padLeft(3), equals(' 12'));
expect('12'.padRight(3), equals('12 '));

expect('12'.padLeft(3, '0'), equals('012'));
expect('12'.padRight(3, '0'), equals('120'));

大文字・小文字の変換

文字列を大文字または小文字に変換します。

  • toUpperCase メソッドを使用
  • toLowerCase メソッドを使用
String caseText = 'Dart Programming';
expect(caseText.toUpperCase(), equals('DART PROGRAMMING'));
expect(caseText.toLowerCase(), equals('dart programming'));

特殊な文字列

このグループでは、特殊文字のエスケープやUnicode文字の使用方法をテストしています。

特殊文字のエスケープ

特殊文字をエスケープして文字列に含める方法を示します。

  • バックスラッシュ \ を使用してエスケープ
String escapedSingle = 'She said, \'Hello!\'';
expect(escapedSingle, equals("She said, 'Hello!'"));

String escapedDouble = "He replied, \"Hi!\"";
expect(escapedDouble, equals('He replied, "Hi!"'));

異なるクォートを使用してエスケープを避ける

エスケープを避けるために異なるクォートを使用します。

  • シングルクォートとダブルクォートを使い分け
String singleQuoteAlt = "She said, 'Hello!'";
String doubleQuoteAlt = 'He replied, "Hi!"';
expect(singleQuoteAlt, equals("She said, 'Hello!'"));
expect(doubleQuoteAlt, equals('He replied, "Hi!"'));

Unicode文字の使用

Unicodeコードポイントを使って特殊文字を表現します。

  • \uXXXX\u{XXXXXX} を使用
String heart = '\u2665';
expect(heart, equals('♥'));

String smile = '\u{1F600}';
expect(smile, equals('😀'));

// コピーライト、登録商標のやつ
expect('Flutter\u00AE', equals('Flutter®'));
expect('\u00A92017 Google', equals('©2017 Google'));

数字との変換

文字列から数値への変換 – intへの変換

文字列を整数に変換します。

  • int.parse メソッドを使用
String number = '42';
int value = int.parse(number);
expect(value, equals(42));

文字列から数値への変換 – 変換に失敗する可能性がある場合

変換に失敗する可能性がある場合、tryParseを使いましょう。数値に変換できなかった場合、nullが帰ってきて、引き続き処理ができます。

  • int.tryParse メソッドを使用
String invalidNumber = 'abc';
int? parseValue = int.tryParse(invalidNumber);
expect(parseValue, isNull);

expect(int.tryParse('123'), equals(123));

文字列から数値への変換 – doubleへの変換

文字列を浮動小数点数に変換します。

  • double.parse メソッドを使用
String doubleNumber = '3.14';
double doubleValue = double.parse(doubleNumber);
expect(doubleValue, equals(3.14));

数値から文字列への変換

数値を文字列に変換します。

  • toString メソッドを使用
int numericValue = 42;
String numericText = numericValue.toString();
expect(numericText, equals('42'));

double.parseのテスト

様々な形式の文字列を double.parse で解析します。

  • 科学的記数法や空白の処理
  • 特殊な値の処理(NaN、Infinity)
expect(double.parse('123'), equals(123));
expect(double.parse('123'), equals(123.0));
expect(double.parse('123'), equals(123.00));

expect(double.parse('  123 '), equals(123)); // 空白があっても大丈夫
expect(double.parse('1.23'), equals(1.23));
expect(double.parse('0.123'), equals(0.123));
expect(double.parse('.123'), equals(.123));

expect(double.parse('12e3'), equals(12000));
expect(double.parse('12.e3'), equals(12000));
expect(double.parse('12E+3'), equals(12000));
expect(double.parse('+12e+3'), equals(12000));
expect(double.parse('-12E+3'), equals(-12000));
expect(double.parse('12.3e3'), equals(12300));

expect(double.parse('12E-3'), equals(0.012));
expect(double.parse('12E-3'), equals(12 / 1000));

expect(double.parse('-NaN'), isNaN);
expect(double.parse('Infinity'), equals(double.infinity));

// べき乗とかできなかった
expect(() => double.parse('10^2'), throwsA(isA<FormatException>()));

1文字での処理

このグループでは、文字列を1文字ずつ操作する方法をテストしています。

文字列を1文字のリストに変換

文字列を1文字ずつのリストに分割します。

  • split('') メソッドを使用
List<String> apple = 'apple'.split('');
expect(apple, equals(['a', 'p', 'p', 'l', 'e']));

文字列の各文字へのアクセス

文字列の各文字にインデックスでアクセスします。

  • インデックス演算子 [] を使用
String accessText = 'Dart';
expect(accessText[0], equals('D'));
expect(accessText[1], equals('a'));

文字列のループ処理

文字列をループして各文字を処理します。

  • for ループとインデックスを使用
String loopText = 'Dart';
List<String> characters = [];
for (int i = 0; i < loopText.length; i++) {
  characters.add(loopText[i]);
}
expect(characters, equals(['D', 'a', 'r', 't']));

Unicode文字のループ処理

Unicode文字列をループ処理します。

  • runes プロパティを使用
  • String.fromCharCode メソッドを使用
String unicodeText = 'あいうえお';
List<String> unicodeCharacters = [];
for (var rune in unicodeText.runes) {
  var character = String.fromCharCode(rune);
  unicodeCharacters.add(character);
}
expect(unicodeCharacters, equals(['あ', 'い', 'う', 'え', 'お']));

応用

このグループでは、文字列操作の応用的なテストを行います。

特定の文字列を削除(空文字に置換)

文字列から特定の文字を削除します。

  • replaceAll メソッドを使用
expect('Dart'.replaceAll('D', ''), equals('art'));

ゼロ埋め

数値をゼロ埋めして指定の桁数に揃えます。

  • padLeft メソッドを使用
String zeroPadding(int value, int digits) {
  return value.toString().padLeft(digits, '0');
}

expect(zeroPadding(1, 4), equals('0001'));
expect(zeroPadding(12, 4), equals('0012'));
expect(zeroPadding(123, 4), equals('0123'));
expect(zeroPadding(1234, 4), equals('1234'));
expect(zeroPadding(12, 3), equals('012'));
expect(zeroPadding(12345, 4), equals('12345'));

3桁区切り

数値を3桁ごとに区切ります。

  • 数字を文字列に変換し、逆順にして処理
String convertThreeDigitSeparator(int value) {
  return value.toString().split('').reversed.reduce((result, e) {
    return e + (result.length % 4 == 3 ? ',' : '') + result;
  });
}

expect(convertThreeDigitSeparator(123), equals('123'));
expect(convertThreeDigitSeparator(1234), equals('1,234'));
expect(convertThreeDigitSeparator(12345), equals('12,345'));
expect(convertThreeDigitSeparator(123456), equals('123,456'));
expect(convertThreeDigitSeparator(1234567), equals('1,234,567'));

単語の先頭を大文字に変換

文字列の最初の文字を大文字に変換します。

  • インデックスと toUpperCase メソッドを使用
String capitalize(String original) {
  if (original.isEmpty) return original;
  return original[0].toUpperCase() + original.substring(1);
}

expect(capitalize('dart'), equals('Dart'));
expect(capitalize('flutter'), equals('Flutter'));
expect(capitalize(''), equals(''));

文字列の反転

文字列を反転させます。

  • split(''), reversed, join('') を使用
String reverse(String s) => s.split('').reversed.join('');

expect(reverse('Dart'), equals('traD'));
expect(reverse('こんにちは'), equals('はちにんこ'));
expect(reverse(''), equals(''));

回文(パリンドローム)の確認

文字列が回文かどうかを確認します。

  • 空白を削除し、小文字に変換して比較
bool isPalindrome(String original) {
  String normalized =
      original.replaceAll(RegExp(r'\s+'), '').toLowerCase();
  return normalized == normalized.split('').reversed.join('');
}

expect(isPalindrome('radar'), isTrue);
expect(isPalindrome('Dart'), isFalse);
expect(isPalindrome('A man a plan a canal Panama'), isTrue);

expect(isPalindrome('つ'), isTrue);
expect(isPalindrome('じもとひともじ'), isTrue);
expect(isPalindrome(''), isTrue);

CSV読み込み

CSV形式の文字列を解析してデータを取得します。

CSVファイルは以下のようにします

const data = '''
name, level, power
勇者,1,10
戦士,2,22
''';

ヘッダー固定

ヘッダーが固定の場合のCSVデータの読み込みを示します。

  • LineSplitter を使用して行に分割
  • split(',') を使用して列に分割
List<List<String>> readCsvFile(String data) {
  return const LineSplitter()
      .convert(data)
      .skip(1)
      .map((e) => e.trim().split(','))
      .toList();
}

// int.parseなどを使って適切なクラスに入れる
final result = readCsvFile(data);
expect(result[0], ['勇者', '1', '10']);
expect(result[1], ['戦士', '2', '22']);

ヘッダー変動

ヘッダーが変動する場合のCSVデータの読み込みを示します。

  • ヘッダー行をキーとして取得
  • 各行をマップに変換
List<Map<String, String>> readCsvFile(String data) {
  final lines = const LineSplitter().convert(data.trim());
  final keys = lines[0].split(',').map((e) => e.trim()).toList();
  return lines.skip(1).map((e) {
    final data = e.trim().split(',');
    final map = <String, String>{};
    for (int i = 0; i < keys.length; i++) {
      map[keys[i]] = data[i];
    }
    return map;
  }).toList();
}

// int.parseなどを使って適切なクラスに入れる
final result = readCsvFile(data);
expect(result[0], {'name': '勇者', 'level': '1', 'power': '10'});
expect(result[1], {'name': '戦士', 'level': '2', 'power': '22'});

ヘッダー変動 改善版

マップの生成を簡潔に行う方法を示します。

  • Map.fromIterables を使用してキーと値からマップを生成
List<Map<String, String>> readCsvFile(String data) {
  final lines = const LineSplitter().convert(data.trim());
  final keys = lines[0].split(',').map((e) => e.trim()).toList();
  return lines
      .skip(1)
      .map((line) => Map.fromIterables(keys, line.trim().split(',')))
      .toList();
}

final result = readCsvFile(data);
expect(result[0], {'name': '勇者', 'level': '1', 'power': '10'});
expect(result[1], {'name': '戦士', 'level': '2', 'power': '22'});

参考

まとめ

この記事では、Dartにおける文字列操作の基本から応用までを学びました。文字列の定義、連結、補間、検索、置換、分割、繰り返しなど、日常的なプログラミングで頻繁に使用する操作を具体的なコード例とともに解説しました。これにより、Dartでの文字列操作の理解が深まり、より効率的で質の高いコードを書く手助けとなったことでしょう。

今後のプロジェクトや学習で、この記事で学んだ知識を活かしていただければ幸いです。

ソース(main.dartにコピペして動作確認用)

import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';

void main() {
  group('定義', () {
    test('文字列の定義と宣言', () {
      String greeting = 'こんにちは';
      String name = "Dart";
      expect('$greeting、$name!', equals('こんにちは、Dart!'));

      String singleQuote = "He said, 'Hello!'";
      String doubleQuote = 'She replied, "Hi!"';
      expect(singleQuote, equals("He said, 'Hello!'"));
      expect(doubleQuote, equals('She replied, "Hi!"'));
    });

    test('マルチライン文字列', () {
      String multiLine = '''
これは
複数行の
文字列です。
''';
      expect(multiLine, equals('これは\n複数行の\n文字列です。\n'));
    });

    test('生の文字列(Raw String)', () {
      String rawString = r'これは\nエスケープされません。';
      expect(rawString, equals('これは\\nエスケープされません。'));
    });
  });

  group('連結', () {
    test('文字列の連結 - +演算子を使用', () {
      String hello = 'Hello';
      String world = 'World';
      String greetingConcat = hello + ' ' + world + '!';
      expect(greetingConcat, equals('Hello World!'));
    });

    test('文字列の連結 - 文字列をそのまま並べる', () {
      String greetingAdjacent = 'Hello' ' ' 'World' '!';
      expect(greetingAdjacent, equals('Hello World!'));
    });

    test('文字列補間(String Interpolation)', () {
      String flutter = 'Flutter';
      expect('$flutterの長さは${flutter.length}', equals('Flutterの長さは7'));
    });

    test('文字列の繰り返し', () {
      String repeated = 'abc' * 3;
      expect(repeated, equals('abcabcabc'));
    });

    test('StringBuffer(何回も文字列連結をするときは早くなる)', () {
      final buffer = StringBuffer();
      buffer.write('a');
      buffer.writeAll(['b', 1]);
      buffer.writeln('2');
      buffer.write(3);

      expect(buffer.toString(), 'ab12\n3');
    });
  });

  group('長さ取得', () {
    test('文字列の長さを取得', () {
      String text = 'こんにちは';
      expect(text.length, equals(5));
    });

    test('配列やリストの長さを取得', () {
      List<String> languages = ['Dart', 'Flutter', 'JavaScript'];
      expect(languages.length, equals(3));
    });

    test('空文字列の確認', () {
      String emptyText = '';
      expect(emptyText.isEmpty, isTrue);
      expect(emptyText.isNotEmpty, isFalse);
    });
  });
  group('文字列の検索', () {
    test('部分文字列の取得', () {
      //                    01234567890123456
      String programming = 'Dart Programming';
      String sub = programming.substring(5, 16);
      expect(sub, equals('Programming'));

      expect(programming.substring(5), equals('Programming'));
      expect(programming.substring(5, 5), equals(''));
      expect(programming.substring(5, 6), equals('P'));
    });

    test('文字列の検索 - 特定の文字列が含まれているか確認', () {
      String searchText = 'Flutter is awesome';
      expect(searchText.contains('awesome'), isTrue);
      expect(searchText.contains('bad'), isFalse);
    });

    test('文字列の検索 - 指定した文字列で始まるか・終わるか確認', () {
      String searchText = 'Flutter is awesome';
      expect(searchText.startsWith('Flutter'), isTrue);
      expect(searchText.endsWith('awesome'), isTrue);
    });

    test('文字列の位置を取得', () {
      //                   012345678901234567
      String searchText = 'Flutter is awesome';
      int index = searchText.indexOf('is');
      expect(index, equals(8));

      // (10-1)文字目以降から検索
      expect(searchText.indexOf('e', 10), 13);

      // 見つからない場合は-1
      expect(searchText.indexOf('Dart'), -1);

      expect(
        () => searchText.indexOf(searchText, 20),
        throwsA(isA<RangeError>()),
      );

      expect(searchText.lastIndexOf('e'), equals(17));
      expect(searchText.lastIndexOf('e', 15), equals(13));
      expect(
        () => searchText.lastIndexOf(searchText, 20),
        throwsA(isA<RangeError>()),
      );
    });

    test('文字列の置換 - 全ての一致を置換', () {
      String replaceText = 'Dart is fun. Dart is easy.';
      String replaced = replaceText.replaceAll('Dart', 'Flutter');
      expect(replaced, equals('Flutter is fun. Flutter is easy.'));
    });

    test('文字列の置換 - 最初の一致を置換', () {
      String replaceText = 'Dart is fun. Dart is easy.';
      String replacedFirst = replaceText.replaceFirst('Dart', 'Flutter');
      expect(replacedFirst, equals('Flutter is fun. Dart is easy.'));
    });

    test('文字列の置換 - 特定の範囲を置換', () {
      //012345678901
      String replaceRangeText = 'Hello, Dart!';
      String replacedRange = replaceRangeText.replaceRange(7, 11, 'Flutter');
      expect(replacedRange, equals('Hello, Flutter!'));
    });
  });

  group('文字列の処理', () {
    test('文字列の分割', () {
      String csv = 'apple,banana,orange';
      List<String> fruits = csv.split(',');
      expect(fruits, equals(['apple', 'banana', 'orange']));
    });

    test('文字列のトリミング - 両端の空白を削除', () {
      String trimText = '   Dart   ';
      expect(trimText.trim(), equals('Dart'));
    });

    test('文字列のトリミング - 左端または右端の空白を削除', () {
      String trimText = '   Dart   ';
      expect(trimText.trimLeft(), equals('Dart   '));
      expect(trimText.trimRight(), equals('   Dart'));
    });

    test('文字列の付加 ', () {
      expect('12'.padLeft(3), equals(' 12'));
      expect('12'.padRight(3), equals('12 '));

      expect('12'.padLeft(3), equals(' 12'));
      expect('12'.padRight(3), equals('12 '));

      expect('12'.padLeft(3, '0'), equals('012'));
      expect('12'.padRight(3, '0'), equals('120'));
    });

    test('大文字・小文字の変換', () {
      String caseText = 'Dart Programming';
      expect(caseText.toUpperCase(), equals('DART PROGRAMMING'));
      expect(caseText.toLowerCase(), equals('dart programming'));
    });
  });
  group('特殊な文字列', () {
    test('特殊文字のエスケープ', () {
      String escapedSingle = 'She said, \'Hello!\'';
      expect(escapedSingle, equals("She said, 'Hello!'"));

      String escapedDouble = "He replied, \"Hi!\"";
      expect(escapedDouble, equals('He replied, "Hi!"'));
    });

    test('異なるクォートを使用してエスケープを避ける', () {
      String singleQuoteAlt = "She said, 'Hello!'";
      String doubleQuoteAlt = 'He replied, "Hi!"';
      expect(singleQuoteAlt, equals("She said, 'Hello!'"));
      expect(doubleQuoteAlt, equals('He replied, "Hi!"'));
    });

    test('Unicode文字の使用', () {
      String heart = '\u2665';
      expect(heart, equals('♥'));

      String smile = '\u{1F600}';
      expect(smile, equals('😀'));

      // コピーライト、登録商標のやつ
      expect('Flutter\u00AE', equals('Flutter®'));
        expect('\u00A92017 Google', equals('©2017 Google'));
    });
  });

  group('数字との変換', () {
    test('文字列から数値への変換 - intへの変換', () {
      String number = '42';
      int value = int.parse(number);
      expect(value, equals(42));
    });

    test('文字列から数値への変換 - 変換に失敗する場合の確認', () {
      String invalidNumber = 'abc';
      int? parseValue = int.tryParse(invalidNumber);
      expect(parseValue, isNull);

      expect(int.tryParse('123'), equals(123));
    });

    test('文字列から数値への変換 - doubleへの変換', () {
      String doubleNumber = '3.14';
      double doubleValue = double.parse(doubleNumber);
      expect(doubleValue, equals(3.14));
    });

    test('数値から文字列への変換', () {
      int numericValue = 42;
      String numericText = numericValue.toString();
      expect(numericText, equals('42'));
    });

    test('double.parseのテスト', () {
      expect(double.parse('123'), equals(123));
      expect(double.parse('123'), equals(123.0));
      expect(double.parse('123'), equals(123.00));

      expect(double.parse('  123 '), equals(123)); // 空白があっても大丈夫
      expect(double.parse('1.23'), equals(1.23));
      expect(double.parse('0.123'), equals(0.123));
      expect(double.parse('.123'), equals(.123));

      expect(double.parse('12e3'), equals(12000));
      expect(double.parse('12.e3'), equals(12000));
      expect(double.parse('12E+3'), equals(12000));
      expect(double.parse('+12e+3'), equals(12000));
      expect(double.parse('-12E+3'), equals(-12000));
      expect(double.parse('12.3e3'), equals(12300));

      expect(double.parse('12E-3'), equals(0.012));
      expect(double.parse('12E-3'), equals(12 / 1000));

      expect(double.parse('-NaN'), isNaN);
      expect(double.parse('Infinity'), equals(double.infinity));

      // べき乗とかできなかった
      expect(() => double.parse('10^2'), throwsA(isA<FormatException>()));
    });
  });

  group('1文字での処理', () {
    test('文字列を1文字のリストに変換', () {
      List<String> apple = 'apple'.split('');
      expect(apple, equals(['a', 'p', 'p', 'l', 'e']));
    });

    test('文字列の各文字へのアクセス', () {
      String accessText = 'Dart';
      expect(accessText[0], equals('D'));
      expect(accessText[1], equals('a'));
    });

    test('文字列のループ処理', () {
      String loopText = 'Dart';
      List<String> characters = [];
      for (int i = 0; i < loopText.length; i++) {
        characters.add(loopText[i]);
      }
      expect(characters, equals(['D', 'a', 'r', 't']));
    });

    test('Unicode文字のループ処理', () {
      String unicodeText = 'あいうえお';
      List<String> unicodeCharacters = [];
      for (var rune in unicodeText.runes) {
        var character = String.fromCharCode(rune);
        unicodeCharacters.add(character);
      }
      expect(unicodeCharacters, equals(['あ', 'い', 'う', 'え', 'お']));
    });
  });

  group('応用', () {
    test('特定の文字列を削除(空文字に置換)', () {
      expect('Dart'.replaceAll('D', ''), equals('art'));
    });

    test('ゼロ埋め', () {
      String zeroPadding(int value, int digits) {
        return value.toString().padLeft(digits, '0');
      }

      expect(zeroPadding(1, 4), equals('0001'));
      expect(zeroPadding(12, 4), equals('0012'));
      expect(zeroPadding(123, 4), equals('0123'));
      expect(zeroPadding(1234, 4), equals('1234'));
      expect(zeroPadding(12, 3), equals('012'));
      expect(zeroPadding(12345, 4), equals('12345'));
    });

    test('3桁区切り', () {
      String convertThreeDigitSeparator(int value) {
        return value.toString().split('').reversed.reduce((result, e) {
          return e + (result.length % 4 == 3 ? ',' : '') + result;
        });
      }

      expect(convertThreeDigitSeparator(123), equals('123'));
      expect(convertThreeDigitSeparator(1234), equals('1,234'));
      expect(convertThreeDigitSeparator(12345), equals('12,345'));
      expect(convertThreeDigitSeparator(123456), equals('123,456'));
      expect(convertThreeDigitSeparator(1234567), equals('1,234,567'));
    });

    test('単語の先頭を大文字に変換', () {
      String capitalize(String original) {
        if (original.isEmpty) return original;
        return original[0].toUpperCase() + original.substring(1);
      }

      expect(capitalize('dart'), equals('Dart'));
      expect(capitalize('flutter'), equals('Flutter'));
      expect(capitalize(''), equals(''));
    });

    test('文字列の反転', () {
      String reverse(String s) => s.split('').reversed.join('');

      expect(reverse('Dart'), equals('traD'));
      expect(reverse('こんにちは'), equals('はちにんこ'));
      expect(reverse(''), equals(''));
    });

    test('回文(パリンドローム)の確認', () {
      bool isPalindrome(String original) {
        // 空白を削除
        String normalized =
            original.replaceAll(RegExp(r'\s+'), '').toLowerCase();
        return normalized == normalized.split('').reversed.join('');
      }

      expect(isPalindrome('radar'), isTrue);
      expect(isPalindrome('Dart'), isFalse);
      expect(isPalindrome('A man a plan a canal Panama'), isTrue);

      expect(isPalindrome('つ'), isTrue);
      expect(isPalindrome('じもとひともじ'), isTrue);
      expect(isPalindrome(''), isTrue);
    });

    group('CSV読み込み', () {
      const data = '''
      name, level, power
      勇者,1,10
      戦士,2,22
      ''';
      test('ヘッダー固定', () {
        List<List<String>> readCsvFile(String data) {
          return const LineSplitter()
              .convert(data)
              .skip(1)
              .map((e) => e.trim().split(','))
              .toList();
        }

        // int.parseなどを使って適切なクラスに入れる
        final result = readCsvFile(data);
        expect(result[0], ['勇者', '1', '10']);
        expect(result[1], ['戦士', '2', '22']);
      });

      test('ヘッダー変動', () {
        List<Map<String, String>> readCsvFile(String data) {
          // dataの最初と最後の改行を削除するためtrimを入れている
          final lines = const LineSplitter().convert(data.trim());
          final keys = lines[0].split(',').map((e) => e.trim()).toList();
          return lines.skip(1).map((e) {
            final data = e.trim().split(',');
            final map = <String, String>{};
            for (int i = 0; i < keys.length; i++) {
              map[keys[i]] = data[i];
            }
            return map;
          }).toList();
        }

        // int.parseなどを使って適切なクラスに入れる
        final result = readCsvFile(data);
        expect(result[0], {'name': '勇者', 'level': '1', 'power': '10'});
        expect(result[1], {'name': '戦士', 'level': '2', 'power': '22'});
      });

      test('ヘッダー変動 改善版', () {
        List<Map<String, String>> readCsvFile(String data) {
          final lines = const LineSplitter().convert(data.trim());
          final keys = lines[0].split(',').map((e) => e.trim()).toList();
          return lines
              .skip(1)
              .map((line) => Map.fromIterables(keys, line.trim().split(',')))
              .toList();
        }

        final result = readCsvFile(data);
        expect(result[0], {'name': '勇者', 'level': '1', 'power': '10'});
        expect(result[1], {'name': '戦士', 'level': '2', 'power': '22'});
      });
    });
  });
}