📚

産まれてくる我が子の名前が決まらないので名前製造アプリを作った話

2023/01/21に公開

我が家に新しい家族が増えるということで大変嬉しいことなのですが名前が決まらない。うちのおくさまが絶対2文字にするというので五十音全部の組み合わせ考えても50音 x 50音 = 2500通りくらいとして毎日25種類の2文字の組み合わせを考えれば100日で全組み合わせ確認できるやんけとなったので二文字の名前の組み合わせを生成するアプリを自作したよという話。

作成したものはこちら

漢字いくつか割り当てた方がイメージしやすいかと思ったけど思ってたのとチガウ、ゼンゼンカワイクナイ

setup

今回はブラウザ上でサクッと実装したかったので一番使い慣れているReact + viteで構築しました。

npm create vite

✔ Project name: … name-generator
? Select a framework: › - Use arrow-keys. Return to submit.
    Vanilla
    Vue
❯   React
    Preact
    Lit
    Svelte
    Others

? Select a variant: › - Use arrow-keys. Return to submit.
    JavaScript
❯   TypeScript
    JavaScript + SWC
    TypeScript + SWC

cd name-generator
npm install
npm run dev

問題なければローカルで起動が確認できるはず。

最低限prettierとtailwindだけインストール

npm install -D prettier
npm install -D tailwindcss postcss autoprefixer
npm install -D prettier-plugin-tailwindcss // いい感じにtailwindのクラスを並び替えしてくれるらしいので入れた

tailwindの初期化

npx tailwindcss init -p

tailwind.config.cjsを以下のように書き換え

tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
-   content: [],
+   content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
};

index.cssに以下追記

index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

prittierの設定

.prettierrc
{
  "singleQuote": true,
  "trailingComma": "all",
  "endOfLine": "auto"
}

ランダムでひらがなの組み合わせを生成する

とりあえず、ランダムでひらがなの組み合わせを生成してみる

// prettier-ignore
const charList: Readonly<string[]> = [
  'あ', 'い', 'う', 'え', 'お',
  'か', 'き', 'く', 'け', 'こ',
  'さ', 'し', 'す', 'せ', 'そ',
  'た', 'ち', 'つ', 'て', 'と',
  'な', 'に', 'ぬ', 'ね', 'の',
  'は', 'ひ', 'ふ', 'へ', 'ほ',
  'ま', 'み', 'む', 'め', 'も',
  'や', 'ゆ', 'よ',
  'ら', 'り', 'る', 'れ', 'ろ',
  //'わ', 'を', 'ん',
  // 'が', 'ぎ', 'ぐ', 'げ', 'ご',
  // 'ざ', 'じ', 'ず', 'ぜ', 'ぞ',
  // 'だ', 'ぢ', 'づ', 'で', 'ど',
  // 'ば', 'び', 'ぶ', 'べ', 'ぼ',
  // 'ぱ', 'ぴ', 'ぷ', 'ぺ', 'ぽ',
]

// 文字数分ランダムに文字を選別
const chars: string[] = [];
for (let i = 0; i < nameCount; i++) {
  chars.push(charList[Math.floor(Math.random() * charList.length)]);
}

// 名前を決定する
const name = chars.reduce((accum, current) => {
  return accum + current;
}, '');

これで処理が走るごとにlistからランダムで文字を取得して名前を決定することができる。
(わ行と濁音、半濁音の文字をコメントアウトしたのはこれらの文字が入るととても人の名前とは思えないネーミングになるから。)

漢字の候補を表示してみる

これだけではあっさりしすぎているので決まった名前の漢字の候補みたいなのも表示したい。最悪常用漢字のAPI作るかとも思ったがちゃんとそのようなAPIが存在してました。

Kanji aliveという漢字を学ぶための無償学習教材があるのですが、OSSとして漢字データなどを全て公開してくださっており、APIも公開しているようなので今回はそのAPIを使用します。

https://github.com/kanjialive/kanji-data-media

https://rapidapi.com/KanjiAlive/api/learn-to-read-and-write-japanese-kanji

APIはRapidAPIで公開されていて、初めて使ったけどかなり使いやすくてエンドポイントごとにリクエスト例がかなり充実していて、その場でresultも確認できる。

例えば、axiosで以下のようなリクエストをすると

const axios = require("axios");

const options = {
  method: 'GET',
  url: 'https://kanjialive-api.p.rapidapi.com/api/public/search/advanced/',
  params: {on: 'シン'},
  headers: {
    'X-RapidAPI-Key': 'xxxx',
    'X-RapidAPI-Host': 'kanjialive-api.p.rapidapi.com'
  }
};

axios.request(options).then(function (response) {
	console.log(response.data);
}).catch(function (error) {
	console.error(error);
});

こんな感じのレスポンスが返ってくる

