Skip to content

Instantly share code, notes, and snippets.

@victorusachev
Created May 25, 2021 07:30
Show Gist options
  • Save victorusachev/95d1a99167a987c1e61e85a392c6a4a9 to your computer and use it in GitHub Desktop.
Save victorusachev/95d1a99167a987c1e61e85a392c6a4a9 to your computer and use it in GitHub Desktop.
Implement the conversion of the dotted version string to an integer and reverse conversion. This approach can be used to compactly store large amounts of version data.
"""
Implement the conversion of the dotted version string to an integer and reverse conversion.
This approach can be used to compactly store large amounts of version data.
"""
import struct
from typing import List, Optional
import pytest
UNSIGNED_SHORT_FORMAT = 'H' # 0 <= number <= 65535
UNSIGNED_SHORT_MAX_NUMBER = 65_535
def _int_to_bytes(number: int, signed=False) -> bytes:
if not signed and number < 0:
raise ValueError
length = (8 + (number + (number < 0)).bit_length()) // 8
_bytes = int.to_bytes(number, length=length, byteorder='big', signed=signed)
return _bytes
def _int_from_bytes(binary_data: bytes, signed=False) -> Optional[int]:
return int.from_bytes(binary_data, byteorder='big', signed=signed)
def _list_int_to_bytes(version_list: List[int]) -> bytes:
try:
version_bytes = struct.pack('<' + UNSIGNED_SHORT_FORMAT * len(version_list), *version_list)
except struct.error as exc:
raise ValueError from exc
return version_bytes
def _bytes_to_list_int(binary_data: bytes) -> List[int]:
version_list = [x[0] for x in struct.iter_unpack('<' + UNSIGNED_SHORT_FORMAT, binary_data)]
return version_list
def _list_int_to_dotted(version_list: List[int]) -> str:
dotted_version = '.'.join(map(str, version_list))
return dotted_version
def _dotted_to_list_int(dotted_version: str) -> List[int]:
version_list = list(map(int, dotted_version.split('.')))
return version_list
def encode_version_to_number(dotted_version: Optional[str]) -> Optional[int]:
if not dotted_version:
return None
version_list = _dotted_to_list_int(dotted_version)
version_list = [min(x, UNSIGNED_SHORT_MAX_NUMBER) for x in version_list]
version_list.insert(0, 1) # an additional value is needed to keep the leading zero
version_bytes = _list_int_to_bytes(version_list)
version_int = _int_from_bytes(version_bytes)
return version_int
def decode_version_from_number(endcoded_version: Optional[int]) -> Optional[str]:
if endcoded_version is None:
return None
version_bytes = _int_to_bytes(endcoded_version)
version_list = _bytes_to_list_int(version_bytes)
version_list.pop(0) # an additional value is needed to keep the leading zero
dotted_version = _list_int_to_dotted(version_list)
return dotted_version
@pytest.mark.parametrize(
'version',
[
None,
'0',
'10',
'65535',
'0.1',
'0.0.0',
'0.1.0',
'14.4.2',
'87.0.4280.77',
'14.0.3.4',
'90.0.4430.212',
'90.0.818.66',
'0.1.2.3.65535.5',
]
)
def test_coding_versions(version: str) -> None:
encoded_version = encode_version_to_number(version)
decoded_version = decode_version_from_number(encoded_version)
assert decoded_version == version
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment