fuzzgpu 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/Cargo.lock +2 -2
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/PKG-INFO +3 -3
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/README.md +2 -2
- fuzzgpu-0.1.2/assets/banner.svg +66 -0
- fuzzgpu-0.1.2/assets/github-stats.svg +51 -0
- fuzzgpu-0.1.2/assets/top-languages.svg +62 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/Cargo.toml +1 -1
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/damerau.rs +87 -13
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/fuzz.rs +52 -49
- fuzzgpu-0.1.2/crates/fuzzgpu-core/src/gpu.rs +151 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/jaro.rs +140 -54
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/levenshtein.rs +81 -41
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/needleman.rs +48 -30
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/shaders/jaro.wgsl +13 -11
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/shaders/jaro_matrix.wgsl +13 -10
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-python/Cargo.toml +2 -2
- fuzzgpu-0.1.2/crates/fuzzgpu-python/src/lib.rs +442 -0
- fuzzgpu-0.1.2/crates/fuzzgpu-wasm/Cargo.toml +19 -0
- fuzzgpu-0.1.2/crates/fuzzgpu-wasm/src/lib.rs +92 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/pyproject.toml +2 -2
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/python/fuzzgpu/__init__.py +28 -15
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/python/fuzzgpu/fuzz.py +13 -2
- fuzzgpu-0.1.2/tests/test_api_signatures.py +171 -0
- fuzzgpu-0.1.2/tests/test_basic.py +323 -0
- fuzzgpu-0.1.2/tests/test_concurrency.py +62 -0
- fuzzgpu-0.1.2/tests/test_edge_cases.py +338 -0
- fuzzgpu-0.1.2/tests/test_invariants.py +169 -0
- fuzzgpu-0.1.2/tests/test_stress.py +123 -0
- fuzzgpu-0.1.0/crates/fuzzgpu-core/src/gpu.rs +0 -96
- fuzzgpu-0.1.0/crates/fuzzgpu-python/src/lib.rs +0 -273
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/Cargo.toml +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/LICENSE +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/assets/logo.png +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/assets/logo.svg +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/lib.rs +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/shaders/levenshtein.wgsl +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/shaders/levenshtein_matrix.wgsl +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/simd.rs +0 -0
|
@@ -248,7 +248,7 @@ dependencies = [
|
|
|
248
248
|
|
|
249
249
|
[[package]]
|
|
250
250
|
name = "fuzzgpu-core"
|
|
251
|
-
version = "0.1.
|
|
251
|
+
version = "0.1.2"
|
|
252
252
|
dependencies = [
|
|
253
253
|
"bytemuck",
|
|
254
254
|
"log",
|
|
@@ -260,7 +260,7 @@ dependencies = [
|
|
|
260
260
|
|
|
261
261
|
[[package]]
|
|
262
262
|
name = "fuzzgpu-python"
|
|
263
|
-
version = "0.1.
|
|
263
|
+
version = "0.1.2"
|
|
264
264
|
dependencies = [
|
|
265
265
|
"fuzzgpu-core",
|
|
266
266
|
"pollster",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fuzzgpu
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -35,7 +35,7 @@ Project-URL: Repository, https://github.com/kuntal-devrat/fuzzgpu
|
|
|
35
35
|
|
|
36
36
|
*Cross-platform GPU compute via WebGPU (`wgpu`) & Multi-Core CPU parallelism with Rayon. Zero CUDA dependencies.*
|
|
37
37
|
|
|
38
|
-
[](https://pypi.org/project/fuzzgpu/)
|
|
39
39
|
[](https://opensource.org/licenses/MIT)
|
|
40
40
|
[](https://www.rust-lang.org)
|
|
41
41
|
[](https://github.com/kuntal-devrat/fuzzgpu)
|
|
@@ -100,7 +100,7 @@ pip install fuzzgpu
|
|
|
100
100
|
### Rust (Cargo.toml)
|
|
101
101
|
```toml
|
|
102
102
|
[dependencies]
|
|
103
|
-
fuzzgpu-core = "0.1.
|
|
103
|
+
fuzzgpu-core = "0.1.2"
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
---
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
*Cross-platform GPU compute via WebGPU (`wgpu`) & Multi-Core CPU parallelism with Rayon. Zero CUDA dependencies.*
|
|
10
10
|
|
|
11
|
-
[](https://pypi.org/project/fuzzgpu/)
|
|
12
12
|
[](https://opensource.org/licenses/MIT)
|
|
13
13
|
[](https://www.rust-lang.org)
|
|
14
14
|
[](https://github.com/kuntal-devrat/fuzzgpu)
|
|
@@ -73,7 +73,7 @@ pip install fuzzgpu
|
|
|
73
73
|
### Rust (Cargo.toml)
|
|
74
74
|
```toml
|
|
75
75
|
[dependencies]
|
|
76
|
-
fuzzgpu-core = "0.1.
|
|
76
|
+
fuzzgpu-core = "0.1.2"
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
---
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<svg width="850" height="200" viewBox="0 0 850 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg_grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#0B0F19" />
|
|
5
|
+
<stop offset="50%" stop-color="#111827" />
|
|
6
|
+
<stop offset="100%" stop-color="#0D1117" />
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<linearGradient id="accent_grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
9
|
+
<stop offset="0%" stop-color="#38BDF8" />
|
|
10
|
+
<stop offset="50%" stop-color="#818CF8" />
|
|
11
|
+
<stop offset="100%" stop-color="#C084FC" />
|
|
12
|
+
</linearGradient>
|
|
13
|
+
<linearGradient id="wave1" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
14
|
+
<stop offset="0%" stop-color="#007ec6" stop-opacity="0.3" />
|
|
15
|
+
<stop offset="50%" stop-color="#8B5CF6" stop-opacity="0.4" />
|
|
16
|
+
<stop offset="100%" stop-color="#EC4899" stop-opacity="0.2" />
|
|
17
|
+
</linearGradient>
|
|
18
|
+
<linearGradient id="wave2" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
19
|
+
<stop offset="0%" stop-color="#38BDF8" stop-opacity="0.15" />
|
|
20
|
+
<stop offset="100%" stop-color="#818CF8" stop-opacity="0.2" />
|
|
21
|
+
</linearGradient>
|
|
22
|
+
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
23
|
+
<feGaussianBlur stdDeviation="6" result="blur" />
|
|
24
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
|
25
|
+
</filter>
|
|
26
|
+
</defs>
|
|
27
|
+
|
|
28
|
+
<!-- Background Card -->
|
|
29
|
+
<rect width="850" height="200" rx="16" fill="url(#bg_grad)" stroke="#30363D" stroke-width="1.5" />
|
|
30
|
+
|
|
31
|
+
<!-- Ambient Wave Layer -->
|
|
32
|
+
<path d="M0 130 C 200 90, 350 170, 550 120 C 700 80, 780 140, 850 110 L 850 200 L 0 200 Z" fill="url(#wave1)" />
|
|
33
|
+
<path d="M0 150 C 180 120, 380 180, 580 140 C 720 110, 800 160, 850 140 L 850 200 L 0 200 Z" fill="url(#wave2)" />
|
|
34
|
+
|
|
35
|
+
<!-- Neon Accent Line Top -->
|
|
36
|
+
<rect x="0" y="0" width="850" height="3" rx="1.5" fill="url(#accent_grad)" />
|
|
37
|
+
|
|
38
|
+
<!-- Title Text -->
|
|
39
|
+
<text x="425" y="78" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Inter', sans-serif" font-size="38" font-weight="800" fill="#F0F6FC" letter-spacing="1">
|
|
40
|
+
Devrat Kuntal
|
|
41
|
+
</text>
|
|
42
|
+
|
|
43
|
+
<!-- Subtitle / Tagline -->
|
|
44
|
+
<text x="425" y="112" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Inter', sans-serif" font-size="15" font-weight="600" fill="#38BDF8" letter-spacing="0.5">
|
|
45
|
+
⚡ AI ARCHITECT | HIGH-PERFORMANCE SYSTEMS & GPU COMPUTE ENGINEER
|
|
46
|
+
</text>
|
|
47
|
+
|
|
48
|
+
<!-- Tech tags pill badges -->
|
|
49
|
+
<g transform="translate(195, 140)">
|
|
50
|
+
<!-- Pill 1: WebGPU / WGSL -->
|
|
51
|
+
<rect x="0" y="0" width="105" height="24" rx="12" fill="#1F2937" stroke="#38BDF8" stroke-width="1" />
|
|
52
|
+
<text x="52.5" y="16" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="11" font-weight="600" fill="#E0F2FE">⚡ WebGPU/WGSL</text>
|
|
53
|
+
|
|
54
|
+
<!-- Pill 2: Rust SIMD -->
|
|
55
|
+
<rect x="115" y="0" width="95" height="24" rx="12" fill="#1F2937" stroke="#DEA584" stroke-width="1" />
|
|
56
|
+
<text x="162.5" y="16" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="11" font-weight="600" fill="#FFEDD5">🦀 Rust SIMD</text>
|
|
57
|
+
|
|
58
|
+
<!-- Pill 3: PyO3 ABI3 -->
|
|
59
|
+
<rect x="220" y="0" width="95" height="24" rx="12" fill="#1F2937" stroke="#818CF8" stroke-width="1" />
|
|
60
|
+
<text x="267.5" y="16" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="11" font-weight="600" fill="#E0E7FF">🐍 PyO3 ABI3</text>
|
|
61
|
+
|
|
62
|
+
<!-- Pill 4: AI Agent Runtimes -->
|
|
63
|
+
<rect x="325" y="0" width="135" height="24" rx="12" fill="#1F2937" stroke="#EC4899" stroke-width="1" />
|
|
64
|
+
<text x="392.5" y="16" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="11" font-weight="600" fill="#FCE7F3">🤖 AI Agent Runtimes</text>
|
|
65
|
+
</g>
|
|
66
|
+
</svg>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<svg width="415" height="195" viewBox="0 0 415 195" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="card_bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#0D1117" />
|
|
5
|
+
<stop offset="100%" stop-color="#161B22" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="icon_grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
8
|
+
<stop offset="0%" stop-color="#38BDF8" />
|
|
9
|
+
<stop offset="100%" stop-color="#8B5CF6" />
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
|
|
13
|
+
<rect width="415" height="195" rx="12" fill="url(#card_bg)" stroke="#30363D" stroke-width="1" />
|
|
14
|
+
|
|
15
|
+
<!-- Header -->
|
|
16
|
+
<text x="25" y="35" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif" font-size="18" font-weight="700" fill="#38BDF8">
|
|
17
|
+
GitHub Analytics
|
|
18
|
+
</text>
|
|
19
|
+
|
|
20
|
+
<!-- Metric 1: Total Stars -->
|
|
21
|
+
<g transform="translate(25, 60)">
|
|
22
|
+
<circle cx="10" cy="10" r="10" fill="#1F2937" />
|
|
23
|
+
<text x="10" y="14" text-anchor="middle" font-size="11">⭐</text>
|
|
24
|
+
<text x="30" y="15" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="500" fill="#94A3B8">Total Stars Earned:</text>
|
|
25
|
+
<text x="365" y="15" text-anchor="end" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="14" font-weight="700" fill="#F0F6FC">4</text>
|
|
26
|
+
</g>
|
|
27
|
+
|
|
28
|
+
<!-- Metric 2: Total Commits (2025/2026) -->
|
|
29
|
+
<g transform="translate(25, 90)">
|
|
30
|
+
<circle cx="10" cy="10" r="10" fill="#1F2937" />
|
|
31
|
+
<text x="10" y="14" text-anchor="middle" font-size="11">⚡</text>
|
|
32
|
+
<text x="30" y="15" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="500" fill="#94A3B8">Total Commits (Recent):</text>
|
|
33
|
+
<text x="365" y="15" text-anchor="end" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="14" font-weight="700" fill="#F0F6FC">335+</text>
|
|
34
|
+
</g>
|
|
35
|
+
|
|
36
|
+
<!-- Metric 3: Total PRs -->
|
|
37
|
+
<g transform="translate(25, 120)">
|
|
38
|
+
<circle cx="10" cy="10" r="10" fill="#1F2937" />
|
|
39
|
+
<text x="10" y="14" text-anchor="middle" font-size="11">🔀</text>
|
|
40
|
+
<text x="30" y="15" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="500" fill="#94A3B8">Pull Requests:</text>
|
|
41
|
+
<text x="365" y="15" text-anchor="end" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="14" font-weight="700" fill="#F0F6FC">12</text>
|
|
42
|
+
</g>
|
|
43
|
+
|
|
44
|
+
<!-- Metric 4: Total Repositories -->
|
|
45
|
+
<g transform="translate(25, 150)">
|
|
46
|
+
<circle cx="10" cy="10" r="10" fill="#1F2937" />
|
|
47
|
+
<text x="10" y="14" text-anchor="middle" font-size="11">📦</text>
|
|
48
|
+
<text x="30" y="15" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="500" fill="#94A3B8">Active Repositories:</text>
|
|
49
|
+
<text x="365" y="15" text-anchor="end" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="14" font-weight="700" fill="#F0F6FC">26</text>
|
|
50
|
+
</g>
|
|
51
|
+
</svg>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<svg width="415" height="195" viewBox="0 0 415 195" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="lang_bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#0D1117" />
|
|
5
|
+
<stop offset="100%" stop-color="#161B22" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<clipPath id="bar_clip">
|
|
8
|
+
<rect x="25" y="55" width="365" height="9" rx="4.5" />
|
|
9
|
+
</clipPath>
|
|
10
|
+
</defs>
|
|
11
|
+
|
|
12
|
+
<rect width="415" height="195" rx="12" fill="url(#lang_bg)" stroke="#30363D" stroke-width="1" />
|
|
13
|
+
|
|
14
|
+
<!-- Header -->
|
|
15
|
+
<text x="25" y="35" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif" font-size="18" font-weight="700" fill="#38BDF8">
|
|
16
|
+
Most Used Languages
|
|
17
|
+
</text>
|
|
18
|
+
|
|
19
|
+
<!-- Stacked Bar -->
|
|
20
|
+
<g clip-path="url(#bar_clip)">
|
|
21
|
+
<rect x="25.0" y="55" width="169.3" height="9" fill="#3178c6" />
|
|
22
|
+
<rect x="194.3" y="55" width="75.4" height="9" fill="#f1e05a" />
|
|
23
|
+
<rect x="269.7" y="55" width="37.0" height="9" fill="#3572A5" />
|
|
24
|
+
<rect x="306.7" y="55" width="28.1" height="9" fill="#dea584" />
|
|
25
|
+
<rect x="334.8" y="55" width="25.4" height="9" fill="#663399" />
|
|
26
|
+
<rect x="360.2" y="55" width="7.9" height="9" fill="#A97BFF" />
|
|
27
|
+
|
|
28
|
+
</g>
|
|
29
|
+
|
|
30
|
+
<!-- Language List -->
|
|
31
|
+
|
|
32
|
+
<g transform="translate(25, 90)">
|
|
33
|
+
<circle cx="6" cy="6" r="5" fill="#3178c6" />
|
|
34
|
+
<text x="20" y="10" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="500" fill="#94A3B8">TypeScript</text>
|
|
35
|
+
<text x="160" y="10" text-anchor="end" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="600" fill="#F0F6FC">46.4%</text>
|
|
36
|
+
</g>
|
|
37
|
+
<g transform="translate(25, 118)">
|
|
38
|
+
<circle cx="6" cy="6" r="5" fill="#f1e05a" />
|
|
39
|
+
<text x="20" y="10" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="500" fill="#94A3B8">JavaScript</text>
|
|
40
|
+
<text x="160" y="10" text-anchor="end" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="600" fill="#F0F6FC">20.7%</text>
|
|
41
|
+
</g>
|
|
42
|
+
<g transform="translate(25, 146)">
|
|
43
|
+
<circle cx="6" cy="6" r="5" fill="#3572A5" />
|
|
44
|
+
<text x="20" y="10" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="500" fill="#94A3B8">Python</text>
|
|
45
|
+
<text x="160" y="10" text-anchor="end" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="600" fill="#F0F6FC">10.1%</text>
|
|
46
|
+
</g>
|
|
47
|
+
<g transform="translate(215, 90)">
|
|
48
|
+
<circle cx="6" cy="6" r="5" fill="#dea584" />
|
|
49
|
+
<text x="20" y="10" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="500" fill="#94A3B8">Rust</text>
|
|
50
|
+
<text x="160" y="10" text-anchor="end" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="600" fill="#F0F6FC">7.7%</text>
|
|
51
|
+
</g>
|
|
52
|
+
<g transform="translate(215, 118)">
|
|
53
|
+
<circle cx="6" cy="6" r="5" fill="#663399" />
|
|
54
|
+
<text x="20" y="10" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="500" fill="#94A3B8">CSS</text>
|
|
55
|
+
<text x="160" y="10" text-anchor="end" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="600" fill="#F0F6FC">7.0%</text>
|
|
56
|
+
</g>
|
|
57
|
+
<g transform="translate(215, 146)">
|
|
58
|
+
<circle cx="6" cy="6" r="5" fill="#A97BFF" />
|
|
59
|
+
<text x="20" y="10" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="500" fill="#94A3B8">Kotlin</text>
|
|
60
|
+
<text x="160" y="10" text-anchor="end" font-family="-apple-system, BlinkMacSystemFont, sans-serif" font-size="13" font-weight="600" fill="#F0F6FC">2.2%</text>
|
|
61
|
+
</g>
|
|
62
|
+
</svg>
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
use rayon::prelude::*;
|
|
2
|
+
use std::collections::HashMap;
|
|
2
3
|
|
|
3
4
|
/// True unrestricted Damerau-Levenshtein distance (Lowrance & Wagner 1975).
|
|
4
5
|
/// Computes edit distance allowing insertions, deletions, substitutions, and transpositions of any characters
|
|
5
6
|
/// (including non-adjacent transpositions where characters were inserted/deleted in between).
|
|
7
|
+
/// Supports both ASCII (fast array path) and full Unicode characters.
|
|
6
8
|
pub fn damerau_levenshtein_distance(a: &str, b: &str) -> u32 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
if a.is_ascii() && b.is_ascii() {
|
|
10
|
+
damerau_bytes(a.as_bytes(), b.as_bytes())
|
|
11
|
+
} else {
|
|
12
|
+
let a_chars: Vec<char> = a.chars().collect();
|
|
13
|
+
let b_chars: Vec<char> = b.chars().collect();
|
|
14
|
+
damerau_chars(&a_chars, &b_chars)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
10
17
|
|
|
18
|
+
fn damerau_bytes(a: &[u8], b: &[u8]) -> u32 {
|
|
19
|
+
let (m, n) = (a.len(), b.len());
|
|
11
20
|
if m == 0 { return n as u32; }
|
|
12
21
|
if n == 0 { return m as u32; }
|
|
13
22
|
if a == b { return 0; }
|
|
14
23
|
|
|
15
24
|
let max_dist = (m + n) as u32;
|
|
16
|
-
|
|
17
|
-
// 2D distance matrix of size (m + 2) x (n + 2)
|
|
18
|
-
let rows = m + 2;
|
|
19
25
|
let cols = n + 2;
|
|
20
|
-
let mut h = vec![0u32;
|
|
26
|
+
let mut h = vec![0u32; (m + 2) * cols];
|
|
21
27
|
|
|
22
28
|
let idx = |i: isize, j: isize| -> usize {
|
|
23
29
|
((i + 1) as usize) * cols + ((j + 1) as usize)
|
|
@@ -33,11 +39,10 @@ pub fn damerau_levenshtein_distance(a: &str, b: &str) -> u32 {
|
|
|
33
39
|
h[idx(0, j as isize)] = j as u32;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
// da[c] stores the last row where character c appeared in a
|
|
37
42
|
let mut da = [0usize; 256];
|
|
38
43
|
|
|
39
44
|
for i in 1..=m {
|
|
40
|
-
let mut db = 0usize;
|
|
45
|
+
let mut db = 0usize;
|
|
41
46
|
let ai = a[i - 1];
|
|
42
47
|
|
|
43
48
|
for j in 1..=n {
|
|
@@ -71,6 +76,67 @@ pub fn damerau_levenshtein_distance(a: &str, b: &str) -> u32 {
|
|
|
71
76
|
h[idx(m as isize, n as isize)]
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
fn damerau_chars(a: &[char], b: &[char]) -> u32 {
|
|
80
|
+
let (m, n) = (a.len(), b.len());
|
|
81
|
+
if m == 0 { return n as u32; }
|
|
82
|
+
if n == 0 { return m as u32; }
|
|
83
|
+
if a == b { return 0; }
|
|
84
|
+
|
|
85
|
+
let max_dist = (m + n) as u32;
|
|
86
|
+
let cols = n + 2;
|
|
87
|
+
let mut h = vec![0u32; (m + 2) * cols];
|
|
88
|
+
|
|
89
|
+
let idx = |i: isize, j: isize| -> usize {
|
|
90
|
+
((i + 1) as usize) * cols + ((j + 1) as usize)
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
h[idx(-1, -1)] = max_dist;
|
|
94
|
+
for i in 0..=m {
|
|
95
|
+
h[idx(i as isize, -1)] = max_dist;
|
|
96
|
+
h[idx(i as isize, 0)] = i as u32;
|
|
97
|
+
}
|
|
98
|
+
for j in 0..=n {
|
|
99
|
+
h[idx(-1, j as isize)] = max_dist;
|
|
100
|
+
h[idx(0, j as isize)] = j as u32;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let mut da: HashMap<char, usize> = HashMap::with_capacity(a.len());
|
|
104
|
+
|
|
105
|
+
for i in 1..=m {
|
|
106
|
+
let mut db = 0usize;
|
|
107
|
+
let ai = a[i - 1];
|
|
108
|
+
|
|
109
|
+
for j in 1..=n {
|
|
110
|
+
let bj = b[j - 1];
|
|
111
|
+
let k = da.get(&bj).copied().unwrap_or(0);
|
|
112
|
+
let l = db;
|
|
113
|
+
|
|
114
|
+
let cost = if ai == bj {
|
|
115
|
+
db = j;
|
|
116
|
+
0u32
|
|
117
|
+
} else {
|
|
118
|
+
1u32
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let sub = h[idx((i - 1) as isize, (j - 1) as isize)] + cost;
|
|
122
|
+
let ins = h[idx(i as isize, (j - 1) as isize)] + 1;
|
|
123
|
+
let del = h[idx((i - 1) as isize, j as isize)] + 1;
|
|
124
|
+
|
|
125
|
+
let trans = if k > 0 && l > 0 {
|
|
126
|
+
h[idx((k - 1) as isize, (l - 1) as isize)] + ((i - k - 1) as u32) + 1 + ((j - l - 1) as u32)
|
|
127
|
+
} else {
|
|
128
|
+
max_dist
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
h[idx(i as isize, j as isize)] = sub.min(ins).min(del).min(trans);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
da.insert(ai, i);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
h[idx(m as isize, n as isize)]
|
|
138
|
+
}
|
|
139
|
+
|
|
74
140
|
/// Batch Damerau-Levenshtein: one query vs many candidates.
|
|
75
141
|
pub fn damerau_levenshtein_batch(query: &str, candidates: &[&str]) -> Vec<u32> {
|
|
76
142
|
candidates.par_iter().map(|c| damerau_levenshtein_distance(query, c)).collect()
|
|
@@ -86,9 +152,11 @@ pub fn damerau_levenshtein_cdist(list_a: &[&str], list_b: &[&str]) -> Vec<Vec<u3
|
|
|
86
152
|
}).collect()
|
|
87
153
|
}
|
|
88
154
|
|
|
89
|
-
/// Damerau-Levenshtein normalized ratio (0.0 to 100.0) based on
|
|
155
|
+
/// Damerau-Levenshtein normalized ratio (0.0 to 100.0) based on standard edit distance similarity formula:
|
|
156
|
+
/// `((total_len - dist) / total_len) * 100.0`.
|
|
90
157
|
pub fn damerau_ratio(s1: &str, s2: &str) -> f64 {
|
|
91
|
-
let
|
|
158
|
+
let len_a = if s1.is_ascii() { s1.len() } else { s1.chars().count() };
|
|
159
|
+
let len_b = if s2.is_ascii() { s2.len() } else { s2.chars().count() };
|
|
92
160
|
let total = len_a + len_b;
|
|
93
161
|
if total == 0 { return 100.0; }
|
|
94
162
|
let dist = damerau_levenshtein_distance(s1, s2) as f64;
|
|
@@ -101,13 +169,19 @@ mod tests {
|
|
|
101
169
|
|
|
102
170
|
#[test]
|
|
103
171
|
fn test_damerau_transposition() {
|
|
104
|
-
// "ab" to "ba" is 1 transposition in Damerau-Levenshtein, but 2 edits in Levenshtein
|
|
105
172
|
assert_eq!(damerau_levenshtein_distance("ab", "ba"), 1);
|
|
106
|
-
// "ca" to "abc" is 2 in true Damerau-Levenshtein (transposition of 'c' and 'a', plus insertion of 'b')
|
|
107
173
|
assert_eq!(damerau_levenshtein_distance("ca", "abc"), 2);
|
|
108
174
|
assert_eq!(damerau_levenshtein_distance("kitten", "sitting"), 3);
|
|
109
175
|
}
|
|
110
176
|
|
|
177
|
+
#[test]
|
|
178
|
+
fn test_damerau_unicode() {
|
|
179
|
+
assert_eq!(damerau_levenshtein_distance("café", "cafe"), 1);
|
|
180
|
+
assert_eq!(damerau_levenshtein_distance("naïve", "naive"), 1);
|
|
181
|
+
assert_eq!(damerau_levenshtein_distance("🚀", ""), 1);
|
|
182
|
+
assert_eq!(damerau_levenshtein_distance("中文", "中问"), 1);
|
|
183
|
+
}
|
|
184
|
+
|
|
111
185
|
#[test]
|
|
112
186
|
fn test_damerau_identical_and_empty() {
|
|
113
187
|
assert_eq!(damerau_levenshtein_distance("", ""), 0);
|
|
@@ -2,10 +2,19 @@ use rayon::prelude::*;
|
|
|
2
2
|
use std::collections::BTreeSet;
|
|
3
3
|
|
|
4
4
|
/// Standard Indel / Levenshtein (substitution cost = 2) edit distance used for fuzzy ratios.
|
|
5
|
+
/// Supports both ASCII fast-path and full Unicode characters.
|
|
5
6
|
#[inline]
|
|
6
7
|
pub fn indel_distance(a: &str, b: &str) -> u32 {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
if a.is_ascii() && b.is_ascii() {
|
|
9
|
+
indel_distance_slice(a.as_bytes(), b.as_bytes())
|
|
10
|
+
} else {
|
|
11
|
+
let a_chars: Vec<char> = a.chars().collect();
|
|
12
|
+
let b_chars: Vec<char> = b.chars().collect();
|
|
13
|
+
indel_distance_slice(&a_chars, &b_chars)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn indel_distance_slice<T: PartialEq>(a: &[T], b: &[T]) -> u32 {
|
|
9
18
|
let (m, n) = (a.len(), b.len());
|
|
10
19
|
if m == 0 { return n as u32; }
|
|
11
20
|
if n == 0 { return m as u32; }
|
|
@@ -19,10 +28,10 @@ pub fn indel_distance(a: &str, b: &str) -> u32 {
|
|
|
19
28
|
for i in 1..=m {
|
|
20
29
|
let mut prev_diag = row[0];
|
|
21
30
|
row[0] = i as u32;
|
|
22
|
-
let ai = a[i - 1];
|
|
31
|
+
let ai = &a[i - 1];
|
|
23
32
|
for j in 1..=n {
|
|
24
33
|
let old = row[j];
|
|
25
|
-
let cost = if ai == b[j - 1] { 0 } else { 2 };
|
|
34
|
+
let cost = if ai == &b[j - 1] { 0 } else { 2 };
|
|
26
35
|
row[j] = (prev_diag + cost).min(row[j] + 1).min(row[j - 1] + 1);
|
|
27
36
|
prev_diag = old;
|
|
28
37
|
}
|
|
@@ -34,9 +43,10 @@ pub fn indel_distance(a: &str, b: &str) -> u32 {
|
|
|
34
43
|
///
|
|
35
44
|
/// Formula: `(|a| + |b| - indel_distance) / (|a| + |b|) × 100`
|
|
36
45
|
///
|
|
37
|
-
/// Matches RapidFuzz and FuzzyWuzzy identically across all
|
|
46
|
+
/// Matches RapidFuzz and FuzzyWuzzy identically across all ASCII and multi-byte Unicode test cases.
|
|
38
47
|
pub fn ratio(s1: &str, s2: &str) -> f64 {
|
|
39
|
-
let
|
|
48
|
+
let len_a = if s1.is_ascii() { s1.len() } else { s1.chars().count() };
|
|
49
|
+
let len_b = if s2.is_ascii() { s2.len() } else { s2.chars().count() };
|
|
40
50
|
let total = len_a + len_b;
|
|
41
51
|
if total == 0 { return 100.0; }
|
|
42
52
|
let dist = indel_distance(s1, s2) as f64;
|
|
@@ -51,33 +61,41 @@ pub fn ratio(s1: &str, s2: &str) -> f64 {
|
|
|
51
61
|
pub fn partial_ratio(s1: &str, s2: &str) -> f64 {
|
|
52
62
|
if s1.is_empty() || s2.is_empty() { return 0.0; }
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
let
|
|
64
|
+
let s1_count = if s1.is_ascii() { s1.len() } else { s1.chars().count() };
|
|
65
|
+
let s2_count = if s2.is_ascii() { s2.len() } else { s2.chars().count() };
|
|
56
66
|
|
|
57
|
-
let
|
|
58
|
-
|
|
67
|
+
let (shorter, longer, short_chars, long_chars) = if s1_count <= s2_count {
|
|
68
|
+
(s1, s2, s1_count, s2_count)
|
|
69
|
+
} else {
|
|
70
|
+
(s2, s1, s2_count, s1_count)
|
|
71
|
+
};
|
|
59
72
|
|
|
60
|
-
if
|
|
73
|
+
if short_chars == long_chars {
|
|
61
74
|
return ratio(shorter, longer);
|
|
62
75
|
}
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if start > 0 && !longer.is_char_boundary(start) { continue; }
|
|
71
|
-
let end = start + short_len;
|
|
72
|
-
if end <= long_len && longer.is_char_boundary(end) {
|
|
73
|
-
let window = &longer[start..end];
|
|
77
|
+
if shorter.is_ascii() && longer.is_ascii() {
|
|
78
|
+
let short_bytes = shorter.as_bytes();
|
|
79
|
+
let long_bytes = longer.as_bytes();
|
|
80
|
+
let mut best = 0.0f64;
|
|
81
|
+
for start in 0..=(long_bytes.len() - short_bytes.len()) {
|
|
82
|
+
let window = &longer[start..start + short_bytes.len()];
|
|
74
83
|
let score = ratio(shorter, window);
|
|
75
84
|
if score > best { best = score; }
|
|
76
|
-
if best == 100.0 { return 100.0; }
|
|
85
|
+
if best == 100.0 { return 100.0; }
|
|
86
|
+
}
|
|
87
|
+
best
|
|
88
|
+
} else {
|
|
89
|
+
let longer_chars: Vec<char> = longer.chars().collect();
|
|
90
|
+
let mut best = 0.0f64;
|
|
91
|
+
for start in 0..=(longer_chars.len() - short_chars) {
|
|
92
|
+
let window: String = longer_chars[start..start + short_chars].iter().collect();
|
|
93
|
+
let score = ratio(shorter, &window);
|
|
94
|
+
if score > best { best = score; }
|
|
95
|
+
if best == 100.0 { return 100.0; }
|
|
77
96
|
}
|
|
97
|
+
best
|
|
78
98
|
}
|
|
79
|
-
|
|
80
|
-
best
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
/// Token sort ratio: sort tokens alphabetically, then compare.
|
|
@@ -108,12 +126,9 @@ pub fn token_set_ratio(s1: &str, s2: &str) -> f64 {
|
|
|
108
126
|
|
|
109
127
|
// If both strings have identical token sets
|
|
110
128
|
if diff1.is_empty() && diff2.is_empty() {
|
|
111
|
-
return
|
|
129
|
+
return 100.0;
|
|
112
130
|
}
|
|
113
131
|
|
|
114
|
-
// t0 = intersection
|
|
115
|
-
// t1 = intersection + diff1
|
|
116
|
-
// t2 = intersection + diff2
|
|
117
132
|
let t0 = &inter_str;
|
|
118
133
|
|
|
119
134
|
let t1 = if inter_str.is_empty() {
|
|
@@ -155,15 +170,10 @@ pub fn ratio_batch(query: &str, candidates: &[&str]) -> Vec<f64> {
|
|
|
155
170
|
/// Extract top matches with partial-sort optimization.
|
|
156
171
|
///
|
|
157
172
|
/// Returns `Vec<(match_string, score, original_index)>` sorted by score descending.
|
|
158
|
-
///
|
|
159
|
-
/// Uses `select_nth_unstable_by` for O(n) top-K selection when `limit << choices.len()`,
|
|
160
|
-
/// and parallelizes scoring with Rayon for large choice sets.
|
|
161
173
|
pub fn extract(query: &str, choices: &[&str], score_cutoff: f64, limit: usize) -> Vec<(String, f64, usize)> {
|
|
162
174
|
if choices.is_empty() || limit == 0 { return vec![]; }
|
|
163
175
|
|
|
164
|
-
// Score all choices, filtering by cutoff.
|
|
165
176
|
let mut results: Vec<(String, f64, usize)> = if choices.len() > 1000 {
|
|
166
|
-
// Parallelize for large choice sets.
|
|
167
177
|
choices.par_iter().enumerate()
|
|
168
178
|
.filter_map(|(i, c)| {
|
|
169
179
|
let score = ratio(query, c);
|
|
@@ -181,14 +191,11 @@ pub fn extract(query: &str, choices: &[&str], score_cutoff: f64, limit: usize) -
|
|
|
181
191
|
|
|
182
192
|
if results.is_empty() { return results; }
|
|
183
193
|
|
|
184
|
-
// Optimized top-K: if we need far fewer results than we have,
|
|
185
|
-
// use partial sort instead of full sort.
|
|
186
194
|
if limit < results.len() {
|
|
187
195
|
results.select_nth_unstable_by(limit, |a, b| {
|
|
188
196
|
b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
|
|
189
197
|
});
|
|
190
198
|
results.truncate(limit);
|
|
191
|
-
// Sort the top-K for deterministic output order.
|
|
192
199
|
results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
|
193
200
|
} else {
|
|
194
201
|
results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
|
@@ -212,7 +219,7 @@ pub fn extract_one(query: &str, choices: &[&str], score_cutoff: f64) -> Option<(
|
|
|
212
219
|
best = Some((c.to_string(), score, i));
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
|
-
if score == 100.0 { break; }
|
|
222
|
+
if score == 100.0 { break; }
|
|
216
223
|
}
|
|
217
224
|
|
|
218
225
|
best
|
|
@@ -224,23 +231,26 @@ mod tests {
|
|
|
224
231
|
|
|
225
232
|
#[test]
|
|
226
233
|
fn test_ratio_sorensen_dice() {
|
|
227
|
-
// "hello" (5) vs "hallo" (5): edit distance = 2 (sub cost = 2)
|
|
228
|
-
// Expected: (5 + 5 - 2) / (5 + 5) * 100 = 80.0
|
|
229
234
|
let r = ratio("hello", "hallo");
|
|
230
235
|
assert!((r - 80.0).abs() < 0.01, "Expected 80.0, got {}", r);
|
|
231
236
|
}
|
|
232
237
|
|
|
238
|
+
#[test]
|
|
239
|
+
fn test_ratio_unicode() {
|
|
240
|
+
let r = ratio("café", "cafe");
|
|
241
|
+
// "café" (4 chars), "cafe" (4 chars), indel distance = 2
|
|
242
|
+
// (4 + 4 - 2) / (4 + 4) * 100 = 75.0%
|
|
243
|
+
assert!((r - 75.0).abs() < 0.01, "Expected 75.0, got {}", r);
|
|
244
|
+
}
|
|
245
|
+
|
|
233
246
|
#[test]
|
|
234
247
|
fn test_ratio_asymmetric() {
|
|
235
|
-
// "a" (1) vs "abc" (3): edit distance = 2
|
|
236
|
-
// Expected: (1 + 3 - 2) / (1 + 3) * 100 = 50.0
|
|
237
248
|
let r = ratio("a", "abc");
|
|
238
249
|
assert!((r - 50.0).abs() < 0.01, "Expected 50.0, got {}", r);
|
|
239
250
|
}
|
|
240
251
|
|
|
241
252
|
#[test]
|
|
242
253
|
fn test_partial_ratio() {
|
|
243
|
-
// "hello" is a substring of "oh hello there"
|
|
244
254
|
let r = partial_ratio("hello", "oh hello there");
|
|
245
255
|
assert!(r >= 100.0 - 0.01, "Expected ~100.0, got {}", r);
|
|
246
256
|
}
|
|
@@ -254,11 +264,4 @@ mod tests {
|
|
|
254
264
|
assert_eq!(m, "apple");
|
|
255
265
|
assert!((s - 100.0).abs() < 0.01);
|
|
256
266
|
}
|
|
257
|
-
|
|
258
|
-
#[test]
|
|
259
|
-
fn test_extract_one_none_below_cutoff() {
|
|
260
|
-
let choices = vec!["zzzzz"];
|
|
261
|
-
let result = extract_one("apple", &choices, 90.0);
|
|
262
|
-
assert!(result.is_none());
|
|
263
|
-
}
|
|
264
267
|
}
|