[
  {
    "kanji": {
      "character": "針",
      "stroke": 10
    },
    "radical": {
      "character": "",
      "stroke": 8,
      "order": 206
    }
  },
  {
    "kanji": {
      "character": "新",
      "stroke": 13
    },
    "radical": {
      "character": "⽄",
      "stroke": 4,
      "order": 89
    }
  },
  {
    "kanji": {
      "character": "心",
      "stroke": 4
    },
    "radical": {
      "character": "⼼",
      "stroke": 4,
      "order": 80
    }
  },
  {
    "kanji": {
      "character": "森",
      "stroke": 12
    },
    "radical": {
      "character": "⽊",
      "stroke": 4,
      "order": 97
    }
  },
  {
    "kanji": {
      "character": "信",
      "stroke": 9
    },
    "radical": {
      "character": "⺅",
      "stroke": 2,
      "order": 11
    }
  },
  {
    "kanji": {
      "character": "申",
      "stroke": 5
    },
    "radical": {
      "character": "⽥",
      "stroke": 5,
      "order": 128
    }
  },
  {
    "kanji": {
      "character": "震",
      "stroke": 15
    },
    "radical": {
      "character": "⻗",
      "stroke": 8,
      "order": 211
    }
  },
  {
    "kanji": {
      "character": "辛",
      "stroke": 7
    },
    "radical": {
      "character": "⾟",
      "stroke": 7,
      "order": 197
    }
  },
  {
    "kanji": {
      "character": "親",
      "stroke": 16
    },
    "radical": {
      "character": "⾒",
      "stroke": 7,
      "order": 179
    }
  },
  {
    "kanji": {
      "character": "伸",
      "stroke": 7
    },
    "radical": {
      "character": "⺅",
      "stroke": 2,
      "order": 11
    }
  },
  {
    "kanji": {
      "character": "寝",
      "stroke": 13
    },
    "radical": {
      "character": "⼧",
      "stroke": 3,
      "order": 48
    }
  },
  {
    "kanji": {
      "character": "真",
      "stroke": 10
    },
    "radical": {
      "character": "⽬",
      "stroke": 5,
      "order": 137
    }
  },
  {
    "kanji": {
      "character": "深",
      "stroke": 11
    },
    "radical": {
      "character": "⺡",
      "stroke": 3,
      "order": 76
    }
  },
  {
    "kanji": {
      "character": "進",
      "stroke": 11
    },
    "radical": {
      "character": "⻌",
      "stroke": 3,
      "order": 72
    }
  },
  {
    "kanji": {
      "character": "身",
      "stroke": 7
    },
    "radical": {
      "character": "⾝",
      "stroke": 7,
      "order": 194
    }
  },
  {
    "kanji": {
      "character": "神",
      "stroke": 9
    },
    "radical": {
      "character": "⺭",
      "stroke": 4,
      "order": 121
    }
  },
  {
    "kanji": {
      "character": "臣",
      "stroke": 7
    },
    "radical": {
      "character": "⾂",
      "stroke": 6,
      "order": 203
    }
  }
]

この例は音読み検索だけど訓読み検索とかいろいろあってほんとよくできてるのでまた使いたい

ということで、以下のような関数を作成し、指定の1文字で漢字検索をして返ってきた候補を組み合わせて漢字候補を生成している。レスポンスの型は上記のようなレスポンス例をjsonファイルにして配置してそれをimportして生成した。

  export type KanjiKunyomiResponse = typeof kanjiKunyomi;
  // 訓読み指定で漢字一覧を取得する
  const getKanjiKunyomiName = useCallback(
    async (kun: string): Promise<KanjiKunyomiResponse> => {
      const options = {
        method: 'GET',
        url: 'https://kanjialive-api.p.rapidapi.com/api/public/search/advanced/',
        params: { kun },
        headers: {
          'X-RapidAPI-Key': import.meta.env.VITE_RAPID_API_KEY,
          'X-RapidAPI-Host': 'kanjialive-api.p.rapidapi.com',
        },
      };
      try {
        return await (
          await axios.request(options)
        ).data;
      } catch (e) {
        return Promise.reject(e);
      }
    },
    [],
  );

コードの全容はこんな感じ

App.tsx
import './App.css';
import { useGetKanjiName } from './hooks/useGetKanjiName';

const App = () => {
  const {
    familyName,
    setFamilyName,
    nameCount,
    setNameCount,
    fullName,
    kanjiNames,
    executeNaming,
  } = useGetKanjiName();

  return (
    <div className="App">
      <div>
        <div className="text-9xl">{fullName}</div>
        <div className="flex justify-center">
          {Array.from(kanjiNames).map((name) => {
            return (
              <div className="p-2" key={name}>
                {familyName} {name}
              </div>
            );
          })}
        </div>
      </div>
      <div>
        <label htmlFor="family-name-input" className="mr-3 text-blue-300">
          Family Name
        </label>
        <input
          type={'text'}
          id="family-name-input"
          className="rounded-md p-2"
          value={familyName}
          onChange={(e) => setFamilyName(e.target.value)}
        />
      </div>
      <div className="my-2 divide-x-2"></div>
      <div>
        <label htmlFor="name-count-input" className="mr-3 text-blue-300">
          Name Count
        </label>
        <input
          type={'number'}
          min={1}
          max={10}
          id="name-count-input"
          className="rounded-md p-2"
          value={nameCount}
          onChange={(e) => setNameCount(Number(e.target.value))}
        />
      </div>
      <button className="mt-5 bg-cyan-700" onClick={() => executeNaming()}>
        Execute
      </button>
    </div>
  );
};

