🐼

@faker-js/faker の内部実装を探る:ダミーデータ生成の仕組み

2024/12/09に公開

この記事はファンタラクティブ 2024 年アドベントカレンダー 12 月 9 日の記事です

はじめに

@faker-js/faker(以下 faker とします) は、ダミーデータを生成するためのライブラリです。
テストで使用したり、モックデータを生成するのに便利なライブラリです。

この記事では faker が内部でどのような処理が行われているのかを簡単に見ていき、faker の実装についての理解を深めることを目的とします。

TL;DR

const firstName = faker.person.firstName();

上記コードは、person モジュールの firstName メソッドを実行した時の処理です。
このコードを実行した時の処理を図示したものが以下になります。

faker インスタンス

faker は主に以下のようなコンセプトを持っています。
もしかしたら他にも挙げることができるかもしれませんが、この記事ではこれらのコンセプトを中心に見ていきます。

  • Modules (for basic datatypes and topic specific modules)
  • Definitions
  • Helpers
  • Randomizer

Modules (for basic datatypes and topic specific modules)

basic datatypes(日付や文字列、数値などを扱う)モジュールと、topic specific(Animal、Food、Person などを扱う)なモジュールがあります。

// basic datatypes
const randomNumber = faker.number.int(); // 2900970162509863

// topic specific
const randomBear = faker.animal.bear(); // "Asian black bear"

Definitions

Definitions は、実際にダミーデータを生成するために使用されるデータを指します。
Definitions は、faker インスタンスに渡す locale の値によって、扱うデータが変わります。

例えば、日本語のデータを扱いたい場合、以下のように fakerJA を import してアプリケーションで使用します。

import { fakerJA } from "@faker-js/faker";

これは、faker 内部では以下を実行しています。

src/locale/ja.ts
import ja from "../locales/ja";

export const faker = new faker({
  locale: [ja, en, base], // localeに`ja`を指定
});

jaは、ja locale の definitions のリスト を指します。以下のようなデータを持っています。

src/locales/ja/person.ts
const ja: LocaleDefinition = {
  company: {
    category: ["ガス", "保険", "印刷", ...],
    // 省略
  },
  person: {
    first_name: {
      generic: ["あゆみ", "きみ", ...],
      female: ["千代子", ...],
      male: ["正一", ...],
    },
    // 省略
  },
  // 省略
};

faker はこれらの definitions を元に、ダミーデータを生成します。

Helpers

ダミーデータを生成するのに便利なヘルパーを指します。
厳密にはヘルパーモジュールというモジュールの一つですが、先に挙げたModulesとは少し性質が異なるので、筆者が意図的に分けています。

特に便利なメソッドは arrayElement() で、配列からランダムな要素を返します。

const randomAnimal = faker.helpers.arrayElement(["cat", "dog", "mouse"]); // "dog"

他にも、weightedArrayElement()を使用して、配列の要素に重みをつけ、重みに応じた確率でランダムな要素を返すことができたり、fromRegExp()を使用して、正規表現にマッチするランダムな文字列を返す(一部制限あり)ことができたりと、便利なメソッドがあります。

詳しくは Helpers のドキュメントをご覧ください。

Randomizer

Randomizer は乱数を生成するための処理を指し、faker ではこれをカスタマイズすることができます。
デフォルトでは、メルセンヌ・ツイスタという擬似乱数生成器を使用します。元々 C 言語で実装された「MT19937 アルゴリズム」を TypeScript で実装して乱数を生成しているようです。

詳しくは実装を確認してみてください。

この Randomizer は、例えば Number モジュールのint()メソッドで乱数を生成するために使用されています。

faker.number.int(); // 2900970162509863
faker.number.int(100); // 52
faker.number.int({ min: 1000000 }); // 2900970162509863
faker.number.int({ max: 100 }); // 42
faker.number.int({ min: 10, max: 100 }); // 57
faker.number.int({ min: 10, max: 100, multipleOf: 10 }); // 50

