🔡

[Dart]乱数文字列を生成する

2024/10/05に公開

Dart に限らずどの言語でも乱数文字列を生成するコードは以下のように書くことができます.

import 'dart:math';

void main() {
 final random = Random();
 final s = "abcdefghijklmnopqrstuvwxyz"; // 生成する文字列の元となる文字
 final len = 10;
 String g = "";
 for (int i = 0; i < len; i++) {
  g += s[random.nextInt(s.length)];
 }
 print(g);
}

ただこのままだと使用する文字種を変更するのが難しく使い勝手があまりよくありません. Java の Apache commons-lang3 の RandomStringUtil[1] のような使い勝手が欲しいです. ということでより使いやすい乱数文字列を生成する関数を作成します.

結論

細かいことは置いておいて結論から.

DartPad から動作確認できます.

https://dartpad.dev/?id=e1ebc05fbf3aeb7d5ad0c8a236a52a0a

インターフェースを決める

まずは使いやすいインターフェースを決めます. 少なくとも文字数を与えて文字列を得る関数であるということは間違いないでしょう.

String generateRandomString(int length);

さて, ここで使用する文字種を指定できるようにしたいです. Apache commons-lang3 の RandomStringUtils では RandomStringUtils#randomAlphabetic のように関数名で使用する文字種を指定できるようになっています. Dart でも同様に文字種ごとに関数を分けることができますが, 少し考えてみます.

使用する文字列は小文字のアルファベットのみや数字のみ, もしくは半角英数のようにそれらの組み合わせである場合があります. そのため使用する文字種ごとに関数を分けると関数の数が膨大になってしまいます. 文字種ごとに関数を分けるよりも引数として与えるのがよさそうです.

文字種ごとに enum を用意してやり, 使用する文字種としてその配列を受け取ればよさそうです.

enum CodeType {
  alphabeticSmall,
  alphabeticCapital,
  numeric
}

String generateRandomString(int length, List<CodeType> containsCodeTypes);

また Dart では名前付き引数を使ってデフォルト引数とすることができます. デフォルトでは半角英数としておきましょう.

enum CodeType {
  alphabeticSmall,
  alphabeticCapital,
  numeric
}

String generateRandomString(int length,
    {List<CodeType> containsCodeTypes = const [
      CodeType.alphabeticSmall,
      CodeType.alphabeticCapital,
      CodeType.numeric
    ]});

またパスワードなどに使う場合, 半角英数以外の記号も使用できるようにしたいです. 記号の場合はURLセーフな文字列など使用したい文字が文脈によって違うことが多いです. そこで記号などの文字種は直接付与できるようにします. デフォルトでは記号を使わないようにします.

enum CodeType {
  alphabeticSmall,
  alphabeticCapital,
  numeric
}

String generateRandomString(int length,
    {List<CodeType> containsCodeTypes = const [
      CodeType.alphabeticSmall,
      CodeType.alphabeticCapital,
      CodeType.numeric
    ],
    String containsOthers = ""});

これで関数のインターフェースができました. 続いて内部処理を実装します.

処理の実装

関数のインターフェースから決めましたが, 内部処理は元の乱数文字列を生成するコードとほとんど変わりません. 使用する文字の一覧からランダムに1文字ずつ拾ったものを返すだけです.

import 'dart:math';

final _random = Random.secure();

String generateRandomString(int length) {
  List<String> generator; // 使用する文字の一覧
  return List<String>.generate(
      length, (i) => generator[_random.nextInt(generator.length)]).join();
}

あとは使用する文字の一覧を与えてやるだけです.

まず簡単な記号など直接付与する文字を与えます. 単純にリストに追加するだけです. 乱数文字列の生成には1文字ずつの文字配列を前提にしているため, 引数の文字列を1文字ずつの文字配列にすることだけ注意します.

import 'dart:math';

final _random = Random.secure();

