Skip to content

Instantly share code, notes, and snippets.

@lloesche
Created February 4, 2025 23:15
Show Gist options
  • Save lloesche/9840fd49cdca4cfab0c33bf1e911f826 to your computer and use it in GitHub Desktop.
Save lloesche/9840fd49cdca4cfab0c33bf1e911f826 to your computer and use it in GitHub Desktop.
Convert version string to int64
#!/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