このint()メソッドはまた、先ほどの Helpers のセクションで挙げたarrayElement()メソッドで、引数である配列のarray.length - 1を上限とした乱数を生成するために使用されています。

src/modules/helpers/index.ts
  /**
   * Returns random element from the given array.
   *
   * @template T The type of the elements to pick from.
   *
   * @param array The array to pick the value from.
   *
   * @throws If the given array is empty.
   *
   * @example
   * faker.helpers.arrayElement(["cat", "dog", "mouse"]) // "dog"
   *
   * @since 6.3.0
   */
  arrayElement<const T>(array: ReadonlyArray<T>): T {
    if (array.length === 0) {
      throw new fakerError("Cannot get value from empty dataset.");
    }

    const index =
      array.length > 1 ? this.faker.number.int({ max: array.length - 1 }) : 0;

    return array[index];
  }

Person モジュール

名前や役職などの個人情報を生成するモジュールです。

詳しくはこちらをご覧ください。

firstName メソッドの実装を見る

firstNameメソッドは、Person モジュールにおける、Person の firstName をランダムに取得するためのメソッドです。
これまで見てきたことを踏まえて、実装を確認してみます。

以下のコードは、firstName メソッドの実装です。

src/modules/person/index.ts
  firstName(sex?: SexType): string {
    return this.faker.helpers.arrayElement(
      selectDefinition(
        this.faker,
        sex,
        this.faker.definitions.person.first_name
      )
    );
  }

初登場のselectDefinition()実装を確認しましょう。

src/modules/person/index.ts
/**
 * Select a definition based on given sex.
 *
 * @param faker faker instance.
 * @param sex Sex.
 * @param personEntry Definitions.
 *
 * @returns Definition based on given sex.
 */
function selectDefinition<T>(
  faker: faker,
  sex: SexType | undefined,
  personEntry: PersonEntryDefinition<T>
): T[] {
  const { generic, female, male } = personEntry;
  switch (sex) {
    case Sex.Female: {
      return female ?? generic;
    }

    case Sex.Male: {
      return male ?? generic;
    }

    default: {
      return (
        generic ??
        faker.helpers.arrayElement([female, male]) ??
        // The last statement should never happen at run time. At this point in time,
        // the entry will satisfy at least (generic || (female && male)).
        // TS is not able to infer the type correctly.
        []
      );
    }
  }
}

JSDoc のコメントからわかる通り、selectDefinition()は、与えられたsexの値に応じて、definitions の配列を返します。
先ほど、definitions のセクションで見たように、definitions は 以下のようになっていました。
sexundefinedの場合は、generic の definition 配列を返し、それ以外の場合は、female か male の definition 配列を返すということですね。

src/locales/ja/person.ts
const ja: LocaleDefinition = {
  company: {
    category: ["ガス", "保険", "印刷", ...],
    // 省略
  },
  person: {
    first_name: {
      generic: ["あゆみ", "きみ", ...],
      female: ["千代子", ...],
      male: ["正一", ...],
    },
    // 省略
  },
  // 省略
};

そして、arrayElement()は、先ほど Helpers のセクション で見たように、配列からランダムな要素を返すメソッドです。

ということでまとめると firstName() メソッドは、

  • definitions からデータを取得
  • 取得した definition 配列からランダムな要素を返す

上記の流れでランダムな firstName を出力していることがわかりました。

おわりに

この記事では、faker の内部実装を簡単に見てきました。
実行すると色々な結果が返ってくる topic specific なデータは一見魔法のようにも感じますが、実際には、事前に定義されているデータから、乱数を生成して取得していることがわかりました。面白いですね。

また、Person モジュールのbio()メソッドなどは出力の pattern を定義し、他メソッドもしくは他モジュールの definitions と組み合わせて最終的な出力をするなど、いろいろなメソッドがあり面白いので気になった方はぜひ覗いてみてください。

明日は moyai さんの記事になります!

参考

https://fakerjs.dev/
https://github.com/faker-js/faker

ファンタラクティブテックブログ

Discussion