Coverage for src\sembra\utils.py: 100%
96 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 12:45 +0700
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 12:45 +0700
1from __future__ import annotations
3import math
4from typing import Any, Dict, Set
7def jaro_winkler(s1: str, s2: str) -> float:
8 if s1 == s2:
9 return 1.0
10 if not s1 or not s2:
11 return 0.0
13 len_s1, len_s2 = len(s1), len(s2)
14 match_distance = max(len_s1, len_s2) // 2 - 1
15 if match_distance < 0:
16 match_distance = 0
18 s1_matches = [False] * len_s1
19 s2_matches = [False] * len_s2
20 matches = 0
22 for i in range(len_s1):
23 start = max(0, i - match_distance)
24 end = min(i + match_distance + 1, len_s2)
25 for j in range(start, end):
26 if s2_matches[j] or s1[i] != s2[j]:
27 continue
28 s1_matches[i] = True
29 s2_matches[j] = True
30 matches += 1
31 break
33 if matches == 0:
34 return 0.0
36 transpositions = 0
37 k = 0
38 for i in range(len_s1):
39 if not s1_matches[i]:
40 continue
41 while not s2_matches[k]:
42 k += 1
43 if s1[i] != s2[k]:
44 transpositions += 1
45 k += 1
47 jaro = (matches / len_s1 + matches / len_s2 + (matches - transpositions / 2.0) / matches) / 3.0
49 prefix = 0
50 for i in range(min(4, len_s1, len_s2)):
51 if s1[i] == s2[i]:
52 prefix += 1
53 else:
54 break
56 return jaro + prefix * 0.1 * (1.0 - jaro)
59def number_similarity(a: float, b: float) -> float:
60 if a == b:
61 return 1.0
62 if math.isnan(a) and math.isnan(b):
63 return 1.0
64 if math.isnan(a) or math.isnan(b):
65 return 0.0
66 max_abs = max(abs(a), abs(b))
67 if not math.isfinite(max_abs):
68 return 0.0
69 return 1.0 - min(1.0, abs(a - b) / max_abs)
72def get_json_type(value: Any) -> str:
73 if value is None:
74 return "null"
75 if isinstance(value, bool):
76 return "boolean"
77 if isinstance(value, (int, float)):
78 return "number"
79 if isinstance(value, str):
80 return "string"
81 if isinstance(value, (list, tuple)):
82 return "array"
83 if isinstance(value, dict):
84 return "object"
85 return f"unknown:{type(value).__name__}"
88def count_nodes(value: Any) -> int:
89 seen: Set[int] = set()
91 def _count(v: Any) -> int:
92 vid = id(v)
93 if vid in seen:
94 return 0
95 seen.add(vid)
96 if isinstance(v, (list, tuple)):
97 total = 1
98 for item in v:
99 total += _count(item)
100 return total
101 if isinstance(v, dict):
102 total = 1
103 for k, val in v.items():
104 total += _count(k) + _count(val)
105 return total
106 return 1
108 return _count(value)
111def extract_keys(obj: dict, prefix: str = "") -> Dict[str, Any]:
112 result: Dict[str, Any] = {}
113 for k, v in obj.items():
114 path = f"{prefix}.{k}" if prefix else str(k)
115 if isinstance(v, dict):
116 result.update(extract_keys(v, path))
117 else:
118 result[path] = v
119 return result