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

1from __future__ import annotations 

2 

3import math 

4from typing import Any, Dict, Set 

5 

6 

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 

12 

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 

17 

18 s1_matches = [False] * len_s1 

19 s2_matches = [False] * len_s2 

20 matches = 0 

21 

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 

32 

33 if matches == 0: 

34 return 0.0 

35 

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 

46 

47 jaro = (matches / len_s1 + matches / len_s2 + (matches - transpositions / 2.0) / matches) / 3.0 

48 

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 

55 

56 return jaro + prefix * 0.1 * (1.0 - jaro) 

57 

58 

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) 

70 

71 

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__}" 

86 

87 

88def count_nodes(value: Any) -> int: 

89 seen: Set[int] = set() 

90 

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 

107 

108 return _count(value) 

109 

110 

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