Skip to content

Commit b3dc10f

Browse files
chore(devdeps): update pnpm to v10.16.0 (#2793) (#2800)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1 parent 1329c1d commit b3dc10f

File tree

13 files changed

+236
-1
lines changed

13 files changed

+236
-1
lines changed

.changeset/lazy-games-create.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@scaleway/fuzzy-search": major
3+
---
4+
5+
feat: create fuzzy search package

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ scaleway-lib is a set of NPM packages used at Scaleway.
3737
![npm bundle size](https://packagephobia.com/badge?p=@scaleway/cookie-consent)
3838
![npm](https://img.shields.io/npm/v/@scaleway/cookie-consent)
3939

40-
4140
- [`@scaleway/countries`](./packages_deprecated/countries/README.md): ISO 3166/3166-2 coutries JSON database.
4241

4342
![npm](https://img.shields.io/npm/dm/@scaleway/countries)
@@ -104,6 +103,12 @@ scaleway-lib is a set of NPM packages used at Scaleway.
104103
![npm bundle size](https://packagephobia.com/badge?p=@scaleway/regex)
105104
![npm](https://img.shields.io/npm/v/@scaleway/regex)
106105

106+
- [`@scaleway/fuzzy-search`](./packages/fuzzy-search/README.md): fuzzy search utility
107+
108+
![npm](https://img.shields.io/npm/dm/@scaleway/fuzzy-search)
109+
![npm bundle size](https://packagephobia.com/badge?p=@scaleway/fuzzy-search)
110+
![npm](https://img.shields.io/npm/v/@scaleway/fuzzy-search)
111+
107112
- [`@scaleway/jest-helpers`](./packages/jest-helpers/README.md): utilities jest functions.
108113

109114
![npm](https://img.shields.io/npm/dm/@scaleway/jest-helpers)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dist/
2+
coverage/
3+
node_modules
4+
.reports/

packages/fuzzy-search/.npmignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**/__tests__/**
2+
src
3+
!.npmignore

packages/fuzzy-search/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Change Log

packages/fuzzy-search/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# `@scaleway/fuzzy-search`
2+
3+
A fuzzy search utility
4+
5+
---
6+
7+
## Install
8+
9+
```bash
10+
$ pnpm add @scaleway/fuzzy-search
11+
```
12+
13+
## Usage
14+
15+
```js
16+
import { isFuzzyMatch } from "@scaleway/fuzzy-search";
17+
18+
const match = isFuzzyMatch("test", "tests");
19+
```

packages/fuzzy-search/package.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "@scaleway/fuzzy-search",
3+
"version": "0.0.1",
4+
"description": "A small utility to use fuzzy search",
5+
"type": "module",
6+
"engines": {
7+
"node": ">=20.x"
8+
},
9+
"main": "./dist/index.cjs",
10+
"sideEffects": false,
11+
"module": "./dist/index.js",
12+
"types": "./dist/index.d.ts",
13+
"exports": {
14+
".": {
15+
"types": "./dist/index.d.ts",
16+
"require": "./dist/index.cjs",
17+
"default": "./dist/index.js"
18+
}
19+
},
20+
"publishConfig": {
21+
"access": "public"
22+
},
23+
"scripts": {
24+
"prebuild": "shx rm -rf dist",
25+
"typecheck": "tsc --noEmit",
26+
"type:generate": "tsc --declaration -p tsconfig.build.json",
27+
"build": "vite build --config vite.config.ts && pnpm run type:generate",
28+
"build:profile": "npx vite-bundle-visualizer -c vite.config.ts",
29+
"lint": "eslint --report-unused-disable-directives --cache --cache-strategy content --ext ts,tsx .",
30+
"test:unit": "vitest --run --config vite.config.ts",
31+
"test:unit:coverage": "pnpm test:unit --coverage"
32+
},
33+
"repository": {
34+
"type": "git",
35+
"url": "https://github.com/scaleway/scaleway-lib",
36+
"directory": "packages/fuzzy-search"
37+
},
38+
"license": "MIT"
39+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { isFuzzyMatch, levenshteinDistance, normalizeString } from ".."
3+
4+
describe('fuzzySearch', () => {
5+
describe('normalizeString', () => {
6+
test('returns correct string', () => {
7+
expect(normalizeString('île-de-France')).toBe('ile de france')
8+
})
9+
})
10+
11+
describe('levenshteinDistance', () => {
12+
test('returns correct lenvenshtein distance', () => {
13+
expect(levenshteinDistance('test', 'test')).toBe(0)
14+
15+
expect(levenshteinDistance('tests', 'test')).toBe(1)
16+
expect(levenshteinDistance('test', 'tests')).toBe(1)
17+
18+
expect(levenshteinDistance('tset', 'test')).toBe(2)
19+
20+
expect(levenshteinDistance('hello', 'test')).toBe(4)
21+
22+
expect(levenshteinDistance('', 'test')).toBe(4)
23+
expect(levenshteinDistance('test', '0')).toBe(4)
24+
})
25+
})
26+
describe('fuzzySearch', () => {
27+
test('with default distance (1)', () => {
28+
expect(isFuzzyMatch('test', 'test')).toBeTruthy()
29+
expect(isFuzzyMatch('tests', 'test')).toBeFalsy()
30+
expect(isFuzzyMatch('test', 'tests')).toBeTruthy()
31+
expect(isFuzzyMatch('tset', 'test')).toBeFalsy()
32+
expect(isFuzzyMatch('hello', 'test')).toBeFalsy()
33+
expect(isFuzzyMatch('', 'test')).toBeTruthy()
34+
})
35+
36+
test('with distance = 0 (exact match)', () => {
37+
expect(isFuzzyMatch('test', 'test', 0)).toBeTruthy()
38+
expect(isFuzzyMatch('tests', 'test', 0)).toBeFalsy()
39+
expect(isFuzzyMatch('test', 'tests', 0)).toBeTruthy()
40+
expect(isFuzzyMatch('tset', 'test', 0)).toBeFalsy()
41+
expect(isFuzzyMatch('hello', 'test', 0)).toBeFalsy()
42+
expect(isFuzzyMatch('', 'test')).toBeTruthy()
43+
})
44+
45+
test('with distance = 2 (swap tolerant)', () => {
46+
expect(isFuzzyMatch('test', 'test', 2)).toBeTruthy()
47+
expect(isFuzzyMatch('tests', 'test', 2)).toBeFalsy()
48+
expect(isFuzzyMatch('test', 'tests', 2)).toBeTruthy()
49+
expect(isFuzzyMatch('tset', 'test', 2)).toBeTruthy()
50+
expect(isFuzzyMatch('hello', 'test', 2)).toBeFalsy()
51+
expect(isFuzzyMatch('', 'test')).toBeTruthy()
52+
})
53+
})
54+
})

packages/fuzzy-search/src/index.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Remove accent & uppercase
2+
export const normalizeString = (string: string) =>
3+
string
4+
.normalize('NFD')
5+
.replace(/[\u0300-\u036F]/g, '')
6+
.replace(/-/g, ' ')
7+
.toLowerCase()
8+
9+
export const levenshteinDistance = (query: string, slice: string): number => {
10+
if (query.length === 0) {
11+
return slice.length
12+
}
13+
if (slice.length === 0) {
14+
return query.length
15+
}
16+
const distancesArray: number[][] = []
17+
for (let i = 0; i <= slice.length; i += 1) {
18+
distancesArray[i] = [i]
19+
for (let j = 1; j <= query.length; j += 1) {
20+
const prev = distancesArray[i - 1] ?? []
21+
const curr = distancesArray[i] ?? []
22+
curr[j] =
23+
i === 0
24+
? j
25+
: Math.min(
26+
(prev[j] ?? 0) + 1,
27+
(curr[j - 1] ?? 0) + 1,
28+
(prev[j - 1] ?? 0) + (query[j - 1] === slice[i - 1] ? 0 : 1),
29+
)
30+
}
31+
}
32+
33+
return distancesArray[slice.length]?.[query.length] ?? 0
34+
}
35+
36+
/**
37+
* Return `true` if there is a fuzz match in a substring
38+
* By default, allow distance of 1 (which mean, 1 character difference for a match)
39+
* @example isFuzzyMatch("merr", "mercury") = true
40+
* isFuzzyMatch("cry", "mercury") = true
41+
* isFuzzyMatch("mrcury", "mercury") = true
42+
* isFuzzyMatch("mrecury", "mercury") = false
43+
* isFuzzyMatch("mrecury", "mercury", 2) = true
44+
*/
45+
export const isFuzzyMatch = (
46+
query: string,
47+
target: string,
48+
maxDistance = 1,
49+
): boolean => {
50+
const normQuery = normalizeString(query)
51+
const normTarget = normalizeString(target)
52+
53+
if (normQuery.length === 0) {
54+
return true
55+
}
56+
if (normQuery.length > normTarget.length) {
57+
return false
58+
}
59+
60+
for (let i = 0; i <= normTarget.length - normQuery.length; i += 1) {
61+
const slice = normTarget.slice(i, i + normQuery.length)
62+
const dist = levenshteinDistance(normQuery, slice)
63+
if (dist <= maxDistance) {
64+
return true
65+
}
66+
}
67+
68+
return false
69+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"noEmit": false,
5+
"emitDeclarationOnly": true,
6+
"rootDir": "src",
7+
"outDir": "dist"
8+
},
9+
"exclude": [
10+
"*.config.ts",
11+
"*.setup.ts",
12+
"**/__tests__",
13+
"**/__mocks__",
14+
"src/**/*.test.tsx"
15+
]
16+
}

0 commit comments

Comments
 (0)