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.
Files changed (38) hide show
  1. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/Cargo.lock +2 -2
  2. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/PKG-INFO +3 -3
  3. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/README.md +2 -2
  4. fuzzgpu-0.1.2/assets/banner.svg +66 -0
  5. fuzzgpu-0.1.2/assets/github-stats.svg +51 -0
  6. fuzzgpu-0.1.2/assets/top-languages.svg +62 -0
  7. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/Cargo.toml +1 -1
  8. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/damerau.rs +87 -13
  9. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/fuzz.rs +52 -49
  10. fuzzgpu-0.1.2/crates/fuzzgpu-core/src/gpu.rs +151 -0
  11. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/jaro.rs +140 -54
  12. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/levenshtein.rs +81 -41
  13. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/needleman.rs +48 -30
  14. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/shaders/jaro.wgsl +13 -11
  15. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/shaders/jaro_matrix.wgsl +13 -10
  16. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-python/Cargo.toml +2 -2
  17. fuzzgpu-0.1.2/crates/fuzzgpu-python/src/lib.rs +442 -0
  18. fuzzgpu-0.1.2/crates/fuzzgpu-wasm/Cargo.toml +19 -0
  19. fuzzgpu-0.1.2/crates/fuzzgpu-wasm/src/lib.rs +92 -0
  20. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/pyproject.toml +2 -2
  21. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/python/fuzzgpu/__init__.py +28 -15
  22. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/python/fuzzgpu/fuzz.py +13 -2
  23. fuzzgpu-0.1.2/tests/test_api_signatures.py +171 -0
  24. fuzzgpu-0.1.2/tests/test_basic.py +323 -0
  25. fuzzgpu-0.1.2/tests/test_concurrency.py +62 -0
  26. fuzzgpu-0.1.2/tests/test_edge_cases.py +338 -0
  27. fuzzgpu-0.1.2/tests/test_invariants.py +169 -0
  28. fuzzgpu-0.1.2/tests/test_stress.py +123 -0
  29. fuzzgpu-0.1.0/crates/fuzzgpu-core/src/gpu.rs +0 -96
  30. fuzzgpu-0.1.0/crates/fuzzgpu-python/src/lib.rs +0 -273
  31. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/Cargo.toml +0 -0
  32. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/LICENSE +0 -0
  33. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/assets/logo.png +0 -0
  34. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/assets/logo.svg +0 -0
  35. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/lib.rs +0 -0
  36. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/shaders/levenshtein.wgsl +0 -0
  37. {fuzzgpu-0.1.0 → fuzzgpu-0.1.2}/crates/fuzzgpu-core/src/shaders/levenshtein_matrix.wgsl +0 -0
  38. {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.0"
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.0"
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.0
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
- [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.0-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
38
+ [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.2-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
39
39
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](https://opensource.org/licenses/MIT)
40
40
  [![Rust](https://img.shields.io/badge/rust-1.75+-orange.svg?style=flat-square)](https://www.rust-lang.org)
41
41
  [![Cross Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux%20%7C%20WASM-lightgrey.svg?style=flat-square)](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.0"
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
- [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.0-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
11
+ [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.2-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](https://opensource.org/licenses/MIT)
13
13
  [![Rust](https://img.shields.io/badge/rust-1.75+-orange.svg?style=flat-square)](https://www.rust-lang.org)
14
14
  [![Cross Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux%20%7C%20WASM-lightgrey.svg?style=flat-square)](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.0"
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 &amp; 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,6 +1,6 @@
1
1
  [package]
2
2
  name = "fuzzgpu-core"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  edition = "2021"
5
5
  description = "GPU-accelerated fuzzy string matching engine"
6
6
  license = "MIT"
@@ -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
- let a = a.as_bytes();
8
- let b = b.as_bytes();
9
- let (m, n) = (a.len(), b.len());
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; rows * cols];
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; // last column where character appeared in b
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 Sørensen-Dice formula.
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 (len_a, len_b) = (s1.len(), s2.len());
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
- let a = a.as_bytes();
8
- let b = b.as_bytes();
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 symmetric and asymmetric test cases.
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 (len_a, len_b) = (s1.len(), s2.len());
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
- // Ensure `shorter` is the shorter string.
55
- let (shorter, longer) = if s1.len() <= s2.len() { (s1, s2) } else { (s2, s1) };
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 short_len = shorter.len();
58
- let long_len = longer.len();
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 short_len == long_len {
73
+ if short_chars == long_chars {
61
74
  return ratio(shorter, longer);
62
75
  }
63
76
 
64
-
65
- let mut best = 0.0f64;
66
-
67
- // Slide a window of `short_len` across `longer`.
68
- for start in 0..=(long_len - short_len) {
69
- // Only compare at valid UTF-8 boundaries.
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; } // Early exit: can't do better.
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 if inter_str.is_empty() { 100.0 } else { 100.0 };
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; } // Can't do better.
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
  }