UNPKG

closewords

Version:

A library for finding the most similar word from a list of words, supporting Japanese (including kanji). / 最も似た単語を単語群から検索する日本語(漢字含む)対応のライブラリ

105 lines (104 loc) 4.59 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const require_isAlphabetOnly = require("./isAlphabetOnly-C3VUVQyc.cjs"); let jaro_winkler = require("jaro-winkler"); jaro_winkler = require_isAlphabetOnly.__toESM(jaro_winkler); let fast_levenshtein = require("fast-levenshtein"); fast_levenshtein = require_isAlphabetOnly.__toESM(fast_levenshtein); let worker_threads = require("worker_threads"); let path = require("path"); path = require_isAlphabetOnly.__toESM(path); //#region src/index.ts let workerInstance = null; let requestId = 0; const pending = /* @__PURE__ */ new Map(); function getWorker() { if (workerInstance) return workerInstance; const isTs = __filename?.endsWith(".ts") ?? false; const workerExt = isTs ? ".ts" : __filename?.endsWith(".mjs") ? ".mjs" : ".cjs"; const w = new worker_threads.Worker(path.default.resolve(__dirname, `./worker${workerExt}`), { execArgv: isTs ? ["--require", "tsx/cjs"] : [] }); w.on("message", (msg) => { const p = pending.get(msg.id); if (!p) return; pending.delete(msg.id); if (msg.error) p.reject(new Error(msg.error)); else p.resolve(msg.results); }); w.on("error", (err) => { for (const p of pending.values()) p.reject(err); pending.clear(); workerInstance = null; }); w.on("exit", () => { for (const p of pending.values()) p.reject(/* @__PURE__ */ new Error("Worker exited unexpectedly")); pending.clear(); workerInstance = null; }); w.unref(); workerInstance = w; return w; } function convertToRomaji(words, dicPath) { const worker = getWorker(); const id = ++requestId; return new Promise((resolve, reject) => { pending.set(id, { resolve, reject }); worker.postMessage({ id, words, dicPath }); }); } async function closeWords(word, candidates, raw = false) { if (typeof word !== "string" && (typeof word !== "object" || !word.word)) throw new Error("word must be a string or an object with 'word'."); if (typeof word === "object" && word.pronounce && !require_isAlphabetOnly.isAlphabetOnly(word.pronounce)) throw new Error("word.pronounce must be an alphabetic string."); if (!Array.isArray(candidates) || !candidates.every((item) => typeof item === "string" || typeof item === "object" && item.word)) throw new Error("Candidates must be an array of strings or objects with 'word'."); if (!candidates.filter((c) => typeof c === "object" && !!c.pronounce).every((item) => require_isAlphabetOnly.isAlphabetOnly(item.pronounce))) throw new Error("pronounces within candidates must be alphabetic strings."); if (typeof raw !== "boolean") throw new Error("raw must be boolean."); const dicPath = path.default.resolve(__dirname, "./dict"); const romajiWords = await convertToRomaji([word, ...candidates], dicPath); const romajiWord = romajiWords[0]; const romajiCandidates = romajiWords.slice(1); const searchWord = typeof word === "string" ? word : word.word; const baseLength = searchWord.length; const searchChars = Array.from(searchWord); const scores = candidates.map((candidate, index) => { const candidateWord = typeof candidate === "string" ? candidate : candidate.word; if (searchWord === candidateWord) return { word: candidateWord, score: 1 }; const candidateLength = candidateWord.length; const maxLen = Math.max(baseLength, candidateLength); const romajiScore = (0, jaro_winkler.default)(romajiWord, romajiCandidates[index]); const stringScore = 1 - fast_levenshtein.get(searchWord, candidateWord) / maxLen; const candidateChars = Array.from(candidateWord); let prefixMatch = 0; const minLen = Math.min(searchChars.length, candidateChars.length); for (let i = 0; i < minLen; i++) if (searchChars[i] === candidateChars[i]) prefixMatch++; else break; const prefixRatio = prefixMatch / maxLen; let charMatchCount = 0; for (const ch of searchChars) if (candidateWord.includes(ch)) charMatchCount++; const charRatio = charMatchCount / maxLen; const exactCharBonus = charRatio * .4; const lengthPenalty = Math.max(.7, 1 - Math.abs(baseLength - candidateLength) / baseLength); const prefixBonus = prefixRatio > 0 ? prefixRatio * .05 : 0; const combinedScore = (romajiScore * .7 + stringScore * .2 + charRatio * .1) * lengthPenalty + exactCharBonus + prefixBonus; return { word: candidateWord, score: Math.min(combinedScore, 1) }; }); scores.sort((a, b) => b.score - a.score); if (!raw) { const maxScore = scores[0]?.score; return scores.filter((item) => item.score === maxScore).map((item) => item.word); } return scores; } //#endregion exports.closeWords = closeWords;