├── . prettierrc.json ├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bun.lockb ├── docs └── README-ko.md ├── package.json ├── src ├── korean.spec.ts ├── korean.ts ├── lib.spec.ts └── lib.ts ├── tsconfig.build.json └── tsconfig.json /. prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "bracketSpacing": true, 8 | "printWidth": 120, 9 | "trimTrailingWhitespace": true, 10 | "endOfLine": "lf", 11 | "arrowParens": "always" 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | max_line_length = 120 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [v0.1.6](https://github.com/taggon/kled-js/compare/v0.1.5...v0.1.6) 9 | 10 | ### Commits 11 | 12 | - Fix a typo [`61e9ad7`](https://github.com/taggon/kled-js/commit/61e9ad7a212c5287839025e38babbb51b9c05a55) 13 | 14 | ## [v0.1.5](https://github.com/taggon/kled-js/compare/v0.1.4...v0.1.5) - 2023-11-20 15 | 16 | ### Commits 17 | 18 | - Bump version to 0.1.5 [`8ba6156`](https://github.com/taggon/kled-js/commit/8ba61566cc4809a1d9f1b2915380427597cb77d2) 19 | - Add auto-changelog as a dev dependency [`bf82b24`](https://github.com/taggon/kled-js/commit/bf82b24187f8c37d6769cf2d2dbff6a03ff3ae53) 20 | - Change code format [`e8a9923`](https://github.com/taggon/kled-js/commit/e8a9923075367c05b5679f5da2a39a20dba3cb46) 21 | - Add missing info to package.json [`da98bbb`](https://github.com/taggon/kled-js/commit/da98bbbfeb356e808219394030e9c5d06c53cf8e) 22 | - Fix: 'ㅊ' does not match '춘' [`c1a5177`](https://github.com/taggon/kled-js/commit/c1a5177a2fb781e67a081d67928a348b2fd61289) 23 | 24 | ## [v0.1.4](https://github.com/taggon/kled-js/compare/v0.1.2...v0.1.4) - 2023-11-20 25 | 26 | ### Commits 27 | 28 | - Put korean utilties into a separate file [`d1bfd56`](https://github.com/taggon/kled-js/commit/d1bfd56edcf66cad2e2024b9946f8e43db4264ac) 29 | - Fix: matches('우산', 'ㅇ산') should return zero [`1f59a96`](https://github.com/taggon/kled-js/commit/1f59a968b87a2a5157d2740d4d959ec59cae3cfd) 30 | - Move the project description up [`789ee42`](https://github.com/taggon/kled-js/commit/789ee42104437d1da41e19ec1a5356b9580b9933) 31 | - Bump to v0.1.4 [`c7913b3`](https://github.com/taggon/kled-js/commit/c7913b3c431f00fd94448261cd4d7ef7b46ea5d7) 32 | - Add missing license notice [`5b61523`](https://github.com/taggon/kled-js/commit/5b61523e3f1e5c17d802a3b93427cc6947e6b8db) 33 | - Add missing types field to package.json [`b4d1c53`](https://github.com/taggon/kled-js/commit/b4d1c534b6377d8386c62a7c8215f19b5b383718) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Taegon Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kled.js 2 | 3 | Fuzzy Matching Library with Levenshtein Edit Distance, Tailored for Korean Language Support 4 | 5 | Also available in: [한국어](https://github.com/taggon/kled-js/blob/main/docs/README-ko.md) 6 | 7 | ## APIs 8 | 9 | ### `distance(a: string, b: string, caseSensitive: bool): number` 10 | 11 | Calculate the Levenshtein distance between two strings. 12 | 13 | **Parameters** 14 | 15 | - `a`: a string 16 | - `b`: another string 17 | - `caseSensitive`: optional parameter (default: false), determines whether to consider case sensitivity. 18 | 19 | **Returns** 20 | 21 | The Levenshtein distance between the input strings. 22 | 23 | ### `matches(needle: string, haystack: string, caseSensitive: bool): number` 24 | 25 | Calculate the similarity score between two strings, providing a numerical value between 0 and 1. If the "haystack" does not contain the "needle," the function returns 0. 26 | 27 | It also supports partial Korean letter matching. For example, "ㅇㄴ" and "아녀" matches "안녕" with a slightly lower score than "안녕", which exactly matches the haystack. 28 | 29 | **Parameters** 30 | 31 | - `needle`: a string to search for 32 | - `haystack`: a string to search in 33 | - `caseSensitive`: optional parameter (default: false), determines whether to consider case sensitivity. 34 | 35 | **Returns** 36 | 37 | A similarity score between the input strings, where 0 indicates no similarity, and 1 indicates a perfect match based on the number of matched letters and their positions. 38 | 39 | ## Usage 40 | 41 | 42 | ```ts 43 | import { distance, matches } from 'kled'; 44 | 45 | const levenshteinDistance = distance('hello', 'hola'); 46 | console.log(`Levenshtein Distance: ${levenshteinDistance}`); 47 | 48 | const similarityScore = matches('abc', 'abCde'); 49 | console.log(`Similarity Score: ${similarityScore}`); 50 | ``` 51 | 52 | ## Reporting Issues 53 | 54 | Please report issues [here](https://github.com/taggon/kled-js) if you find any. 55 | 56 | ## License 57 | 58 | MIT 59 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taggon/kled-js/d9592170a00a20f0249ac16001d0f930af8e84bd/bun.lockb -------------------------------------------------------------------------------- /docs/README-ko.md: -------------------------------------------------------------------------------- 1 | # kled.js 2 | 3 | KLED: 한국어를 지원하는 레벤슈타인 편집 거리 알고리즘 기반 퍼지 매칭 라이브러리 4 | 5 | ## APIs 6 | 7 | ### `distance(a: string, b: string, caseSensitive: bool): number` 8 | 9 | 두 문자열 간의 레벤슈타인 거리를 계산합니다. 10 | 11 | **파라미터** 12 | 13 | - `a`: 문자열 14 | - `b`: 문자열 15 | - `caseSensitive`: (선택사항) 대소문자를 구분할 때는 true, 그렇지 않으면 false. 기본값: false 16 | 17 | **반환값** 18 | 19 | 두 문자열 간의 레벤슈타인 거리를 의미하는 숫자. 20 | 21 | ### `matches(needle: string, haystack: string, caseSensitive: bool): number` 22 | 23 | 두 문자열의 간의 유사도를 0부터 1까지의 숫자로 반환합니다. 대상 문자열(haystack)이 검색어(needle)를 포함하지 않으면 0을 반환합니다. 24 | 25 | 한국어 부분 매칭도 지원합니다. 예를 들어 "ㅇㄴ"이나 "아녀"는 "안녕"에 매칭되지만 완벽하게 일치하는 "안녕"보다는 유사도 점수가 낮습니다. 26 | 27 | **파라미터** 28 | 29 | - `needle`: 검색할 문자열 30 | - `haystack`: 검색의 대상이 되는 문자열 31 | - `caseSensitive`: (선택사항) 대소문자를 구분할 때는 true, 그렇지 않으면 false. 기본값: false 32 | 33 | **반환값** 34 | 35 | 0부터 1까지의 숫자. 0은 전혀 매칭되지 않는다는 의미이고 1은 두 문자열이 완전하게 동일하다는 의미입니다. 36 | 37 | ## Usage 38 | 39 | 40 | ```ts 41 | import { distance, matches } from 'kled'; 42 | 43 | const levenshteinDistance = distance('hello', 'hola'); 44 | console.log(`Levenshtein Distance: ${levenshteinDistance}`); 45 | 46 | const similarityScore = matches('abc', 'abCde'); 47 | console.log(`Similarity Score: ${similarityScore}`); 48 | ``` 49 | 50 | ## 버그 신고 51 | 52 | 문제를 발견하면 [저장소](https://github.com/taggon/kled-js)로 보고해주세요. 53 | 54 | ## 라이선스 55 | 56 | MIT 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kled", 3 | "version": "0.1.6", 4 | "type": "module", 5 | "module": "dist/lib.js", 6 | "types": "dist/lib.d.ts", 7 | "keywords": [ 8 | "fuzzy", 9 | "string", 10 | "search", 11 | "levenshtein", 12 | "distance", 13 | "similarity", 14 | "korean" 15 | ], 16 | "author": { 17 | "name": "Taegon Kim", 18 | "url": "https://taegon.kim", 19 | "email": "gonom9@gmail.com" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/taggon/kled-js.git" 24 | }, 25 | "license": "MIT", 26 | "scripts": { 27 | "build": "tsc -p tsconfig.build.json", 28 | "test": "bun test", 29 | "coverage": "bun test --coverage", 30 | "prepare": "bun run build", 31 | "version": "auto-changelog -p --starting-version v0.1.4 && git add CHANGELOG.md" 32 | }, 33 | "files": [ 34 | "dist", 35 | "docs" 36 | ], 37 | "devDependencies": { 38 | "auto-changelog": "^2.4.0", 39 | "bun-types": "latest", 40 | "typescript": "^5.2.2" 41 | }, 42 | "auto-changelog": { 43 | "output": "CHANGELOG.md", 44 | "template": "keepachangelog", 45 | "unreleased": false, 46 | "commitLimit": false, 47 | "backfillLimit": false, 48 | "hideCredit": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/korean.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'bun:test'; 2 | import { isSimilar } from './korean'; 3 | 4 | describe('isSimilar()', () => { 5 | it('returns true when both are the same', () => { 6 | expect(isSimilar('강', '강')).toBe(true); 7 | expect(isSimilar('나', '나')).toBe(true); 8 | expect(isSimilar('ㄷ', 'ㄷ')).toBe(true); 9 | }); 10 | 11 | it('returns true when one is a partial letter of the other one', () => { 12 | expect(isSimilar('ㄱ', '강')).toBe(true); 13 | expect(isSimilar('ㄲ', '강')).toBe(false); 14 | expect(isSimilar('가', '강')).toBe(true); 15 | expect(isSimilar('거', '강')).toBe(false); 16 | 17 | expect(isSimilar('ㄴ', '날')).toBe(true); 18 | expect(isSimilar('ㄷ', '날')).toBe(false); 19 | expect(isSimilar('나', '날')).toBe(true); 20 | expect(isSimilar('눌', '날')).toBe(false); 21 | 22 | expect(isSimilar('ㅊ', '춘')).toBe(true); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/korean.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Korean letter utilities. 3 | * @file korean.ts 4 | */ 5 | 6 | const CONSONANT_SYLLABLES = 'ㄱ가ㄲ까ㄴ나ㄷ다ㄸ따ㄹ라ㅁ마ㅂ바ㅃ빠ㅅ사ㅆ싸ㅇ아ㅈ자ㅉ짜ㅊ차ㅋ카ㅌ타ㅍ파ㅎ하'; 7 | 8 | /** 9 | * Check whether a letter is a vowel. 10 | */ 11 | export function isConsonant(c: string): boolean { 12 | return 'ㄱ' <= c && c <= 'ㅎ'; 13 | } 14 | 15 | /** 16 | * Check whether a letter is a syllable. 17 | */ 18 | export function isSyllable(c: string): boolean { 19 | return '가' <= c && c <= '힣'; 20 | } 21 | 22 | /** 23 | * Check whether a letter is a Korean letter. 24 | */ 25 | export function isKorean(c: string): boolean { 26 | return isConsonant(c) || isSyllable(c); 27 | } 28 | 29 | /** 30 | * Check whether two Korean letters are similar. 31 | */ 32 | export function isSimilar(a: string, b: string): boolean { 33 | if (a === b) return true; 34 | if (isConsonant(a) || isConsonant(b)) { 35 | return getConsonant(a) === getConsonant(b); 36 | } 37 | 38 | return omitFinal(a) === omitFinal(b); 39 | } 40 | 41 | /** 42 | * Check whether a Korean letter has a final consonant. 43 | * @param c 44 | */ 45 | export function hasFinal(c: string): boolean { 46 | if (!isSyllable(c)) return false; 47 | return (c.charCodeAt(0) - 0xac00) % 28 !== 0; 48 | } 49 | 50 | function omitFinal(c: string): string { 51 | if (!hasFinal(c)) return c; 52 | return String.fromCharCode((((c.charCodeAt(0) - 0xac00) / 28)|0) * 28 + 0xac00); 53 | } 54 | 55 | /** 56 | * Get the first consonant of a Korean letter 57 | */ 58 | function getConsonant(c: string): string { 59 | if (!isSyllable(c)) return c; 60 | 61 | const code = c.charCodeAt(0) - 0xac00; 62 | const withoutFinal = ((code / 588)|0) * 588 + 0xac00; 63 | const firstLetterOfTheConsonant = String.fromCharCode(withoutFinal); 64 | const index = CONSONANT_SYLLABLES.indexOf(firstLetterOfTheConsonant); 65 | 66 | return (index === -1) ? '' : CONSONANT_SYLLABLES[index - 1]; 67 | } 68 | -------------------------------------------------------------------------------- /src/lib.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from 'bun:test'; 2 | import { distance, matches } from './lib'; 3 | 4 | describe('distance()', () => { 5 | it('returns the other string length when one string is empty', () => { 6 | expect(distance('', 'foo')).toBe(3); 7 | expect(distance('hello', '')).toBe(5); 8 | expect(distance('', '한글')).toBe(2); 9 | }); 10 | 11 | it('adds 0.1 when every korean character partially matches', () => { 12 | expect(distance('A학급', 'B학급')).toBe(1); 13 | expect(distance('Aㅎㄱ', 'B학급')).toBe(1.02); 14 | }); 15 | }); 16 | 17 | describe('matches()', () => { 18 | it('returns 1 when needle is same with haystack', () => { 19 | expect(matches('foo', 'foo')).toBe(1); 20 | 21 | // Case-insensitive by default 22 | expect(matches('foo', 'FOo')).toBe(1); 23 | 24 | // Case-sensitive when specified 25 | expect(matches('foo', 'FOo', true)).toBe(0); 26 | 27 | expect(matches('홍길동', '홍길동')).toBe(1); 28 | }); 29 | 30 | it('returns when needle is not in heystock', () => { 31 | expect(matches('foo', 'bar')).toBe(0); 32 | 33 | // Case-sensitive 34 | expect(matches('foo', 'FOO', true)).toBe(0); 35 | 36 | // Korean 37 | expect(matches('홍길동', '김철수')).toBe(0); 38 | expect(matches('하길동', '홍길동')).toBe(0); 39 | expect(matches('홍가동', '홍길동')).toBe(0); 40 | expect(matches('홍가두', '홍길동')).toBe(0); 41 | }); 42 | 43 | it('throws an error when needle is longer than haystack', () => { 44 | expect(() => matches('greater', 'great')).toThrow(); 45 | }); 46 | 47 | it('returns zero when haystack does not contain needle', () => { 48 | expect(matches('dog', 'digging')).toBe(0); 49 | expect(matches('고성', '군산')).toBe(0); 50 | expect(matches('우산', 'ㅇ산')).toBe(0); 51 | }); 52 | 53 | it('returns a number that shows how much similar two strings are', () => { 54 | expect(matches('brb', 'Be Right Back')).toBeGreaterThan(0); 55 | expect(matches('brb', 'bring back')).toBeGreaterThan(0); 56 | expect(matches('brb', 'bring back')).toBeGreaterThan(matches('brb', 'Be Right Back')); 57 | expect(matches('강성', '서울시 강남구 삼성동')).toBeGreaterThan(0); 58 | expect(matches('강남', '서울시 강남구 삼성동')).toBeGreaterThan(0); 59 | expect(matches('ㄱㄴ', '서울시 강남구 삼성동')).toBeGreaterThan(0); 60 | expect(matches('강남', '서울시 강남구 삼성동')).toBeGreaterThan(matches('ㄱㄴ', '서울시 강남구 삼성동')); 61 | }); 62 | 63 | it('returns non-zero value if korean characters partially match', () => { 64 | expect(matches('ㅎㄱ', '한글')).toBeGreaterThan(0); 65 | expect(matches('하그', '한글')).toBeGreaterThan(0); 66 | expect(matches('하ㄱ', '한글')).toBeGreaterThan(0); 67 | expect(matches('하그', '한글')).toBe(matches('ㅎㄱ', '한글')); 68 | expect(matches('하그', '한글')).toBe(matches('하ㄱ', '한글')); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | import * as korean from './korean'; 2 | 3 | /** 4 | * Get a Levenshtein distance between two strings 5 | * 6 | * @param a a string 7 | * @param b another string 8 | * @param caseSensitive whether to consider case sensitivity (default: false) 9 | */ 10 | export function distance(a: string, b: string, caseSensitive = false): number { 11 | if (a.length == 0) return b.length; 12 | if (b.length == 0) return a.length; 13 | 14 | if (!caseSensitive) { 15 | a = a.toLowerCase(); 16 | b = b.toLowerCase(); 17 | } 18 | if (a === b) return 0; 19 | 20 | const table: number[][] = []; 21 | 22 | table[0] = Array(a.length + 1) 23 | .fill(0) 24 | .map((_, n) => n); 25 | 26 | for (let i = 1; i <= b.length; i++) { 27 | table[i] = [i]; 28 | } 29 | 30 | for (let i = 1; i <= b.length; i++) { 31 | for (let j = 1; j <= a.length; j++) { 32 | const bChar = b[i - 1]; 33 | const aChar = a[j - 1]; 34 | let korSimilarity = 0; // Similarity between Korean characters 35 | 36 | if (bChar !== aChar && korean.isKorean(aChar) && korean.isKorean(bChar)) { 37 | if (korean.isSimilar(aChar, bChar)) { 38 | korSimilarity = 0.01; 39 | } 40 | } 41 | 42 | if (korSimilarity > 0 || bChar === aChar) { 43 | table[i][j] = table[i - 1][j - 1] + korSimilarity; 44 | } else { 45 | table[i][j] = Math.min(table[i - 1][j - 1] + 1, table[i][j - 1] + 1, table[i - 1][j] + 1); 46 | } 47 | } 48 | } 49 | 50 | return table[b.length][a.length]; 51 | } 52 | 53 | /** 54 | * Calculate the similarity score between two strings, providing a numerical value between 0 and 1. 55 | * If the "haystack" does not contain the "needle," the function returns 0. 56 | * @param needle a string to search for 57 | * @param haystack a string to search in 58 | * @param caseSensitive whether to consider case sensitivity (default: false) 59 | */ 60 | export function matches(needle: string, haystack: string, caseSensitive = false): number { 61 | if (needle.length > haystack.length) { 62 | throw new Error('haystack cannot be shorter than needle.'); 63 | } 64 | 65 | if (!caseSensitive) { 66 | needle = needle.toLowerCase(); 67 | haystack = haystack.toLowerCase(); 68 | } 69 | 70 | const _needle = needle.split(''); 71 | let _haystack = haystack.split(''); 72 | 73 | while (_needle.length > 0) { 74 | const ch = _needle[0]; 75 | let idx = 0; 76 | 77 | if (!korean.isKorean(ch) || korean.hasFinal(ch)) { 78 | idx = _haystack.indexOf(ch); 79 | } else { 80 | idx = _haystack.findIndex((c) => korean.isSimilar(c, ch) && c >= ch); 81 | } 82 | 83 | if (idx === -1) { 84 | return 0; 85 | } 86 | 87 | _needle.shift(); 88 | _haystack = _haystack.slice(idx + 1); 89 | } 90 | 91 | return (haystack.length - distance(needle, haystack, caseSensitive)) / haystack.length; 92 | } 93 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "outDir": "./dist", 6 | "declarationDir": "./dist", 7 | "declaration": true, 8 | "sourceMap": true 9 | }, 10 | "exclude": ["src/**/*.spec.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true, 10 | "types": ["bun-types"] 11 | }, 12 | "include": ["src/**/*.ts"], 13 | "exclude": ["node_modules"] 14 | } 15 | --------------------------------------------------------------------------------