taler-deployment

Deployment scripts and configuration files
Log | Files | Refs | README

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