ejschwartz commited on
Commit
a89d907
·
1 Parent(s): b034509

Update for bytes

Browse files
Files changed (1) hide show
  1. dist.py +199 -80
dist.py CHANGED
@@ -1,23 +1,89 @@
1
- def levenshtein_with_wildcard(str1, str2, wildcard_offsets_str1=None, wildcard_offsets_str2=None, verbose=False):
 
 
 
 
 
 
 
 
2
  """
3
- Calculate the Levenshtein distance between two strings with support for wildcards at specific positions.
4
 
5
  Args:
6
- str1 (str): The first string.
7
- str2 (str): The second string.
8
- wildcard_offsets_str1 (iterable, optional): Indices in str1 that are wildcards. Defaults to None.
9
- wildcard_offsets_str2 (iterable, optional): Indices in str2 that are wildcards. Defaults to None.
10
- verbose (bool, optional): If True, prints the DP matrix and explains the process.
11
 
12
  Returns:
13
- int: The Levenshtein distance between the two strings.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  list: If verbose=True, also returns a list of operations performed.
15
  """
16
  # Initialize empty sets if None
17
- wildcard_offsets_str1 = set(wildcard_offsets_str1 or [])
18
- wildcard_offsets_str2 = set(wildcard_offsets_str2 or [])
19
 
20
- m, n = len(str1), len(str2)
21
 
22
  # Create a matrix of size (m+1) x (n+1)
23
  dp = [[0] * (n + 1) for _ in range(m + 1)]
