「文字の類似度」からポケモン名をサジェストするコンポーネントを実装【React】

# 概要

前回、ChatGPTを利用してレーベンシュタイン距離をもとにサジェストの配列を作成するプログラムを実装しました。

https://qiita.com/ishi720/items/3f58685ce6230c0e4d7d

今回は、Reactを使ったWEBアプリケーションに組み込んで実装したいと思います。

# 環境

- React
- Javascript
- sass



# 完成したもの

![Animation4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/dae4535f-b4af-ea25-b754-aa9307239eb9.gif)

一番名前入力が難しいであろう「バウッツェル」を曖昧な入力でも補完することができます。

**リポジトリはこちらとなります**

https://github.com/ishi720/levenshtein_distance_search


# プログラム

```js:App.js
import React, { useState, useEffect } from 'react';
import styles from './App.scss';
import { pokeNameList } from './pokeNameList';

const App = () => {
  return <SuggestForm />;
};

const SuggestForm = () => {
  const [value, setValue] = useState('');
  const suggestNum = 15 ;
  const wordList = pokeNameList;
  const [suggestions, setSuggestions] = useState([]);

  useEffect(() => {
    const suggestions = getSuggest(hiraganaToKatakana(value), wordList, suggestNum);
    setSuggestions(suggestions);
  }, [value, wordList]);

  // イベント
  const handleChange = (event) => {
    const updatedValue = event.target.value;
    setValue(updatedValue);
  };
  /**
  * クリックされたワードを検索ボックスにセット
  * @param {word} str1 文字列
  */
  const handleClickWord = (word) => {
    setValue(word); 
  };
  /**
  * レーベンシュタイン距離の計測
  * @param {string} str1 文字列1
  * @param {string} str2 文字列2
  * @return {array} レーベンシュタイン距離の計測結果
  */
  const levenshteinDistance = (str1, str2) => {
    const len1 = str1.length;
    const len2 = str2.length;
    
    const dp = [];
    
    for (let i = 0; i <= len1; i++) {
      dp[i] = [];
      for (let j = 0; j <= len2; j++) {
        if (i === 0) {
          dp[i][j] = j;
        } else if (j === 0) {
          dp[i][j] = i;
        } else {
          dp[i][j] = 0;
        }
      }
    }
    
    for (let i = 1; i <= len1; i++) {
      for (let j = 1; j <= len2; j++) {
        if (str1[i - 1] === str2[j - 1]) {
          dp[i][j] = dp[i - 1][j - 1];
        } else {
          dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
        }
      }
    }
    
    return dp[len1][len2];
  };
  /**
  * 複数の文字列をレーベンシュタイン距離で評価した結果をソートし返却する
  * @param {string} searchWord 検索文字列
  * @param {array} wordList 検索対象の一覧
  * @param {number} n 返却する文字列の数
  * @return {array} 評価順のデータ
  */
  const getSuggest = (searchWord, wordList, n) => {
    return wordList
      .sort((a, b) => levenshteinDistance(searchWord, a) - levenshteinDistance(searchWord, b))
      .slice(0, n);
  };
  /**
  * ひらがなをカタカナに変換する
  * @param {string} s ひらがな文字列
  * @return {string} カタカナ文字列
  */
  const hiraganaToKatakana = (s) => {
    return s.normalize('NFKC').replace(/[\u3041-\u3096]/g, function(match) {
      return String.fromCharCode(match.charCodeAt(0) + 0x60);
    });
  };

  return (
    <div>
      <div>
        <label name="pokemonSearch">ポケモン名　</label>
        <input id="pokemonSearch" type="search" value={value} onChange={handleChange} autoComplete="off" />
      </div>
      <div id="wordList">
        <p>もしかして...</p>
        {suggestions.map((word, index) => (
          <span
            key={index}
            className="keyword"
            onClick={() => handleClickWord(word)}
          >
            {word}
          </span>
        ))}
      </div>
    </div>
  );
};

export default App;
```

```scss:App.scss
body {
	padding: 10px;
}
.keyword {
  padding: 2px 5px;
  margin: 5px;
  border-radius: 3px;
  color: #fff;
  background-color: #7368b1;
  word-break: break-all;
  display: inline-block;
  cursor: context-menu;
  user-select: none;
  &:hover {
    background-color: #3920c3;
  }
}
input {
  padding: 5px 10px;
  margin-bottom: 10px;
}
#wordList {
  background-color: rgb(235, 235, 235);
  padding: 5px 10px;
  
  p {
    padding: 3px;
    margin: 0px;
  }
}
```

```pokemonNameList.js
export const pokeNameList = [
    'フシギダネ',
    'フシギソウ',
    'フシギバナ',
    'ヒトカゲ',
    'リザード',
    'リザードン',
    'ゼニガメ',
    'カメール',
    'カメックス',
    'キャタピー',
    
    //
    //　（長いので省略）
    //
    
    'タケルライコ',
    'テツノカシラ',
    'ブリジュラス'
]
```


# プログラム解説

前回の記事で説明した、「レーベンシュタイン距離を測定する関数」と「サジェストリストを作成する機能」は割愛します。

それでは追加した機能です。

### HTML出力部分

下記の個所でhtmlを出力しており、`{word}`の部分を動的に表示するようにしています。

```js
  return (
    <div>
      <div>
        <label name="pokemonSearch">ポケモン名　</label>
        <input id="pokemonSearch" type="search" value={value} onChange={handleChange} autoComplete="off" />
      </div>
      <div id="wordList">
        <p>もしかして...</p>
        {suggestions.map((word, index) => (
          <span
            key={index}
            className="keyword"
            onClick={() => handleClickWord(word)}
          >
            {word}
          </span>
        ))}
      </div>
    </div>
  );
  ```

### ひらがなをカタカナに変換する

入力ボックスがひらがなでの入力なので、内部でカタカナに変換してから文字の類似度を計測するようにしています。

```js
  /**
  * ひらがなをカタカナに変換する
  * @param {string} s ひらがな文字列
  * @return {string} カタカナ文字列
  */
  const hiraganaToKatakana = (s) => {
    return s.normalize('NFKC').replace(/[\u3041-\u3096]/g, function(match) {
      return String.fromCharCode(match.charCodeAt(0) + 0x60);
    });
  };
```

### データ一覧

前回は、初代ポケモン151匹の不完全データをChatGPTに作ってもらいましたが、今回は自分で現在公開されているすべてのポケモンの一覧を作成しました。長くなるのでリンクだけ張っておきます。

https://github.com/ishi720/levenshtein_distance_search/blob/master/src/pokeNameList.ts


# おわりに

今回は、Reactで「文字の類似度」からポケモン名をサジェストするコンポーネントを実装を行いました。

ポケモン以外のデータでも動作するので、様々な用途で使えると思います。

また、何も入力していない状態だと、文字数が少ないものが出てきてしまうため、この辺りを何とかしたいところです。

ここまで、読んで頂きありがとうございました。