vercomp.py (5962B)
1 import re 2 3 def order(char: str) -> int: 4 """ 5 Determines the sort weight of a character in the non-digit part of a version. 6 7 According to deb-version(5): 8 1. Tilde (~) sorts before everything. 9 2. The end of a part sorts before anything else (except tilde). 10 3. Letters sort earlier than non-letters (symbols). 11 """ 12 if char == '~': 13 return -1 14 if char == '' or char is None: 15 return 0 16 if char.isalpha(): 17 return ord(char) 18 # Map non-alphanumeric symbols (other than ~) higher than letters 19 # Standard ASCII letters are < 128, so adding 256 pushes symbols above them. 20 return ord(char) + 256 21 22 def compare_string_portions(val_a: str, val_b: str) -> int: 23 """ 24 Compares two string portions (upstream or debian revision) using the 25 algorithm described in deb-version(5). 26 """ 27 # Use lists as mutable cursors 28 list_a = list(val_a) 29 list_b = list(val_b) 30 31 while list_a or list_b: 32 # --- 1. Compare non-digit initial parts --- 33 34 # Extract non-digit prefixes 35 non_digit_a = [] 36 while list_a and not list_a[0].isdigit(): 37 non_digit_a.append(list_a.pop(0)) 38 39 non_digit_b = [] 40 while list_b and not list_b[0].isdigit(): 41 non_digit_b.append(list_b.pop(0)) 42 43 # Compare them character by character 44 # We loop until we exhaust the longest of the two non-digit strings 45 # (effectively padding the shorter one with empty strings/None) 46 len_cmp = max(len(non_digit_a), len(non_digit_b)) 47 48 for i in range(len_cmp): 49 # Get chars or None if exhausted 50 ca = non_digit_a[i] if i < len(non_digit_a) else None 51 cb = non_digit_b[i] if i < len(non_digit_b) else None 52 53 wa = order(ca) 54 wb = order(cb) 55 56 if wa < wb: return -1 57 if wa > wb: return 1 58 59 # --- 2. Compare numerical parts --- 60 61 # Extract digit prefixes 62 digit_a_str = [] 63 while list_a and list_a[0].isdigit(): 64 digit_a_str.append(list_a.pop(0)) 65 66 digit_b_str = [] 67 while list_b and list_b[0].isdigit(): 68 digit_b_str.append(list_b.pop(0)) 69 70 # Convert to int (empty string defaults to 0) 71 num_a = int("".join(digit_a_str)) if digit_a_str else 0 72 num_b = int("".join(digit_b_str)) if digit_b_str else 0 73 74 if num_a < num_b: return -1 75 if num_a > num_b: return 1 76 77 return 0 78 79 def parse_version(version: str): 80 """ 81 Parses a version string into (epoch, upstream, debian). 82 Defaults: epoch=0, debian='0' if missing. 83 """ 84 epoch = 0 85 upstream = version 86 debian = "0" 87 88 # 1. Parse Epoch (last colon wins? No, first colon) 89 # deb-version: "The epoch is a single (generally small) unsigned integer 90 # and may be omitted. If it is omitted then the upstream_version may 91 # not contain any colons." 92 if ':' in version: 93 epoch_str, remainder = version.split(':', 1) 94 if epoch_str.isdigit(): 95 epoch = int(epoch_str) 96 upstream = remainder 97 98 # 2. Parse Debian Revision (last hyphen splits upstream and debian) 99 # deb-version: "If there is no debian_revision then hyphens are not 100 # allowed [in upstream]; if there is no debian_revision then the 101 # upstream_version is the whole version string." 102 r_hyphen_index = upstream.rfind('-') 103 if r_hyphen_index != -1: 104 # Found a hyphen, split 105 debian = upstream[r_hyphen_index + 1:] 106 upstream = upstream[:r_hyphen_index] 107 108 return epoch, upstream, debian 109 110 def compare_versions(ver_a: str, ver_b: str) -> int: 111 """ 112 Compare two Debian version strings. 113 114 Returns: 115 -1 if ver_a < ver_b 116 0 if ver_a == ver_b 117 1 if ver_a > ver_b 118 """ 119 # 1. Parse 120 epoch_a, up_a, deb_a = parse_version(ver_a) 121 epoch_b, up_b, deb_b = parse_version(ver_b) 122 123 # 2. Compare Epochs (Integers) 124 if epoch_a < epoch_b: return -1 125 if epoch_a > epoch_b: return 1 126 127 # 3. Compare Upstream Versions (String Logic) 128 res = compare_string_portions(up_a, up_b) 129 if res != 0: return res 130 131 # 4. Compare Debian Revisions (String Logic) 132 return compare_string_portions(deb_a, deb_b) 133 134 # --- Tests and Examples --- 135 if __name__ == "__main__": 136 test_cases = [ 137 # (Version A, Version B, Operator Expected) 138 # 1.0 < 1.1 139 ("1.0", "1.1", "<"), 140 # 1.0-1 < 1.0-2 141 ("1.0-1", "1.0-2", "<"), 142 # Tilde logic: 1.0~rc1 < 1.0 143 ("1.0~rc1", "1.0", "<"), 144 # Tilde sorts before empty string (end of chunk) 145 # "1.0" parses as 1, ., 0, (end) 146 # "1.0~" parses as 1, ., 0, ~ 147 # ~ < (end), so 1.0~ < 1.0 148 ("1.0~", "1.0", "<"), 149 # Letter vs Symbol logic: 1.0a < 1.0+ 150 # 'a' (letter) sorts earlier than '+' (symbol) 151 ("1.0a", "1.0+", "<"), 152 # Epoch logic 153 ("1:1.0", "1.0", ">"), 154 # Digit comparison: 1.0 == 1.00 155 ("1.0", "1.00", "="), 156 # Parsing split logic 157 ("1.2.3-1", "1.2.3-1", "="), 158 ("1.2.3-alpha", "1.2.3-beta", "<"), 159 # Tilde 160 ("1.1.2~trixie", "1.1.2", "<"), 161 ] 162 163 print(f"{'Version A':<15} {'Op':<5} {'Version B':<15} {'Result':<10}") 164 print("-" * 50) 165 166 for va, vb, expected_op in test_cases: 167 result = compare_versions(va, vb) 168 169 op_map = { -1: "<", 0: "=", 1: ">" } 170 actual_op = op_map[result] 171 172 # Check correctness 173 passed = False 174 if expected_op == "<" and result == -1: passed = True 175 elif expected_op == ">" and result == 1: passed = True 176 elif expected_op == "=" and result == 0: passed = True 177 178 status = "PASS" if passed else "FAIL" 179 print(f"{va:<15} {actual_op:<5} {vb:<15} {status}")