import re from typing import Any, Dict, List, Optional, Tuple def normalize_version(raw: Optional[str]) -> Optional[str]: if not raw: return None return raw.strip().lstrip("v") def parse_version(value: str) -> Optional[Dict[str, Any]]: match = re.match( r"^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$", value, ) if not match: return None major, minor, patch, prerelease, _build = match.groups() prerelease_parts: Optional[List[Tuple]] = None if prerelease: prerelease_parts = [] for part in prerelease.split("."): if part.isdigit(): prerelease_parts.append((True, int(part))) else: prerelease_parts.append((False, part)) return { "major": int(major), "minor": int(minor), "patch": int(patch), "prerelease": prerelease_parts, } def _compare_prerelease(a: Optional[list], b: Optional[list]) -> int: if a is None and b is None: return 0 if a is None: return -1 if b is None: return 1 for (a_is_num, a_val), (b_is_num, b_val) in zip(a, b): if a_is_num and b_is_num: if a_val != b_val: return 1 if a_val > b_val else -1 elif a_is_num != b_is_num: return -1 if a_is_num else 1 else: if a_val != b_val: return 1 if a_val > b_val else -1 if len(a) == len(b): return 0 return 1 if len(a) > len(b) else -1 def compare_versions(current: Optional[str], latest: Optional[str]) -> Optional[int]: current_norm = normalize_version(current) latest_norm = normalize_version(latest) if not current_norm or not latest_norm: return None current_v = parse_version(current_norm) latest_v = parse_version(latest_norm) if current_v is None or latest_v is None: return None c = (current_v["major"], current_v["minor"], current_v["patch"]) l = (latest_v["major"], latest_v["minor"], latest_v["patch"]) if l > c: return 1 if l < c: return -1 return _compare_prerelease(current_v["prerelease"], latest_v["prerelease"])