Created
February 4, 2025 23:15
-
-
Save lloesche/9840fd49cdca4cfab0c33bf1e911f826 to your computer and use it in GitHub Desktop.
Convert version string to int64
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3.13 | |
import pytest | |
import sys | |
import re | |
from packaging.version import Version | |
from typing import Tuple | |
def encode_version( | |
major: int, | |
minor: int, | |
patch: int, | |
pre_release_rank: int = 0, | |
pre_release_number: int = 0, | |
major_bits: int = 16, | |
minor_bits: int = 16, | |
patch_bits: int = 16, | |
pre_release_rank_bits: int = 4, | |
pre_release_number_bits: int = 12, | |
) -> int: | |
bit_allocations = { | |
"major": major_bits, | |
"minor": minor_bits, | |
"patch": patch_bits, | |
"pre_release_rank": pre_release_rank_bits, | |
"pre_release_number": pre_release_number_bits, | |
} | |
total_bits = sum(bit_allocations.values()) | |
if total_bits > 64: | |
raise ValueError(f"Total allocated bits ({total_bits}) exceed 64!") | |
max_values = {key: (1 << bits) - 1 for key, bits in bit_allocations.items()} | |
for key, value in [ | |
("major", major), | |
("minor", minor), | |
("patch", patch), | |
("pre_release_rank", pre_release_rank), | |
("pre_release_number", pre_release_number), | |
]: | |
if not (0 <= value <= max_values[key]): | |
raise ValueError(f"{key.capitalize()} value {value} exceeds allocated {bit_allocations[key]} bits.") | |
offsets = {} | |
shift = 0 | |
for key in ["pre_release_number", "pre_release_rank", "patch", "minor", "major"]: | |
offsets[key] = shift | |
shift += bit_allocations[key] | |
version_int = ( | |
(major << offsets["major"]) | |
| (minor << offsets["minor"]) | |
| (patch << offsets["patch"]) | |
| (pre_release_rank << offsets["pre_release_rank"]) | |
| (pre_release_number << offsets["pre_release_number"]) | |
) | |
return version_int | |
def parse_version(version_str: str) -> Tuple[int, int, int, int, int, int, int, int, int, int]: | |
def _parse_date_version(version_str: str) -> Tuple[int, int, int, int, int, int, int, int, int, int] | None: | |
date_pattern = re.compile( | |
r""" | |
^(\d{4}) # YYYY | |
-?(\d{1,2})? # Optional (M)M | |
-?(\d{1,2})? # Optional (D)D | |
-?(\d{4,6})?$ # Optional HHMM(SS) | |
""", | |
re.VERBOSE, | |
) | |
match = date_pattern.match(version_str) | |
if not match: | |
return None | |
y_str, m_str, d_str, time_str = match.groups() | |
year = int(y_str) | |
month = int(m_str) if m_str else 1 | |
day = int(d_str) if d_str else 1 | |
time_val = int(time_str) if time_str else 0 | |
date_version_str = f"{year * 10000 + month * 100 + day}.{time_val}.0" | |
return _parse_as_semver(date_version_str, is_date_based=True) | |
def _parse_as_semver( | |
version_str: str, is_date_based: bool = False | |
) -> Tuple[int, int, int, int, int, int, int, int, int, int]: | |
def _parse_pre_release(v: Version) -> Tuple[int, int]: | |
pre_release_map = {"alpha": -3, "a": -3, "beta": -2, "b": -2, "rc": -1, "stable": 0, "post": 1} | |
pre_release_rank = 0 # stable = default | |
pre_release_number = 0 | |
if v.pre: | |
pre_key, pre_num = v.pre | |
pre_release_rank = pre_release_map.get(pre_key.casefold(), -4) | |
pre_release_number = pre_num if pre_num is not None else 0 | |
elif v.post is not None: | |
pre_release_rank = pre_release_map["post"] | |
pre_release_number = v.post | |
return pre_release_rank + 3, pre_release_number | |
v = Version(version_str) | |
major = v.major | |
minor = v.minor | |
patch = v.micro if v.micro is not None else 0 | |
pre_release_rank, pre_release_number = _parse_pre_release(v) | |
if is_date_based: | |
return (major, minor, 0, pre_release_rank, pre_release_number, 30, 18, 0, 4, 12) | |
return (major, minor, patch, pre_release_rank, pre_release_number, 16, 16, 16, 4, 12) | |
if parsed_version := _parse_date_version(version_str): | |
return parsed_version | |
return _parse_as_semver(version_str) | |
def test_encode_version(): | |
assert encode_version(*parse_version("1.2.3")) > encode_version(*parse_version("1.2.0")) | |
assert encode_version(*parse_version("15.2")) > encode_version(*parse_version("8.1")) | |
assert encode_version(*parse_version("2.0.0")) > encode_version(*parse_version("1.9.9")) | |
assert encode_version(*parse_version("2.0.0")) > encode_version(*parse_version("1.9.555")) | |
assert encode_version(*parse_version("3.0.0")) > encode_version(*parse_version("1.3.37")) | |
assert encode_version(*parse_version("1.2.3")) > encode_version(*parse_version("1.2.3-alpha1")) | |
assert encode_version(*parse_version("1.2.3-beta2")) > encode_version(*parse_version("1.2.3-alpha1")) | |
assert encode_version(*parse_version("1.2.3-rc4")) > encode_version(*parse_version("1.2.3-beta2")) | |
assert encode_version(*parse_version("1.2.3")) > encode_version(*parse_version("1.2.3-rc4")) | |
assert encode_version(*parse_version("1.2.3")) == encode_version(*parse_version("1.2.3")) | |
assert encode_version(*parse_version("1.2.3.post1")) > encode_version(*parse_version("1.2.3")) | |
assert encode_version(*parse_version("2025-02-04")) > encode_version(*parse_version("2025-02-03")) | |
assert encode_version(*parse_version("20250204")) > encode_version(*parse_version("20250203")) | |
assert encode_version(*parse_version("2025-02-04-1230")) > encode_version(*parse_version("2025-02-04-1200")) | |
assert encode_version(*parse_version("2025-02-04-123000")) > encode_version(*parse_version("2025-02-04-120000")) | |
assert encode_version(*parse_version("2025-02-04-123001")) > encode_version(*parse_version("2025-02-04-123000")) | |
assert encode_version(*parse_version("202502041230")) > encode_version(*parse_version("202502041129")) | |
assert encode_version(*parse_version("2025-02-04-9999")) > encode_version(*parse_version("2025-02-04-1231")) | |
assert encode_version(*parse_version("01.02.03")) == encode_version(*parse_version("1.2.3")) | |
assert encode_version(*parse_version("v1.2.3")) == encode_version(*parse_version("1.2.3")) | |
assert encode_version(*parse_version("2025-02")) < encode_version(*parse_version("2025-03")) | |
assert encode_version(*parse_version("202502")) < encode_version(*parse_version("202503")) | |
encode_version(*parse_version("65535.0.0")) | |
encode_version(*parse_version("1.65535.0")) | |
encode_version(*parse_version("1.2.65535")) | |
with pytest.raises(ValueError, match="exceeds allocated"): | |
encode_version(*parse_version("999999.0.0")) | |
with pytest.raises(ValueError, match="exceeds allocated"): | |
encode_version(*parse_version("1.999999.0")) | |
with pytest.raises(ValueError, match="exceeds allocated"): | |
encode_version(*parse_version("1.2.999999")) | |
with pytest.raises(ValueError, match="exceeds allocated"): | |
encode_version(*parse_version("1.2.3-alpha999999")) | |
print("All tests passed!") | |
if __name__ == "__main__": | |
if len(sys.argv) == 1: | |
test_encode_version() | |
else: | |
version_str = sys.argv[1] | |
encoded_version = encode_version(*parse_version(version_str)) | |
print(f"{version_str}: {encoded_version}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment