検索フォームで大文字・小文字や句読点の違いを吸収すると、入力は楽になります。ただし、同じ検索キーになった文字列を、同じレコードとして保存してよいとは限りません。
開発している Krillion Answers の回答チェック処理では、正規化した文字列を検索に使い、元の回答レコードは残しています。ここでは、その実装から検索時の衝突処理を取り出して説明します。日替わりデータ、商品名、用語集などでも使える考え方です。
まず検索する範囲を決める
このアプリで確認したいのは、ある単語が一般的に珍しいかどうかではありません。「特定の日付・問題・保存済みデータに、その回答と得点の記録があるか」です。
そのため、回答を探す前に次を確定します。
- 対象日と問題 ID
- 保存されたスナップショットのバージョン
- その問題の回答プールが完全に読み出せているか
実装では、要求されたバージョンが保存データと違えば stale、読み出した件数が予定件数と違えば unavailable にします。入力文字列だけをキーにして、別の日や別の問題から得点を持ってくる処理にはしません。
正規化は照合用のキーを作る処理
実際に使っている正規化関数です。
function normalizeAnswer(answer) {
return typeof answer === 'string'
? answer.trim().normalize('NFC').toLowerCase()
.replace(/[\p{P}\p{Z}\s]/gu, '')
: '';
}
NFC、大文字・小文字の統一、句読点・区切り文字・空白の除去を行います。これはこの検索用途の方針であって、あらゆる名前に適した正規化ではありません。元の綴りは表示用に保持します。
たとえば、合成済みの é と e\u0301 は同じキーになります。一方、NFC は NFKC ではないので、全角の A と半角の A を同一視しません。
Map に一件だけ入れると、衝突が見えなくなる
Map<正規化キー, 回答> として最後の一件で上書きすると、異なる記録が同じキーになったことを後から検出できません。
検索側では、該当する候補をすべて残し、回答本文だけでなく、保存データに含まれる別名も照合します。以下は実装中の判定部分です。pool.answers は、対象の問題に限定した保存済みデータです。
const normalized = normalizeAnswer(answer);
const matches = pool.answers.filter(record =>
[record.answer, ...(record.aliases || [])]
.some(text => normalizeAnswer(text) === normalized)
);
const scores = new Set(
matches.map(record => `${record.tier}:${record.score}`)
);
if (scores.size > 1) {
return {
status: 'unavailable',
reason: 'conflicting_official_records',
matchCount: matches.length
};
}
候補が複数でも、稀有度と得点が一致していれば直ちに矛盾とはしません。両者が食い違った場合に、勝手に高い方や最後の一件を選ばない設計です。
候補がない場合も区別します。not_found は「保存済みプールに一致する記録がない」という意味です。「公式ゲームが必ず不正解にする」や「得点は 0」とは断定しません。
小さいテストでも、境界を具体化できる
次は正規化の境界を確認するテストです。
import assert from 'node:assert/strict';
assert.equal(normalizeAnswer('é'), normalizeAnswer('e\u0301'));
assert.equal(normalizeAnswer('co-op'), normalizeAnswer('coop'));
assert.notEqual(normalizeAnswer('A'), normalizeAnswer('A'));
さらに、テスト用の co-op と coop に異なる稀有度・得点を付けると、同じ検索キーから二つの記録が見つかり、衝突として扱うべきだと分かります。これらの語は説明用の架空データで、公式の出題や採点例ではありません。
実装では「入力を便利にすること」と「記録を一つに決めること」を分けると、判定の意味を保ちやすくなります。日付、問題、バージョン、元の綴りを残すことで、検索結果を説明する手掛かりも失わずに済みます。
実装例の背景
対象は、開発中の独立した補助サイト Krillion Answers の回答チェック です。保存済みの公式記録と照合して、稀有度と得点を表示します。サイトの利用は無料です。
本記事のコードと説明は、このプロジェクトの現行コードを確認して整理しました。ゲーム本体の公式判定を代替するものではなく、検索対象は保存済みの回答記録です。