@@ -33,14 +99,14 @@ def levenshtein_with_wildcard(str1, str2, wildcard_offsets_str1=None, wildcard_o
33
  for i in range(1, m + 1):
34
  for j in range(1, n + 1):
35
  # Check if either position is a wildcard
36
- is_str1_wildcard = (i - 1) in wildcard_offsets_str1
37
- is_str2_wildcard = (j - 1) in wildcard_offsets_str2
38
 
39
  # If either position is a wildcard, treat it as a match (cost = 0)
40
- if is_str1_wildcard or is_str2_wildcard:
41
  dp[i][j] = dp[i - 1][j - 1] # No cost for wildcard matches
42
  else:
43
- cost = 0 if str1[i - 1] == str2[j - 1] else 1
44
  dp[i][j] = min(
45
  dp[i - 1][j] + 1, # deletion
46
  dp[i][j - 1] + 1, # insertion
@@ -48,26 +114,26 @@ def levenshtein_with_wildcard(str1, str2, wildcard_offsets_str1=None, wildcard_o
48
  )
49
 
50
  if verbose:
51
- operations = explain_match(str1, str2, dp, wildcard_offsets_str1, wildcard_offsets_str2)
52
  return dp[m][n], operations
53
 
54
  return dp[m][n]
55
 
56
- def explain_match(str1, str2, dp, wildcard_offsets_str1, wildcard_offsets_str2):
57
  """
58
  Traces the optimal alignment path and explains each step of the matching process.
59
 
60
  Args:
61
- str1 (str): The first string.
62
- str2 (str): The second string.
63
  dp (list): The dynamic programming matrix.
64
- wildcard_offsets_str1 (set): Indices in str1 that are wildcards.
65
- wildcard_offsets_str2 (set): Indices in str2 that are wildcards.
66
 
67
  Returns:
68
  list: A list of explanation strings for each operation performed.
69
  """
70
- m, n = len(str1), len(str2)
71
  operations = []
72
 
73
  # Find the optimal path
@@ -108,68 +174,87 @@ def explain_match(str1, str2, dp, wildcard_offsets_str1, wildcard_offsets_str2):
108
  if curr_i > prev_i and curr_j > prev_j:
109
  char1_idx = curr_i-1
110
  char2_idx = curr_j-1
111
- char1 = str1[char1_idx]
112
- char2 = str2[char2_idx]
113
 
114
- is_str1_wildcard = char1_idx in wildcard_offsets_str1
115
- is_str2_wildcard = char2_idx in wildcard_offsets_str2
116
 
117
- if is_str1_wildcard and is_str2_wildcard:
118
- operations.append(f"Double wildcard: Position {char1_idx} in str1 and position {char2_idx} in str2 are both wildcards")
119
- elif is_str1_wildcard:
120
- operations.append(f"Wildcard match: Position {char1_idx} in str1 is a wildcard, matches '{char2}' at position {char2_idx} in str2")
121
- elif is_str2_wildcard:
122
- operations.append(f"Wildcard match: Position {char2_idx} in str2 is a wildcard, matches '{char1}' at position {char1_idx} in str1")
 
 
 
123
  elif char1 == char2:
124
- operations.append(f"Match: '{char1}' at position {char1_idx} matches '{char2}' at position {char2_idx}")
125
  else:
126
- operations.append(f"Substitution: Replace '{char1}' at position {char1_idx} with '{char2}' at position {char2_idx}")
127
 
128
  # Horizontal move (insertion)
129
  elif curr_i == prev_i and curr_j > prev_j:
130
  char_idx = curr_j-1
131
- operations.append(f"Insertion: Insert '{str2[char_idx]}' at position {char_idx} in str2")
 
132
 
133
  # Vertical move (deletion)
134
  elif curr_i > prev_i and curr_j == prev_j:
135
  char_idx = curr_i-1
136
- operations.append(f"Deletion: Delete '{str1[char_idx]}' at position {char_idx} in str1")
 
137
 
138
  return operations
139
 
140
- def to_bytes(s):
141
- return s.encode('utf-8') if isinstance(s, str) else s
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
- def print_match_summary(str1, str2, wildcard_offsets_str1=None, wildcard_offsets_str2=None):
144
  """
145
- Prints a summary of the match between two strings, highlighting wildcards by their offsets.
 
146
 
147
  Args:
148
- str1 (str): The first string.
149
- str2 (str): The second string.
150
- wildcard_offsets_str1 (iterable, optional): Indices in str1 that are wildcards. Defaults to None.
151
- wildcard_offsets_str2 (iterable, optional): Indices in str2 that are wildcards. Defaults to None.
 
 
 
152
  """
153
- # Initialize empty lists if None
154
- wildcard_offsets_str1 = set(wildcard_offsets_str1 or [])
155
- wildcard_offsets_str2 = set(wildcard_offsets_str2 or [])
 
 
 
156
 
157
  distance, operations = levenshtein_with_wildcard(
158
- str1, str2, wildcard_offsets_str1, wildcard_offsets_str2, verbose=True
159
  )
160
 
161
- # Create visual representations of the strings with wildcard markers
162
- str1_visual = type(str1)()
163
- for i, char in enumerate(str1):
164
- str1_visual += bytes([char])
165
 
166
- str2_visual = type(str1)()
167
- for i, char in enumerate(str2):
168
- str2_visual += bytes([char])
169
-
170
- print(f"Comparing '{str1_visual}' and '{str2_visual}'")
171
- print(f"Wildcards in str1: {sorted(wildcard_offsets_str1)}")
172
- print(f"Wildcards in str2: {sorted(wildcard_offsets_str2)}")
173
  print(f"Edit distance: {distance}")
174
  print("\nMatch process:")
175
 
@@ -178,15 +263,27 @@ def print_match_summary(str1, str2, wildcard_offsets_str1=None, wildcard_offsets
178
 
179
  # Visual representation of the alignment
180
  i, j = 0, 0
181
- aligned_str1 = type(str1)()
182
- aligned_str2 = type(str1)()
 
 
 
 
 
 
 
 
 
183
  match_indicators = ""
184
 
185
  for op in operations:
186
  if "Match:" in op or "Substitution:" in op or "Wildcard match:" in op or "Double wildcard:" in op:
187
- aligned_str1 += str1[i:i+1]
188
-
189
- aligned_str2 += str2[j:j+1]
 
 
 
190
 
191
  # Determine match indicator
192
  if "Wildcard match:" in op or "Double wildcard:" in op:
@@ -199,25 +296,36 @@ def print_match_summary(str1, str2, wildcard_offsets_str1=None, wildcard_offsets
199
  i += 1
200
  j += 1
201
  elif "Insertion:" in op:
202
- aligned_str1 += to_bytes("-")
203
-
204
- aligned_str2 += str2[j:j+1]
 
 
 
205
 
206
  match_indicators += " "
207
  j += 1
208
  elif "Deletion:" in op:
209
- aligned_str1 += str1[i:i+1]
210
-
211
- aligned_str2 += to_bytes("-")
 
 
 
 
212
  match_indicators += " "
213
  i += 1
214
 
215
  print("\nAlignment:")
216
- print(aligned_str1)
 
 
 
 
217
  print(match_indicators)
218
- print(aligned_str2)
219
  print("\nLegend:")
220
- print("| = exact match, * = wildcard match, X = substitution, - = gap (insertion/deletion), [c] = wildcard position")
221
 
222
  # Summary of wildcard matches
223
  wildcard_matches = [op for op in operations if "Wildcard match:" in op or "Double wildcard:" in op]
@@ -230,6 +338,7 @@ def print_match_summary(str1, str2, wildcard_offsets_str1=None, wildcard_offsets
230
 
231
  # Example usage
232
  if __name__ == "__main__":
 
233
  # Example 1: "hello" vs "hello" with no wildcards
234
  print_match_summary("hello", "hello")
235
 
@@ -237,13 +346,23 @@ if __name__ == "__main__":
237
  print_match_summary("hello", "hallo")
238
 
239
  # Example 3: "hello" with 3rd position (index 2) as wildcard vs "hallo" - expect distance of 0
240
- print_match_summary("hello", "hallo", wildcard_offsets_str1=[2])
 
 
 
 
 
 
 
 
 
 
241
 
242
- # Example 4: "hello" vs "hillo" with 2nd position (index 1) as wildcard in str2 - expect distance of 0
243
- print_match_summary("hello", "hillo", wildcard_offsets_str2=[1])
244
 
245
- # Example 5: Multiple wildcards in str1
246
- print_match_summary("hello", "haxyz", wildcard_offsets_str1=[2, 3, 4])
247
 
248
- # Example 6: Wildcards in both strings at different positions
249
- print_match_summary("hello", "howdy", wildcard_offsets_str1=[2], wildcard_offsets_str2=[3, 4])
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Levenshtein distance calculation with wildcard support for both strings and bytes.
4
+
5
+ This module provides functions to calculate Levenshtein (edit) distance between
6
+ two sequences (strings or bytes) with support for wildcard positions.
7
+ """
8
+
9
+ def ensure_same_type(seq1, seq2):
10
  """
11
+ Ensure both sequences are the same type (both str or both bytes).
12
 
13
  Args:
14
+ seq1: First sequence (str or bytes)
15
+ seq2: Second sequence (str or bytes)
 
 
 
16
 
17
  Returns:
18
+ Tuple of (seq1, seq2) with consistent types
19
+ """
20
+ if isinstance(seq1, str) and isinstance(seq2, bytes):
21
+ seq2 = seq2.decode('utf-8', errors='replace')
22
+ elif isinstance(seq1, bytes) and isinstance(seq2, str):
23
+ seq2 = seq2.encode('utf-8', errors='replace')
24
+ return seq1, seq2
25
+
26
+ def to_bytes(s):
27
+ """
28
+ Convert a sequence to bytes if it's a string, otherwise return as is.
29
+
30
+ Args:
31
+ s: The sequence to convert (str or bytes)
32
+
33
+ Returns:
34
+ bytes: The input converted to bytes if it was a string
35
+ """
36
+ return s.encode('utf-8', errors='replace') if isinstance(s, str) else s
37
+
38
+ def to_str(s):
39
+ """
40
+ Convert a sequence to string if it's bytes, otherwise return as is.
41
+
42
+ Args:
43
+ s: The sequence to convert (str or bytes)
44
+
45
+ Returns:
46
+ str: The input converted to string if it was bytes
47
+ """
48
+ return s.decode('utf-8', errors='replace') if isinstance(s, bytes) else s
49
+
50
+ def get_element_repr(element):
51
+ """
52
+ Get a human-readable representation of a sequence element.
53
+
54
+ Args:
55
+ element: A single element from a sequence (byte or character)
56
+
57
+ Returns:
58
+ str: A printable representation of the element
59
+ """
60
+ if isinstance(element, int): # For bytes objects
61
+ if 32 <= element <= 126: # Printable ASCII
62
+ return repr(chr(element))
63
+ return f"0x{element:02x}"
64
+ return repr(element) # For str objects
65
+
66
+ def levenshtein_with_wildcard(seq1, seq2, wildcard_offsets_seq1=None, wildcard_offsets_seq2=None, verbose=False):
67
+ """
68
+ Calculate the Levenshtein distance between two sequences with support for wildcards.
69
+ Works with both strings and bytes.
70
+
71
+ Args:
72
+ seq1: First sequence (str or bytes)
73
+ seq2: Second sequence (str or bytes)
74
+ wildcard_offsets_seq1 (iterable, optional): Indices in seq1 that are wildcards. Defaults to None.
75
+ wildcard_offsets_seq2 (iterable, optional): Indices in seq2 that are wildcards. Defaults to None.
76
+ verbose (bool, optional): If True, returns additional information about operations. Defaults to False.
77
+
78
+ Returns:
79
+ int: The Levenshtein distance between the two sequences.
80
  list: If verbose=True, also returns a list of operations performed.
81
  """
82
  # Initialize empty sets if None
83
+ wildcard_offsets_seq1 = set(wildcard_offsets_seq1 or [])
84
+ wildcard_offsets_seq2 = set(wildcard_offsets_seq2 or [])
85
 
86
+ m, n = len(seq1), len(seq2)
87
 
88
  # Create a matrix of size (m+1) x (n+1)
89
  dp = [[0] * (n + 1) for _ in range(m + 1)]
 
99
  for i in range(1, m + 1):
100
  for j in range(1, n + 1):
101
  # Check if either position is a wildcard
102
+ is_seq1_wildcard = (i - 1) in wildcard_offsets_seq1
103
+ is_seq2_wildcard = (j - 1) in wildcard_offsets_seq2
104
 
105
  # If either position is a wildcard, treat it as a match (cost = 0)
106
+ if is_seq1_wildcard or is_seq2_wildcard:
107
  dp[i][j] = dp[i - 1][j - 1] # No cost for wildcard matches
108
  else:
109
+ cost = 0 if seq1[i - 1] == seq2[j - 1] else 1
110
  dp[i][j] = min(
111
  dp[i - 1][j] + 1, # deletion
112
  dp[i][j - 1] + 1, # insertion
 
114
  )
115
 
116
  if verbose:
117
+ operations = explain_match(seq1, seq2, dp, wildcard_offsets_seq1, wildcard_offsets_seq2)
118
  return dp[m][n], operations
119
 
120
  return dp[m][n]
121
 
122
+ def explain_match(seq1, seq2, dp, wildcard_offsets_seq1, wildcard_offsets_seq2):
123
  """
124
  Traces the optimal alignment path and explains each step of the matching process.
125
 
126
  Args:
127
+ seq1: First sequence (str or bytes)
128
+ seq2: Second sequence (str or bytes)
129
  dp (list): The dynamic programming matrix.
130
+ wildcard_offsets_seq1 (set): Indices in seq1 that are wildcards.
131
+ wildcard_offsets_seq2 (set): Indices in seq2 that are wildcards.
132
 
133
  Returns:
134
  list: A list of explanation strings for each operation performed.
135
  """
136
+ m, n = len(seq1), len(seq2)
137
  operations = []
138
 
139
  # Find the optimal path
 
174
  if curr_i > prev_i and curr_j > prev_j:
175
  char1_idx = curr_i-1
176
  char2_idx = curr_j-1
177
+ char1 = seq1[char1_idx]
178
+ char2 = seq2[char2_idx]
179
 
180
+ is_seq1_wildcard = char1_idx in wildcard_offsets_seq1
181
+ is_seq2_wildcard = char2_idx in wildcard_offsets_seq2
182
 
183
+ char1_repr = get_element_repr(char1)
184
+ char2_repr = get_element_repr(char2)
185
+
186
+ if is_seq1_wildcard and is_seq2_wildcard:
187
+ operations.append(f"Double wildcard: Position {char1_idx} in seq1 and position {char2_idx} in seq2 are both wildcards")
188
+ elif is_seq1_wildcard:
189
+ operations.append(f"Wildcard match: Position {char1_idx} in seq1 is a wildcard, matches {char2_repr} at position {char2_idx} in seq2")
190
+ elif is_seq2_wildcard:
191
+ operations.append(f"Wildcard match: Position {char2_idx} in seq2 is a wildcard, matches {char1_repr} at position {char1_idx} in seq1")
192
  elif char1 == char2:
193
+ operations.append(f"Match: {char1_repr} at position {char1_idx} matches {char2_repr} at position {char2_idx}")
194
  else:
195
+ operations.append(f"Substitution: Replace {char1_repr} at position {char1_idx} with {char2_repr} at position {char2_idx}")
196
 
197
  # Horizontal move (insertion)
198
  elif curr_i == prev_i and curr_j > prev_j:
199
  char_idx = curr_j-1
200
+ char_repr = get_element_repr(seq2[char_idx])
201
+ operations.append(f"Insertion: Insert {char_repr} at position {char_idx} in seq2")
202
 
203
  # Vertical move (deletion)
204
  elif curr_i > prev_i and curr_j == prev_j:
205
  char_idx = curr_i-1
206
+ char_repr = get_element_repr(seq1[char_idx])
207
+ operations.append(f"Deletion: Delete {char_repr} at position {char_idx} in seq1")
208
 
209
  return operations
210
 
211
+ def create_gap_element(sequence):
212
+ """
213
+ Create a gap element compatible with the sequence type.
214
+
215
+ Args:
216
+ sequence: The sequence (str or bytes) to create a gap for
217
+
218
+ Returns:
219
+ The appropriate gap element for the sequence type
220
+ """
221
+ if isinstance(sequence, bytes):
222
+ return b'-'
223
+ else:
224
+ return '-'
225
 
226
+ def print_match_summary(seq1, seq2, wildcard_offsets_seq1=None, wildcard_offsets_seq2=None):
227
  """
228
+ Prints a summary of the match between two sequences, highlighting wildcards by their offsets.
229
+ Works with both strings and bytes.
230
 
231
  Args:
232
+ seq1: First sequence (str or bytes)
233
+ seq2: Second sequence (str or bytes)
234
+ wildcard_offsets_seq1 (iterable, optional): Indices in seq1 that are wildcards. Defaults to None.
235
+ wildcard_offsets_seq2 (iterable, optional): Indices in seq2 that are wildcards. Defaults to None.
236
+
237
+ Returns:
238
+ tuple: (distance, operations) The edit distance and list of operations
239
  """
240
+ # Ensure sequences are of the same type for comparison
241
+ seq1, seq2 = ensure_same_type(seq1, seq2)
242
+
243
+ # Initialize empty sets if None
244
+ wildcard_offsets_seq1 = set(wildcard_offsets_seq1 or [])
245
+ wildcard_offsets_seq2 = set(wildcard_offsets_seq2 or [])
246
 
247
  distance, operations = levenshtein_with_wildcard(
248
+ seq1, seq2, wildcard_offsets_seq1, wildcard_offsets_seq2, verbose=True
249
  )
250
 
251
+ # For reporting, convert to a human-readable representation if needed
252
+ seq1_repr = repr(seq1)
253
+ seq2_repr = repr(seq2)
 
254
 
255
+ print(f"Comparing {seq1_repr} and {seq2_repr}")
256
+ print(f"Wildcards in seq1: {sorted(wildcard_offsets_seq1)}")
257
+ print(f"Wildcards in seq2: {sorted(wildcard_offsets_seq2)}")
 
 
 
 
258
  print(f"Edit distance: {distance}")
259
  print("\nMatch process:")
260
 
 
263
 
264
  # Visual representation of the alignment
265
  i, j = 0, 0
266
+ is_bytes = isinstance(seq1, bytes)
267
+
268
+ if is_bytes:
269
+ aligned_seq1 = bytearray()
270
+ aligned_seq2 = bytearray()
271
+ gap = ord('-')
272
+ else:
273
+ aligned_seq1 = ""
274
+ aligned_seq2 = ""
275
+ gap = '-'
276
+
277
  match_indicators = ""
278
 
279
  for op in operations:
280
  if "Match:" in op or "Substitution:" in op or "Wildcard match:" in op or "Double wildcard:" in op:
281
+ if is_bytes:
282
+ aligned_seq1.append(seq1[i])
283
+ aligned_seq2.append(seq2[j])
284
+ else:
285
+ aligned_seq1 += seq1[i]
286
+ aligned_seq2 += seq2[j]
287
 
288
  # Determine match indicator
289
  if "Wildcard match:" in op or "Double wildcard:" in op:
 
296
  i += 1
297
  j += 1
298
  elif "Insertion:" in op:
299
+ if is_bytes:
300
+ aligned_seq1.append(gap)
301
+ aligned_seq2.append(seq2[j])
302
+ else:
303
+ aligned_seq1 += gap
304
+ aligned_seq2 += seq2[j]
305
 
306
  match_indicators += " "
307
  j += 1
308
  elif "Deletion:" in op:
309
+ if is_bytes:
310
+ aligned_seq1.append(seq1[i])
311
+ aligned_seq2.append(gap)
312
+ else:
313
+ aligned_seq1 += seq1[i]
314
+ aligned_seq2 += gap
315
+
316
  match_indicators += " "
317
  i += 1
318
 
319
  print("\nAlignment:")
320
+ if is_bytes:
321
+ aligned_seq1 = bytes(aligned_seq1)
322
+ aligned_seq2 = bytes(aligned_seq2)
323
+
324
+ print(repr(aligned_seq1))
325
  print(match_indicators)
326
+ print(repr(aligned_seq2))
327
  print("\nLegend:")
328
+ print("| = exact match, * = wildcard match, X = substitution, - = gap (insertion/deletion)")
329
 
330
  # Summary of wildcard matches
331
  wildcard_matches = [op for op in operations if "Wildcard match:" in op or "Double wildcard:" in op]
 
338
 
339
  # Example usage
340
  if __name__ == "__main__":
341
+ print("\n--- String Examples ---")
342
  # Example 1: "hello" vs "hello" with no wildcards
343
  print_match_summary("hello", "hello")
344
 
 
346
  print_match_summary("hello", "hallo")
347
 
348
  # Example 3: "hello" with 3rd position (index 2) as wildcard vs "hallo" - expect distance of 0
349
+ print_match_summary("hello", "hallo", wildcard_offsets_seq1=[2])
350
+
351
+ # Example 4: "hello" vs "hillo" with 2nd position (index 1) as wildcard in seq2 - expect distance of 0
352
+ print_match_summary("hello", "hillo", wildcard_offsets_seq2=[1])
353
+
354
+ # Example 5: Multiple wildcards in seq1
355
+ print_match_summary("hello", "haxyz", wildcard_offsets_seq1=[2, 3, 4])
356
+
357
+ print("\n--- Bytes Examples ---")
358
+ # Example 6: Working with bytes
359
+ print_match_summary(b"hello", b"hallo")
360
 
361
+ # Example 7: Working with bytes with wildcard
362
+ print_match_summary(b"hello", b"hallo", wildcard_offsets_seq1=[2])
363
 
364
+ # Example 8: Mixed types (bytes and string)
365
+ print_match_summary(b"hello", "hallo", wildcard_offsets_seq1=[2])
366
 
367
+ # Example 9: Non-printable bytes example
368
+ print_match_summary(b"\x01\x02\x03\x04", b"\x01\x05\x03\x04", wildcard_offsets_seq1=[1])