export default App;
useGetKanjiName.ts
import { useCallback, useState } from 'react';
import axios from 'axios';
import kanjiKunyomi from '../resource/kunyomi_reading.json';
// import 'dotenv/config';

// prettier-ignore
const charList: Readonly<string[]> = [
  'あ', 'い', 'う', 'え', 'お',
  'か', 'き', 'く', 'け', 'こ',
  'さ', 'し', 'す', 'せ', 'そ',
  'た', 'ち', 'つ', 'て', 'と',
  'な', 'に', 'ぬ', 'ね', 'の',
  'は', 'ひ', 'ふ', 'へ', 'ほ',
  'ま', 'み', 'む', 'め', 'も',
  'や', 'ゆ', 'よ',
  'ら', 'り', 'る', 'れ', 'ろ',
  //'わ', 'を', 'ん',
  // 'が', 'ぎ', 'ぐ', 'げ', 'ご',
  // 'ざ', 'じ', 'ず', 'ぜ', 'ぞ',
  // 'だ', 'ぢ', 'づ', 'で', 'ど',
  // 'ば', 'び', 'ぶ', 'べ', 'ぼ',
  // 'ぱ', 'ぴ', 'ぷ', 'ぺ', 'ぽ',
]

const KANJI_NAME_COUNT: Readonly<number> = 5;

export type KanjiKunyomiResponse = typeof kanjiKunyomi;

export const useGetKanjiName = () => {
  const [familyName, setFamilyName] = useState('');
  const [nameCount, setNameCount] = useState(2);
  const [fullName, setFullName] = useState('');
  const [kanjiNames, setKanjiNames] = useState<Set<string>>(new Set());

  // 訓読み指定で漢字一覧を取得する
  const getKanjiKunyomiName = useCallback(
    async (kun: string): Promise<KanjiKunyomiResponse> => {
      const options = {
        method: 'GET',
        url: 'https://kanjialive-api.p.rapidapi.com/api/public/search/advanced/',
        params: { kun },
        headers: {
          'X-RapidAPI-Key': import.meta.env.VITE_RAPID_API_KEY,
          'X-RapidAPI-Host': 'kanjialive-api.p.rapidapi.com',
        },
      };
      try {
        return await (
          await axios.request(options)
        ).data;
      } catch (e) {
        return Promise.reject(e);
      }
    },
    [],
  );

  // 命名実行
  const executeNaming = useCallback(() => {
    // 文字数分ランダムに文字を選別
    const chars: string[] = [];
    for (let i = 0; i < nameCount; i++) {
      chars.push(charList[Math.floor(Math.random() * charList.length)]);
    }

    // 名前を決定する
    const name = chars.reduce((accum, current) => {
      return accum + current;
    }, '');

    // kanjiAPIで文字ごとの漢字一覧を取得
    const promises: Promise<KanjiKunyomiResponse>[] = chars.map(
      (char: string) => {
        return getKanjiKunyomiName(char);
      },
    );

    Promise.all(promises)
      .then((responses: KanjiKunyomiResponse[]) => {
        // 全ての文字で漢字が見つかったかどうか
        let notFoundKanji = false;
        for (const res of responses) {
          if (res.length === 0) {
            notFoundKanji = true;
          }
        }
        if (notFoundKanji) {
          return;
        }

        const totalCombination = responses.reduce((accum, current) => {
          return accum * current.length;
        }, 1);

        const limit =
          totalCombination <= KANJI_NAME_COUNT
            ? totalCombination
            : KANJI_NAME_COUNT;

        // 規定の数に達するまで組み合わせを探す
        const kanjiNames = new Set<string>();
        while (kanjiNames.size !== limit) {
          let kanjiName = '';
          responses.forEach((res) => {
            kanjiName +=
              res[Math.floor(Math.random() * res.length)].kanji.character;
          });
          if (!kanjiNames.has(kanjiName)) {
            kanjiNames.add(kanjiName);
          }
        }
        setKanjiNames(kanjiNames);
        setFullName(`${familyName} ${name}`);
      })
      .catch((err) => console.error(err));
  }, [familyName]);

  return {
    familyName,
    setFamilyName,
    nameCount,
    setNameCount,
    fullName,
    setFullName,
    kanjiNames,
    setKanjiNames,
    getKanjiKunyomiName,
    executeNaming,
  };
};

一応リポジトリはこちら
https://github.com/JY8752/name-generator

せっかくなので公開してみる

同じように悩む新米パパエンジニアがどこかにいるかもしれないので公開する。今回はCloud Flare Pagesを使用した。

CLIツールのwranglerがなければインストール

npm install -g wrangler

ログインしてビルドしたものをデプロイ

wrangler login
npm run build
source .env; npx wrangler pages publish dist

え、めちゃくちゃ簡単。フロントのデプロイまわりの進化すげー、、

まとめ

kanji aliveのAPIがよくできているので、興味ある方は使ってみてください!なお、まだ見ぬ我が子の名前はまだ決まっていません。以上!

GitHubで編集を提案

Discussion