String generateRandomString(int length,
    {String containsOthers = ""}) {
  List<String> generator = List.empty(growable: true); // 使用する文字の一覧
  generator.addAll(containsOthers.split(""));
  return List<String>.generate(
      length, (i) => generator[_random.nextInt(generator.length)]).join();
}

void main() {
  print(generateRandomString(10, containsOthers: "!?@#%^&*~-_"));
}

これで与えた文字列を利用して乱数文字列を生成する関数ができました.

次に数字などの文字種を使えるようにします. 文字種は enum で指定できるようにしました. enum にはメンバー変数を与えることができるため, enum から使用する文字列をそのまま取り出すようにすることができます.

import 'dart:math';

enum CodeType {
  alphabeticSmall("abcdefghijklmnopqrstuvwxyz");

  final String chars;
  const CodeType(this.chars);
}

final _random = Random.secure();

String generateRandomString(int length,
    {List<CodeType> containsCodeTypes = const [
      CodeType.alphabeticSmall
    ],
    String containsOthers = ""}) {
  List<String> generator = List.empty(growable: true); // 使用する文字の一覧
  for (CodeType type in containsCodeTypes) {
    generator.addAll(type.chars.split(""));
  }
  generator.addAll(containsOthers.split(""));
  return List<String>.generate(
      length, (i) => generator[_random.nextInt(generator.length)]).join();
}

小文字のアルファベットを書いたところでめんどくさくなってきました. もっと簡単に追加できるようにしたいです. 例えば正規表現の [a-z] のように指定できるようにしたいです. 小文字のアルファベット a から z までは文字コードが連続しているため, 開始から終了までの文字コードの配列を作ればよさそうです[2].

List<String> _charactors(String start, String end) {
  if (end.codeUnitAt(0) - start.codeUnitAt(0) <= 0) {
    throw RangeError(
        "Invalid character code from start to end. start: ${start.codeUnitAt(0)}, end: ${end.codeUnitAt(0)}.");
  }
  return List<String>.generate(end.codeUnitAt(0) - start.codeUnitAt(0) + 1,
      (i) => String.fromCharCode(start.codeUnitAt(0) + i));
}

Map<CodeType, List<String>> _codes = Map.fromIterables(CodeType.values,
    CodeType.values.map((code) => _charactors(code.start, code.end)));

void main() {
  print(_charactors('a', 'z'));
}

文字種は enum を使っていますが, enum のメンバー変数は関数を使って動的に設定することはできません. そこで enum のメンバー変数としては文字種の開始と終了だけを設定して, enum をキーとした連想配列から文字列を取得できるようにします.

enum CodeType {
  alphabeticSmall('a', 'z'),
  alphabeticCapital('A', 'Z'),
  numeric('0', '9'),
  kana('あ', 'ん');

  final String start;
  final String end;
  const CodeType(this.start, this.end);
}

List<String> _charactors(String start, String end) {
  if (end.codeUnitAt(0) - start.codeUnitAt(0) <= 0) {
    throw RangeError(
        "Invalid character code from start to end. start: ${start.codeUnitAt(0)}, end: ${end.codeUnitAt(0)}.");
  }
  return List<String>.generate(end.codeUnitAt(0) - start.codeUnitAt(0) + 1,
      (i) => String.fromCharCode(start.codeUnitAt(0) + i));
}

Map<CodeType, List<String>> _codes = Map.fromIterables(CodeType.values,
    CodeType.values.map((code) => _charactors(code.start, code.end)));

void main() {
  for (final value in _codes.values) {
    print(value);
  }
}

これでひらがなでも簡単に追加することができるようになりました.

これを組み合わせると, 以下のコードになります.

脚注
  1. https://commons.apache.org/proper/commons-lang/javadocs/api-3.9/org/apache/commons/lang3/RandomStringUtils.html ↩︎

  2. 以下を参考. https://stackoverflow.com/questions/67897675/how-to-generate-a-list-of-all-alphabets-uppercase-in-dart/67898387#67898387 ↩︎

Discussion