Initial commit. Needs more test coverage.
This commit is contained in:
commit
bd2cc20110
17 changed files with 2903 additions and 0 deletions
163
.gitignore
vendored
Normal file
163
.gitignore
vendored
Normal file
|
@ -0,0 +1,163 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Dev settings
|
||||
.vscode/
|
0
README.md
Normal file
0
README.md
Normal file
27
contrib/example.py
Normal file
27
contrib/example.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
"""
|
||||
An example showing off the current D'Ni time and it's conversion back to an
|
||||
Earth datetime.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import sys
|
||||
from time import sleep
|
||||
|
||||
from dnidatetime import DniDatetime
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
while True:
|
||||
try:
|
||||
earth_now = datetime.now().astimezone()
|
||||
dni_now = DniDatetime.from_earth(earth_now)
|
||||
back = dni_now.to_earth().astimezone()
|
||||
print(
|
||||
f"{earth_now} --> {dni_now} --> {back}",
|
||||
end=" \r",
|
||||
flush=True,
|
||||
)
|
||||
sleep(0.01)
|
||||
except KeyboardInterrupt:
|
||||
print("\n")
|
||||
sys.exit()
|
2
mypy.ini
Normal file
2
mypy.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[mypy]
|
||||
mypy_path = $MYPY_CONFIG_FILE_DIR/src
|
1044
poetry.lock
generated
Normal file
1044
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
28
pyproject.toml
Normal file
28
pyproject.toml
Normal file
|
@ -0,0 +1,28 @@
|
|||
[tool.poetry]
|
||||
name = "dnidatetime"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Josh Washburne <josh@jodh.us>"]
|
||||
readme = "README.md"
|
||||
packages = [{include = "dnidatetime", from = "src"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pylint = "^3.1.0"
|
||||
mypy = "^1.9.0"
|
||||
black = "^24.3.0"
|
||||
pytest = "^8.1.1"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs = "^1.5.3"
|
||||
mkdocstrings = {extras = ["python"], version = "^0.24.1"}
|
||||
mkdocs-material = "^9.5.14"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = "src"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
7
src/dnidatetime/__init__.py
Normal file
7
src/dnidatetime/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from .dnitimedelta import DniTimedelta
|
||||
from .dnidate import DniDate
|
||||
from .dnitime import DniTime
|
||||
from .dnidatetime import DniDatetime
|
||||
|
||||
|
||||
__all__ = ["DniDate", "DniDatetime", "DniTime", "DniTimedelta"]
|
120
src/dnidatetime/constants.py
Normal file
120
src/dnidatetime/constants.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
"""Constants for all classes and functions with D'Ni datetimes."""
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timezone
|
||||
from typing import Tuple, TypedDict
|
||||
|
||||
|
||||
DniDateTimeTuple = Tuple[int, int, int, int, int, int, int]
|
||||
|
||||
|
||||
class DniUnitType(TypedDict):
|
||||
"""Stores D'Ni units of time. Used for type-checking purposes."""
|
||||
|
||||
min: int
|
||||
max: int
|
||||
total_pro: int
|
||||
|
||||
|
||||
# All the units of time in the D'Ni culture from largest to smallest.
|
||||
#
|
||||
# Hahr - The D'Ni eqivalent of an earth year.
|
||||
# Vailee - The D'Ni eqivalent of an earth month, 10 total in each hahr.
|
||||
# Yahr - The D'Ni equivalent of an earth day, 29 total in each vailee.
|
||||
# Gahrtahvo - 5 total in each yahr.
|
||||
# Pahrtahvo - The D'Ni equivalent of an earth hour, 5 total in each gahrtahvo
|
||||
# and 25 total in each yahr. Not used in calculations, but can
|
||||
# be used to show a different method of D'Ni time.
|
||||
# Tahvo - 5 total in each pahrtahvo and 25 total in each gahrtahvo.
|
||||
# Gorahn - 25 total in each tahvo.
|
||||
# Prorahn - The D'Ni equivalent of an earth second, 25 total in each gorahn.
|
||||
#
|
||||
# Limit: Maximum amount before rollover (ie. 60 seconds in 1 minute)
|
||||
# Total Prorahntee: A "prorahn" is the smallest unit of time and is
|
||||
# used in the calcuation of the other units.
|
||||
DNI_UNITS: OrderedDict[str, DniUnitType] = OrderedDict(
|
||||
[
|
||||
("hahr", {"min": 7654, "max": 17655, "total_pro": 22656250}),
|
||||
("vailee", {"min": 1, "max": 10, "total_pro": 2265625}),
|
||||
("yahr", {"min": 1, "max": 29, "total_pro": 78125}),
|
||||
("gahrtahvo", {"min": 0, "max": 5, "total_pro": 15625}),
|
||||
("tahvo", {"min": 0, "max": 25, "total_pro": 625}),
|
||||
("gorahn", {"min": 0, "max": 25, "total_pro": 25}),
|
||||
("prorahn", {"min": 0, "max": 25, "total_pro": 1}),
|
||||
]
|
||||
)
|
||||
|
||||
# A hahr's length is equal to the Mean Solar Tropical Year for 1995 in
|
||||
# milliseconds.
|
||||
MS_PER_HAHR = 31556925216
|
||||
|
||||
# A prorahn's length in milliseconds.
|
||||
# Some previous [wrong] calculations:
|
||||
# JS = 1392.8573857142859
|
||||
# Python = 1392.8573888441379
|
||||
MS_PER_PRORAHN = 1392.85737275
|
||||
|
||||
# Just as the UNIX Epoch is based on midnight on 1-1-1970, the D'Ni Epoch
|
||||
# is based on the timestamp of the original MYST Hypercard file:
|
||||
# April 21, 1991 9:54AM PST (16:54 UTC) --> Leefo 1, 9647 00:00:00:00
|
||||
EARTH_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
DNI_EPOCH_EDT = datetime(1991, 4, 21, 16, 54, 0, tzinfo=timezone.utc) # Earth
|
||||
DNI_EPOCH_HAHR = 9647
|
||||
|
||||
# Converted leap seconds to ms, but adjusted for the UNIX epoch instead.
|
||||
# https://data.iana.org/time-zones/data/leap-seconds.list
|
||||
# delta_ms_1900_to_1970 = -2208988800000
|
||||
# leap_ms = leap_sec_from_ietf * 1000 + delta_ms_1900_to_1970
|
||||
LEAP_MS_FROM_EPOCH = [
|
||||
63072000000, # 1 Jan 1972
|
||||
78796800000, # 1 Jul 1972
|
||||
94694400000, # 1 Jan 1973
|
||||
126230400000, # 1 Jan 1974
|
||||
157766400000, # 1 Jan 1975
|
||||
189302400000, # 1 Jan 1976
|
||||
220924800000, # 1 Jan 1977
|
||||
252460800000, # 1 Jan 1978
|
||||
283996800000, # 1 Jan 1979
|
||||
315532800000, # 1 Jan 1980
|
||||
362793600000, # 1 Jul 1981
|
||||
394329600000, # 1 Jul 1982
|
||||
425865600000, # 1 Jul 1983
|
||||
489024000000, # 1 Jul 1985
|
||||
567993600000, # 1 Jan 1988
|
||||
631152000000, # 1 Jan 1990
|
||||
662688000000, # 1 Jan 1991
|
||||
709948800000, # 1 Jul 1992
|
||||
741484800000, # 1 Jul 1993
|
||||
773020800000, # 1 Jul 1994
|
||||
820454400000, # 1 Jan 1996
|
||||
867715200000, # 1 Jul 1997
|
||||
915148800000, # 1 Jan 1999
|
||||
1136073600000, # 1 Jan 2006
|
||||
1230768000000, # 1 Jan 2009
|
||||
1341100800000, # 1 Jul 2012
|
||||
1435708800000, # 1 Jul 2015
|
||||
1483228800000, # 1 Jan 2017
|
||||
]
|
||||
|
||||
# DniDate.max.to_ordinal()
|
||||
# (For whatever reason, 5119863 was the former value and I can't figure why.
|
||||
# Noting it here in case I need it for something.)
|
||||
MIN_DNI_ORDINAL = 1
|
||||
MAX_DNI_ORDINAL = 2900494
|
||||
|
||||
# The names of the 10 vailee in a hahr.
|
||||
VAILEE_NAMES = [
|
||||
"Leefo",
|
||||
"Leebro",
|
||||
"Leesahn",
|
||||
"Leetar",
|
||||
"Leevot",
|
||||
"Leevofo",
|
||||
"Leevobro",
|
||||
"Leevosahn",
|
||||
"Leevotar",
|
||||
"Leenovoo",
|
||||
]
|
||||
|
||||
# The number of yahrtee (days) before each consectutive vailee (month).
|
||||
YAHRTEE_BEFORE_VAILEE = [-1, 0, 29, 58, 87, 116, 145, 174, 203, 232, 261]
|
152
src/dnidatetime/conversions.py
Normal file
152
src/dnidatetime/conversions.py
Normal file
|
@ -0,0 +1,152 @@
|
|||
"""Conversion functions for Earth/D'Ni datetimes & Ordinal/Julian dates."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from math import floor
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from .constants import (
|
||||
DniDateTimeTuple,
|
||||
DNI_EPOCH_EDT,
|
||||
DNI_EPOCH_HAHR,
|
||||
DNI_UNITS,
|
||||
MAX_DNI_ORDINAL,
|
||||
MIN_DNI_ORDINAL,
|
||||
MS_PER_HAHR,
|
||||
MS_PER_PRORAHN,
|
||||
YAHRTEE_BEFORE_VAILEE
|
||||
)
|
||||
|
||||
from .utils import (
|
||||
check_dni_date_fields,
|
||||
get_adj_ms_from_epoch,
|
||||
get_ms_from_epoch,
|
||||
is_timezone_aware
|
||||
)
|
||||
|
||||
|
||||
# Number of yahrtee (days) in a hahr (year) == 290.
|
||||
_YAHR_IN_HAHR = DNI_UNITS["yahr"]["max"] * DNI_UNITS["vailee"]["max"]
|
||||
|
||||
|
||||
def hvy2ord(hahr: int, vailee: int, yahr: int) -> int:
|
||||
"""The amount of yahrtee since Leefo 1, 7654 (DniDate.min)."""
|
||||
ok_hahr, ok_vailee, ok_yahr = check_dni_date_fields(hahr, vailee, yahr)
|
||||
yahr_before_hahr = (ok_hahr - DNI_UNITS["hahr"]["min"]) * _YAHR_IN_HAHR
|
||||
return yahr_before_hahr + YAHRTEE_BEFORE_VAILEE[ok_vailee] + ok_yahr
|
||||
|
||||
|
||||
def ord2hvy(ordinal: int) -> Tuple[int, int, int]:
|
||||
"""Find the exact hahr, vailee, and yahr from an ordinal value of
|
||||
yahrtee.
|
||||
"""
|
||||
if not MIN_DNI_ORDINAL <= ordinal <= MAX_DNI_ORDINAL:
|
||||
raise ValueError(
|
||||
f"Ordinal must be in {MIN_DNI_ORDINAL}..{MAX_DNI_ORDINAL}",
|
||||
ordinal,
|
||||
)
|
||||
# TODO: I hate this with a firey passion, but it works. Needs a revamp at
|
||||
# some point.
|
||||
hahr, rem = divmod(ordinal, _YAHR_IN_HAHR)
|
||||
hahr += DNI_UNITS["hahr"]["min"]
|
||||
if rem == 0:
|
||||
hahr -= 1
|
||||
vailee = DNI_UNITS["vailee"]["max"]
|
||||
yahr = DNI_UNITS["yahr"]["max"]
|
||||
else:
|
||||
vailee, yahr = divmod(
|
||||
rem + DNI_UNITS["yahr"]["max"],
|
||||
DNI_UNITS["yahr"]["max"]
|
||||
)
|
||||
if yahr == 0:
|
||||
vailee -= 1
|
||||
yahr = DNI_UNITS["yahr"]["max"]
|
||||
return hahr, vailee, yahr
|
||||
|
||||
|
||||
def earth_to_dni(
|
||||
date_time: Optional[datetime] = None
|
||||
) -> DniDateTimeTuple:
|
||||
"""Converts an Earth gregorian datetime to a D'Ni datetime."""
|
||||
if date_time is None:
|
||||
moment = datetime.now(timezone.utc)
|
||||
else:
|
||||
if is_timezone_aware(date_time):
|
||||
moment = date_time.astimezone(timezone.utc)
|
||||
else:
|
||||
raise ValueError("Supplied datetime must be timezone aware.")
|
||||
|
||||
dni_dt: Dict[str, int] = {}
|
||||
|
||||
stamp = get_adj_ms_from_epoch(moment)
|
||||
dni_epoch = get_adj_ms_from_epoch(DNI_EPOCH_EDT)
|
||||
delta = float(stamp - dni_epoch)
|
||||
|
||||
for name, details in DNI_UNITS.items():
|
||||
if name == "hahr":
|
||||
dni_dt[name] = floor(delta / MS_PER_HAHR)
|
||||
delta -= dni_dt[name] * MS_PER_HAHR
|
||||
delta *= details["total_pro"] / MS_PER_HAHR
|
||||
else:
|
||||
dni_dt[name] = floor(delta / details["total_pro"])
|
||||
delta -= dni_dt[name] * details["total_pro"]
|
||||
|
||||
# Correct potential value underflow (dates before the reference date)
|
||||
temp_units = list(DNI_UNITS.items())
|
||||
rev_index = len(temp_units) - 1
|
||||
for index, unit in enumerate(reversed(temp_units)):
|
||||
if dni_dt[unit[0]] < 0:
|
||||
dni_dt[unit[0]] += unit[1]["max"]
|
||||
dni_dt[temp_units[rev_index - index][0]] -= 1
|
||||
|
||||
# Add reference D'ni hahr
|
||||
dni_dt["hahr"] += DNI_EPOCH_HAHR
|
||||
|
||||
# Vailee and Yahr are not 0-based
|
||||
dni_dt["vailee"] += 1
|
||||
dni_dt["yahr"] += 1
|
||||
|
||||
return (
|
||||
dni_dt["hahr"],
|
||||
dni_dt["vailee"],
|
||||
dni_dt["yahr"],
|
||||
dni_dt["gahrtahvo"],
|
||||
dni_dt["tahvo"],
|
||||
dni_dt["gorahn"],
|
||||
dni_dt["prorahn"],
|
||||
)
|
||||
|
||||
|
||||
def dni_to_earth(dni_date_time: DniDateTimeTuple) -> datetime:
|
||||
"""Converts a D'Ni datetime to an Earth gregorian datetime."""
|
||||
|
||||
if not isinstance(dni_date_time, tuple):
|
||||
raise ValueError(
|
||||
(
|
||||
"D'Ni datetime must be in the form of a timetuple. "
|
||||
"See DniDateTime.timetuple()."
|
||||
),
|
||||
dni_date_time
|
||||
)
|
||||
|
||||
if len(dni_date_time) != 7:
|
||||
raise ValueError(
|
||||
"The D'Ni timetuple is malformed. See DniDateTime.timetuple().",
|
||||
dni_date_time
|
||||
)
|
||||
|
||||
temp_units = list(dni_date_time)
|
||||
|
||||
temp_units[0] -= DNI_EPOCH_HAHR
|
||||
temp_units[1] -= 1
|
||||
temp_units[2] -= 1
|
||||
|
||||
total_prorahn = 0
|
||||
|
||||
for index, unit in enumerate(DNI_UNITS.items()):
|
||||
total_prorahn += temp_units[index] * unit[1]["total_pro"]
|
||||
|
||||
timestamp = total_prorahn * MS_PER_PRORAHN
|
||||
epoch = get_ms_from_epoch(DNI_EPOCH_EDT)
|
||||
date = timestamp + epoch
|
||||
|
||||
return datetime.fromtimestamp(date / 1000.0, timezone.utc)
|
183
src/dnidatetime/dnidate.py
Normal file
183
src/dnidatetime/dnidate.py
Normal file
|
@ -0,0 +1,183 @@
|
|||
"""The DniDate class."""
|
||||
|
||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||
|
||||
from .constants import DniDateTimeTuple, DNI_UNITS, MAX_DNI_ORDINAL
|
||||
from .conversions import hvy2ord, ord2hvy
|
||||
from .dnitimedelta import DniTimedelta
|
||||
from .utils import check_dni_date_fields, cmp
|
||||
|
||||
|
||||
class DniDate:
|
||||
"""A D'Ni date."""
|
||||
|
||||
__slots__ = "_hahr", "_vailee", "_yahr"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_hahr: int
|
||||
_vailee: int
|
||||
_yahr: int
|
||||
|
||||
# Class properties
|
||||
min: "DniDate"
|
||||
max: "DniDate"
|
||||
resolution: DniTimedelta
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
hahr: int = DNI_UNITS["hahr"]["min"],
|
||||
vailee: int = DNI_UNITS["vailee"]["min"],
|
||||
yahr: int = DNI_UNITS["yahr"]["min"],
|
||||
) -> "DniDate":
|
||||
if isinstance(hahr, int):
|
||||
hahr, vailee, yahr = check_dni_date_fields(hahr, vailee, yahr)
|
||||
self = object.__new__(cls)
|
||||
self._hahr = hahr
|
||||
self._vailee = vailee
|
||||
self._yahr = yahr
|
||||
return self
|
||||
return NotImplemented
|
||||
|
||||
@classmethod
|
||||
def from_dnitimestamp(cls, prorahntee: int) -> "DniDate":
|
||||
"""Construct a D'Ni date from a D'Ni timestamp."""
|
||||
yahrtee = prorahntee // DNI_UNITS["yahr"]["total_pro"]
|
||||
return cls.from_ordinal(yahrtee)
|
||||
|
||||
@classmethod
|
||||
def today(cls) -> "DniDate":
|
||||
# TODO
|
||||
return NotImplemented
|
||||
|
||||
@classmethod
|
||||
def from_isoformat(cls, date_str: str) -> "DniDate":
|
||||
if not isinstance(date_str, str):
|
||||
raise TypeError("from_isoformat: argument must be str.")
|
||||
|
||||
try:
|
||||
assert 8 <= len(date_str) <= 11
|
||||
parts = date_str.split("-")
|
||||
if len(parts) != 3:
|
||||
raise ValueError(f"Invalid isoformat string: {date_str!r}")
|
||||
return cls(int(parts[0]), int(parts[1]), int(parts[2]))
|
||||
except Exception as exc:
|
||||
raise ValueError(f"Invalid isoformat string: {date_str!r}") from exc
|
||||
|
||||
@classmethod
|
||||
def from_ordinal(cls, yahrtee: int) -> "DniDate":
|
||||
"""Construct a D'ni date from a D'ni ordinal."""
|
||||
hahr, vailee, yahr = ord2hvy(yahrtee)
|
||||
return cls(hahr, vailee, yahr)
|
||||
|
||||
def to_ordinal(self) -> int:
|
||||
"""Return a D'ni ordinal for the hahr, vailee, and yahr."""
|
||||
return hvy2ord(self._hahr, self._vailee, self._yahr)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"{self.__class__.__qualname__}"
|
||||
f"({self._hahr}, "
|
||||
f"{self._vailee}, "
|
||||
f"{self._yahr})"
|
||||
)
|
||||
|
||||
def isoformat(self) -> str:
|
||||
"""Return the date formatted according to ISO standard."""
|
||||
return f"{self._hahr}-{self._vailee:02}-{self._yahr:02}"
|
||||
|
||||
__str__ = isoformat
|
||||
|
||||
# Read-only field accessors
|
||||
@property
|
||||
def hahr(self) -> int:
|
||||
"""hahr"""
|
||||
return self._hahr
|
||||
|
||||
@property
|
||||
def vailee(self) -> int:
|
||||
"""vailee"""
|
||||
return self._vailee
|
||||
|
||||
@property
|
||||
def yahr(self) -> int:
|
||||
"""yahr"""
|
||||
return self._yahr
|
||||
|
||||
def replace(
|
||||
self,
|
||||
hahr: Optional[int] = None,
|
||||
vailee: Optional[int] = None,
|
||||
yahr: Optional[int] = None,
|
||||
) -> "DniDate":
|
||||
"""Return a new D'ni date with new values for the specified fields."""
|
||||
if hahr is None:
|
||||
hahr = self._hahr
|
||||
if vailee is None:
|
||||
vailee = self._vailee
|
||||
if yahr is None:
|
||||
yahr = self._yahr
|
||||
return type(self)(hahr, vailee, yahr)
|
||||
|
||||
# Arithmetic
|
||||
def __add__(self, other: Any) -> "DniDate":
|
||||
if isinstance(other, DniTimedelta):
|
||||
ordinal = self.to_ordinal() + other._yahrtee
|
||||
if 0 < ordinal <= MAX_DNI_ORDINAL:
|
||||
return type(self).from_ordinal(ordinal)
|
||||
raise OverflowError("Result out of range.")
|
||||
return NotImplemented
|
||||
|
||||
__radd__ = __add__
|
||||
|
||||
def __sub__(self, other: Any) -> Union["DniDate", DniTimedelta]:
|
||||
if isinstance(other, DniTimedelta):
|
||||
return self + DniTimedelta(-other._yahrtee)
|
||||
if isinstance(other, DniDate):
|
||||
yahrtee1 = self.to_ordinal()
|
||||
yahrtee2 = other.to_ordinal()
|
||||
return DniTimedelta(yahrtee1 - yahrtee2)
|
||||
return NotImplemented
|
||||
|
||||
# Rich comparisons
|
||||
def _cmp(self, other: Any) -> int:
|
||||
"""Helper function for rich comparison of DniDate objects."""
|
||||
# pylint: disable=protected-access
|
||||
assert isinstance(other, DniDate)
|
||||
hahr1, vailee1, yahr1 = self._hahr, self._vailee, self._yahr
|
||||
hahr2, vailee2, yahr2 = other._hahr, other._vailee, other._yahr
|
||||
return cmp((hahr1, vailee1, yahr1), (hahr2, vailee2, yahr2))
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniDate):
|
||||
return self._cmp(other) == 0
|
||||
return NotImplemented
|
||||
|
||||
def __le__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniDate):
|
||||
return self._cmp(other) <= 0
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniDate):
|
||||
return self._cmp(other) < 0
|
||||
return NotImplemented
|
||||
|
||||
def __ge__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniDate):
|
||||
return self._cmp(other) >= 0
|
||||
return NotImplemented
|
||||
|
||||
def __gt__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniDate):
|
||||
return self._cmp(other) > 0
|
||||
return NotImplemented
|
||||
|
||||
def dnitimetuple(self) -> DniDateTimeTuple:
|
||||
"""Local time tuple compatible with dnitime.localtime()."""
|
||||
# TODO
|
||||
return NotImplemented
|
||||
|
||||
|
||||
DniDate.min = DniDate(7654, 1, 1)
|
||||
DniDate.max = DniDate(17655, 8, 1)
|
||||
DniDate.resolution = DniTimedelta(yahrtee=1)
|
319
src/dnidatetime/dnidatetime.py
Normal file
319
src/dnidatetime/dnidatetime.py
Normal file
|
@ -0,0 +1,319 @@
|
|||
"""The DniDatetime class."""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||
|
||||
from .constants import (
|
||||
DniDateTimeTuple,
|
||||
DNI_EPOCH_HAHR,
|
||||
DNI_UNITS,
|
||||
MAX_DNI_ORDINAL
|
||||
)
|
||||
from .conversions import dni_to_earth, earth_to_dni, hvy2ord
|
||||
from .dnidate import DniDate
|
||||
from .dnitime import DniTime
|
||||
from .dnitimedelta import DniTimedelta
|
||||
from .utils import check_dni_date_fields, check_dni_time_fields, cmp
|
||||
|
||||
|
||||
class DniDatetime:
|
||||
"""A D'ni timestamp."""
|
||||
|
||||
__slots__ = DniDate.__slots__ + DniTime.__slots__
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_hahr: int
|
||||
_vailee: int
|
||||
_yahr: int
|
||||
_gahrtahvo: int
|
||||
_tahvo: int
|
||||
_gorahn: int
|
||||
_prorahn: int
|
||||
|
||||
# Class properties
|
||||
min: "DniDatetime"
|
||||
max: "DniDatetime"
|
||||
resolution: DniTimedelta
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
hahr: int = DNI_UNITS["hahr"]["min"],
|
||||
vailee: int = DNI_UNITS["vailee"]["min"],
|
||||
yahr: int = DNI_UNITS["yahr"]["min"],
|
||||
gahrtahvo: int = DNI_UNITS["gahrtahvo"]["min"],
|
||||
tahvo: int = DNI_UNITS["tahvo"]["min"],
|
||||
gorahn: int = DNI_UNITS["gorahn"]["min"],
|
||||
prorahn: int = DNI_UNITS["prorahn"]["min"],
|
||||
) -> "DniDatetime":
|
||||
if isinstance(hahr, int):
|
||||
hahr, vailee, yahr = check_dni_date_fields(hahr, vailee, yahr)
|
||||
gahrtahvo, tahvo, gorahn, prorahn = check_dni_time_fields(
|
||||
gahrtahvo, tahvo, gorahn, prorahn
|
||||
)
|
||||
self = object.__new__(cls)
|
||||
self._hahr = hahr
|
||||
self._vailee = vailee
|
||||
self._yahr = yahr
|
||||
self._gahrtahvo = gahrtahvo
|
||||
self._tahvo = tahvo
|
||||
self._gorahn = gorahn
|
||||
self._prorahn = prorahn
|
||||
return self
|
||||
return NotImplemented
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"{self.__class__.__qualname__}"
|
||||
f"({self._hahr}, "
|
||||
f"{self._vailee}, "
|
||||
f"{self._yahr}, "
|
||||
f"{self._gahrtahvo}, "
|
||||
f"{self._tahvo}, "
|
||||
f"{self._gorahn}, "
|
||||
f"{self._prorahn})"
|
||||
)
|
||||
|
||||
def isoformat(self) -> str:
|
||||
"""Return the datetime formatted according to ISO standard."""
|
||||
return (
|
||||
f"{self._hahr}-{self._vailee:02}-{self._yahr:02} "
|
||||
f"{self._gahrtahvo}:{self._tahvo:02}:"
|
||||
f"{self._gorahn:02}:{self._prorahn:02}"
|
||||
)
|
||||
|
||||
__str__ = isoformat
|
||||
|
||||
# Read-only field accessors
|
||||
@property
|
||||
def hahr(self) -> int:
|
||||
"""hahr"""
|
||||
return self._hahr
|
||||
|
||||
@property
|
||||
def vailee(self) -> int:
|
||||
"""vailee"""
|
||||
return self._vailee
|
||||
|
||||
@property
|
||||
def yahr(self) -> int:
|
||||
"""yahr"""
|
||||
return self._yahr
|
||||
|
||||
@property
|
||||
def gahrtahvo(self) -> int:
|
||||
"""gahrtahvo"""
|
||||
return self._gahrtahvo
|
||||
|
||||
@property
|
||||
def tahvo(self) -> int:
|
||||
"""tahvo"""
|
||||
return self._tahvo
|
||||
|
||||
@property
|
||||
def gorahn(self) -> int:
|
||||
"""gorahn"""
|
||||
return self._gorahn
|
||||
|
||||
@property
|
||||
def prorahn(self) -> int:
|
||||
"""prorahn"""
|
||||
return self._prorahn
|
||||
|
||||
def replace(
|
||||
self,
|
||||
hahr: Optional[int] = None,
|
||||
vailee: Optional[int] = None,
|
||||
yahr: Optional[int] = None,
|
||||
gahrtahvo: Optional[int] = None,
|
||||
tahvo: Optional[int] = None,
|
||||
gorahn: Optional[int] = None,
|
||||
prorahn: Optional[int] = None,
|
||||
) -> "DniDatetime":
|
||||
"""Return a new D'ni datetime with new values for the specified fields."""
|
||||
if hahr is None:
|
||||
hahr = self._hahr
|
||||
if vailee is None:
|
||||
vailee = self._vailee
|
||||
if yahr is None:
|
||||
yahr = self._yahr
|
||||
if gahrtahvo is None:
|
||||
gahrtahvo = self._gahrtahvo
|
||||
if tahvo is None:
|
||||
tahvo = self._tahvo
|
||||
if gorahn is None:
|
||||
gorahn = self._gorahn
|
||||
if prorahn is None:
|
||||
prorahn = self._prorahn
|
||||
return type(self)(hahr, vailee, yahr, gahrtahvo, tahvo, gorahn, prorahn)
|
||||
|
||||
# Arithmetic
|
||||
def __add__(self, other: Any) -> "DniDatetime":
|
||||
if not isinstance(other, (timedelta, DniTimedelta)):
|
||||
return NotImplemented
|
||||
if isinstance(other, timedelta):
|
||||
other = DniTimedelta.from_timedelta(other)
|
||||
delta = DniTimedelta(
|
||||
self.to_ordinal(),
|
||||
gahrtahvotee=self._gahrtahvo,
|
||||
tahvotee=self._tahvo,
|
||||
gorahntee=self._gorahn,
|
||||
prorahntee=self._prorahn,
|
||||
)
|
||||
delta += other
|
||||
total_prorahn = delta.pahrtahvotee * 3125 + delta.prorahntee
|
||||
gahrtahvo, rem = divmod(total_prorahn, 15625)
|
||||
tahvo, rem = divmod(rem, 625)
|
||||
gorahn, prorahn = divmod(rem, 25)
|
||||
if 0 < delta.yahrtee <= MAX_DNI_ORDINAL:
|
||||
return type(self).combine(
|
||||
DniDate.from_ordinal(delta.yahrtee),
|
||||
DniTime(gahrtahvo, tahvo, gorahn, prorahn),
|
||||
)
|
||||
raise OverflowError("result out of range")
|
||||
|
||||
__radd__ = __add__
|
||||
|
||||
def __sub__(self, other: Any) -> Union["DniDatetime", DniTimedelta]:
|
||||
if not isinstance(other, DniDatetime):
|
||||
if isinstance(other, timedelta):
|
||||
return self + -other
|
||||
else:
|
||||
yahrtee1 = self.to_ordinal()
|
||||
yahrtee2 = other.to_ordinal()
|
||||
prorahntee1 = (
|
||||
self._gahrtahvo * 15625
|
||||
+ self._tahvo * 625
|
||||
+ self._gorahn * 25
|
||||
+ self._prorahn
|
||||
)
|
||||
prorahntee2 = (
|
||||
other._gahrtahvo * 15625
|
||||
+ other._tahvo * 625
|
||||
+ other._gorahn * 25
|
||||
+ other._prorahn
|
||||
)
|
||||
return DniTimedelta(yahrtee1 - yahrtee2, prorahntee1 - prorahntee2)
|
||||
return NotImplemented
|
||||
|
||||
# Rich comparisons
|
||||
def _cmp(self, other: Any) -> int:
|
||||
"""Helper function for rich comparison of DniDatetime objects."""
|
||||
# pylint: disable=protected-access
|
||||
assert isinstance(other, DniDatetime)
|
||||
return cmp(
|
||||
(
|
||||
self._hahr,
|
||||
self._vailee,
|
||||
self._yahr,
|
||||
self._gahrtahvo,
|
||||
self._tahvo,
|
||||
self._gorahn,
|
||||
self._prorahn,
|
||||
),
|
||||
(
|
||||
other._hahr,
|
||||
other._vailee,
|
||||
other._yahr,
|
||||
other._gahrtahvo,
|
||||
other._tahvo,
|
||||
other._gorahn,
|
||||
other._prorahn,
|
||||
),
|
||||
)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniDatetime):
|
||||
return self._cmp(other) == 0
|
||||
if not isinstance(other, DniDate):
|
||||
return NotImplemented
|
||||
return False
|
||||
|
||||
def __le__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniDatetime):
|
||||
return self._cmp(other) <= 0
|
||||
if not isinstance(other, DniDate):
|
||||
return NotImplemented
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniDatetime):
|
||||
return self._cmp(other) < 0
|
||||
if not isinstance(other, DniDate):
|
||||
return NotImplemented
|
||||
return NotImplemented
|
||||
|
||||
def __ge__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniDatetime):
|
||||
return self._cmp(other) >= 0
|
||||
if not isinstance(other, DniDate):
|
||||
return NotImplemented
|
||||
return NotImplemented
|
||||
|
||||
def __gt__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniDatetime):
|
||||
return self._cmp(other) > 0
|
||||
if not isinstance(other, DniDate):
|
||||
return NotImplemented
|
||||
return NotImplemented
|
||||
|
||||
def date(self) -> DniDate:
|
||||
"""Return the date part of the DniDatetime."""
|
||||
return DniDate(self._hahr, self._vailee, self._yahr)
|
||||
|
||||
def time(self) -> DniTime:
|
||||
"""Return the time part of the DniDatetime."""
|
||||
return DniTime(self._gahrtahvo, self._tahvo, self._gorahn, self._prorahn)
|
||||
|
||||
def timestamp(self) -> int:
|
||||
"""Return total number of prorahntee since the D'ni epoch."""
|
||||
return (self - DNI_EPOCH_DDT).total_prorahntee() #type: ignore
|
||||
|
||||
def timetuple(self) -> DniDateTimeTuple:
|
||||
"""Return the DniDateTime in the form of a timetuple."""
|
||||
return (
|
||||
self._hahr,
|
||||
self._vailee,
|
||||
self._yahr,
|
||||
self._gahrtahvo,
|
||||
self._tahvo,
|
||||
self._gorahn,
|
||||
self._prorahn
|
||||
)
|
||||
|
||||
def to_ordinal(self) -> int:
|
||||
"""Return a D'ni ordinal for the hahr, vailee, and yahr."""
|
||||
return hvy2ord(self._hahr, self._vailee, self._yahr)
|
||||
|
||||
@classmethod
|
||||
def combine(cls, date_obj: DniDate, time_obj: DniTime) -> "DniDatetime":
|
||||
"""Construct a D'ni datetime from a given DniDate and DniTime."""
|
||||
# pylint: disable=protected-access
|
||||
if not isinstance(date_obj, DniDate):
|
||||
raise TypeError("date_obj argument must be a DniDate instance.")
|
||||
if not isinstance(time_obj, DniTime):
|
||||
raise TypeError("time_obj argument must be a DniTime instance.")
|
||||
return cls(
|
||||
date_obj._hahr,
|
||||
date_obj._vailee,
|
||||
date_obj._yahr,
|
||||
time_obj._gahrtahvo,
|
||||
time_obj._tahvo,
|
||||
time_obj._gorahn,
|
||||
time_obj._prorahn,
|
||||
)
|
||||
|
||||
def to_earth(self, tz: Optional[timezone] = timezone.utc) -> datetime:
|
||||
"""Create an earth datetime from a DniDatetime."""
|
||||
return dni_to_earth(self.timetuple()).astimezone(tz)
|
||||
|
||||
@classmethod
|
||||
def from_earth(cls, date_time: datetime) -> "DniDatetime":
|
||||
"""Create a DniDatetime from an earth datetime."""
|
||||
ddt = earth_to_dni(date_time)
|
||||
return cls(*ddt)
|
||||
|
||||
|
||||
DniDatetime.min = DniDatetime(7654, 1, 1)
|
||||
DniDatetime.max = DniDatetime(17655, 8, 1, 4, 24, 24, 24)
|
||||
DniDatetime.resolution = DniTimedelta(prorahntee=1)
|
||||
DNI_EPOCH_DDT = DniDatetime(DNI_EPOCH_HAHR, 1, 1, 0, 0, 0, 0)
|
154
src/dnidatetime/dnitime.py
Normal file
154
src/dnidatetime/dnitime.py
Normal file
|
@ -0,0 +1,154 @@
|
|||
"""The DniTime class."""
|
||||
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
|
||||
from .constants import DNI_UNITS
|
||||
from .dnitimedelta import DniTimedelta
|
||||
from .utils import check_dni_time_fields, cmp
|
||||
|
||||
|
||||
class DniTime:
|
||||
"""A D'ni representation of a timestamp."""
|
||||
|
||||
__slots__ = "_gahrtahvo", "_tahvo", "_gorahn", "_prorahn"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_gahrtahvo: int
|
||||
_tahvo: int
|
||||
_gorahn: int
|
||||
_prorahn: int
|
||||
|
||||
# Class properties
|
||||
min: "DniTime"
|
||||
max: "DniTime"
|
||||
resolution: DniTimedelta
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
gahrtahvo: int = DNI_UNITS["gahrtahvo"]["min"],
|
||||
tahvo: int = DNI_UNITS["tahvo"]["min"],
|
||||
gorahn: int = DNI_UNITS["gorahn"]["min"],
|
||||
prorahn: int = DNI_UNITS["prorahn"]["min"],
|
||||
) -> "DniTime":
|
||||
gahrtahvo, tahvo, gorahn, prorahn = check_dni_time_fields(
|
||||
gahrtahvo, tahvo, gorahn, prorahn
|
||||
)
|
||||
self = object.__new__(cls)
|
||||
self._gahrtahvo = gahrtahvo
|
||||
self._tahvo = tahvo
|
||||
self._gorahn = gorahn
|
||||
self._prorahn = prorahn
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_isoformat(cls, time_str: str) -> "DniTime":
|
||||
if not isinstance(time_str, str):
|
||||
raise TypeError("from_isoformat: argument must be str.")
|
||||
|
||||
try:
|
||||
assert 7 <= len(time_str) <= 11
|
||||
parts = time_str.split(":")
|
||||
if len(parts) != 4:
|
||||
raise ValueError(f"Invalid isoformat string: {time_str!r}")
|
||||
return cls(int(parts[0]), int(parts[1]), int(parts[2]), int(parts[3]))
|
||||
except Exception as exc:
|
||||
raise ValueError(f"Invalid isoformat string: {time_str!r}") from exc
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"{self.__class__.__qualname__}"
|
||||
f"({self._gahrtahvo}, "
|
||||
f"{self._tahvo}, "
|
||||
f"{self._gorahn}, "
|
||||
f"{self._prorahn})"
|
||||
)
|
||||
|
||||
def isoformat(self) -> str:
|
||||
"""Return the date formatted according to ISO standard."""
|
||||
return (
|
||||
f"{self._gahrtahvo}:{self._tahvo:02}:"
|
||||
f"{self._gorahn:02}:{self._prorahn:02}"
|
||||
)
|
||||
|
||||
__str__ = isoformat
|
||||
|
||||
# Read-only field accessors
|
||||
@property
|
||||
def gahrtahvo(self) -> int:
|
||||
"""gahrtahvo"""
|
||||
return self._gahrtahvo
|
||||
|
||||
@property
|
||||
def tahvo(self) -> int:
|
||||
"""tahvo"""
|
||||
return self._tahvo
|
||||
|
||||
@property
|
||||
def gorahn(self) -> int:
|
||||
"""gorahn"""
|
||||
return self._gorahn
|
||||
|
||||
@property
|
||||
def prorahn(self) -> int:
|
||||
"""prorahn"""
|
||||
return self._prorahn
|
||||
|
||||
def replace(
|
||||
self,
|
||||
gahrtahvo: Optional[int] = None,
|
||||
tahvo: Optional[int] = None,
|
||||
gorahn: Optional[int] = None,
|
||||
prorahn: Optional[int] = None,
|
||||
) -> "DniTime":
|
||||
"""Return a new D'ni time with new values for the specified fields."""
|
||||
if gahrtahvo is None:
|
||||
gahrtahvo = self._gahrtahvo
|
||||
if tahvo is None:
|
||||
tahvo = self._tahvo
|
||||
if gorahn is None:
|
||||
gorahn = self._gorahn
|
||||
if prorahn is None:
|
||||
prorahn = self._prorahn
|
||||
return type(self)(gahrtahvo, tahvo, gorahn, prorahn)
|
||||
|
||||
# Rich comparisons
|
||||
def _cmp(self, other: Any) -> int:
|
||||
"""Helper function for rich comparison of dnidate objects."""
|
||||
# pylint: disable=protected-access
|
||||
assert isinstance(other, DniTime)
|
||||
mygopro = self._gorahn * 25 + self._prorahn
|
||||
othergopro = self._gorahn * 25 + self._prorahn
|
||||
return cmp(
|
||||
(self._gahrtahvo, self._tahvo, mygopro),
|
||||
(other._gahrtahvo, other._tahvo, othergopro),
|
||||
)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniTime):
|
||||
return self._cmp(other) == 0
|
||||
return NotImplemented
|
||||
|
||||
def __le__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniTime):
|
||||
return self._cmp(other) <= 0
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniTime):
|
||||
return self._cmp(other) < 0
|
||||
return NotImplemented
|
||||
|
||||
def __ge__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniTime):
|
||||
return self._cmp(other) >= 0
|
||||
return NotImplemented
|
||||
|
||||
def __gt__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniTime):
|
||||
return self._cmp(other) > 0
|
||||
return NotImplemented
|
||||
|
||||
|
||||
DniTime.min = DniTime(0, 0, 0, 0)
|
||||
DniTime.max = DniTime(4, 24, 24, 24)
|
||||
DniTime.resolution = DniTimedelta(prorahntee=1)
|
320
src/dnidatetime/dnitimedelta.py
Normal file
320
src/dnidatetime/dnitimedelta.py
Normal file
|
@ -0,0 +1,320 @@
|
|||
"""The DniTimedelta class."""
|
||||
|
||||
from datetime import timedelta
|
||||
from math import modf
|
||||
from typing import Any, Tuple, TYPE_CHECKING, Union
|
||||
|
||||
from .constants import DNI_UNITS, MS_PER_PRORAHN
|
||||
from .utils import cmp, divide_and_round
|
||||
|
||||
|
||||
class DniTimedelta:
|
||||
"""A D'ni timedelta, modelled after the Python datetime.timedelta class."""
|
||||
|
||||
__slots__ = "_yahrtee", "_pahrtahvotee", "_prorahntee"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_yahrtee: int
|
||||
_pahrtahvotee: int
|
||||
_prorahntee: int
|
||||
|
||||
# Class properties
|
||||
min: "DniTimedelta"
|
||||
max: "DniTimedelta"
|
||||
resolution: "DniTimedelta"
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
yahrtee: Union[float, int] = 0,
|
||||
pahrtahvotee: Union[float, int] = 0,
|
||||
prorahntee: Union[float, int] = 0,
|
||||
gorahntee: Union[float, int] = 0,
|
||||
tahvotee: Union[float, int] = 0,
|
||||
gahrtahvotee: Union[float, int] = 0,
|
||||
vaileetee: Union[float, int] = 0,
|
||||
) -> "DniTimedelta":
|
||||
# Final values are integer.
|
||||
yahr: int = 0
|
||||
pahr: int = 0
|
||||
pro: int = 0
|
||||
|
||||
# Normalize
|
||||
yahrtee += vaileetee * DNI_UNITS["yahr"]["max"]
|
||||
pahrtahvotee += gahrtahvotee * 5
|
||||
prorahntee += (gorahntee * DNI_UNITS["prorahn"]["max"]) + (
|
||||
tahvotee * DNI_UNITS["gorahn"]["max"] * DNI_UNITS["prorahn"]["max"]
|
||||
)
|
||||
|
||||
# Remove fractions: yahrtee
|
||||
if isinstance(yahrtee, float):
|
||||
yahr_frac, yahrtee = modf(yahrtee)
|
||||
yahr_pahr_frac, yahr_pahr_whole = modf(yahr_frac * 25.0)
|
||||
assert yahr_pahr_whole == int(yahr_pahr_whole)
|
||||
pahr = int(yahr_pahr_whole)
|
||||
assert yahrtee == int(yahrtee)
|
||||
yahr = int(yahrtee)
|
||||
else:
|
||||
yahr_pahr_frac = 0.0
|
||||
yahr = yahrtee
|
||||
assert isinstance(yahr_pahr_frac, float)
|
||||
assert abs(yahr_pahr_frac) <= 1.0
|
||||
assert isinstance(yahr, int)
|
||||
assert abs(pahr) <= 25
|
||||
|
||||
# Remove fractions: pahrtahvotee
|
||||
if isinstance(pahrtahvotee, float):
|
||||
pahr_frac, pahrtahvotee = modf(pahrtahvotee)
|
||||
assert pahrtahvotee == int(pahrtahvotee)
|
||||
pahrtahvotee = int(pahrtahvotee)
|
||||
pahr_frac += yahr_pahr_frac
|
||||
assert abs(pahr_frac) <= 2.0
|
||||
else:
|
||||
pahr_frac = yahr_pahr_frac
|
||||
assert isinstance(pahr_frac, float)
|
||||
assert abs(pahr_frac) <= 2.0
|
||||
|
||||
assert isinstance(pahrtahvotee, int)
|
||||
yahrtee, pahrtahvotee = divmod(pahrtahvotee, 25)
|
||||
yahr += yahrtee
|
||||
pahr += int(pahrtahvotee)
|
||||
assert isinstance(pahr, int)
|
||||
assert abs(pahr) <= 2 * 25
|
||||
|
||||
# Remove fractions: prorahntee
|
||||
pro_double = pahr_frac * 3125.0
|
||||
assert abs(pro_double) < 2 * 3125.0
|
||||
|
||||
if isinstance(prorahntee, float):
|
||||
prorahntee = round(prorahntee + pro_double)
|
||||
pahrtahvotee, prorahntee = divmod(prorahntee, 3125)
|
||||
yahrtee, pahrtahvotee = divmod(pahrtahvotee, 25)
|
||||
yahr += yahrtee
|
||||
pahr += pahrtahvotee
|
||||
else:
|
||||
prorahntee = int(prorahntee)
|
||||
pahrtahvotee, prorahntee = divmod(prorahntee, 3125)
|
||||
yahrtee, pahrtahvotee = divmod(pahrtahvotee, 25)
|
||||
yahr += yahrtee
|
||||
pahr += pahrtahvotee
|
||||
prorahntee = round(prorahntee + pro_double)
|
||||
assert isinstance(pahr, int)
|
||||
assert isinstance(prorahntee, int)
|
||||
assert abs(pahr) <= 3 * 25
|
||||
assert abs(prorahntee) < 3 * 3125.0
|
||||
|
||||
# Last bit of normalization
|
||||
pahrtahvotee, pro = divmod(prorahntee, 3125)
|
||||
pahr += pahrtahvotee
|
||||
yahrtee, pahr = divmod(pahr, 25)
|
||||
yahr += yahrtee
|
||||
|
||||
assert isinstance(yahr, int)
|
||||
assert isinstance(pahr, int) and 0 <= pahr < 25
|
||||
assert isinstance(pro, int) and 0 <= pro < 3125
|
||||
|
||||
if abs(yahr) > 999999999:
|
||||
raise OverflowError(f"DniTimedelta # of yahrtee is too large: {yahr}")
|
||||
|
||||
self = object.__new__(cls)
|
||||
self._yahrtee = yahr
|
||||
self._pahrtahvotee = pahr
|
||||
self._prorahntee = pro
|
||||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
args = []
|
||||
if self._yahrtee:
|
||||
args.append(f"yahrtee={self._yahrtee}")
|
||||
if self._pahrtahvotee:
|
||||
args.append(f"pahrtahvotee={self._pahrtahvotee}")
|
||||
if self._prorahntee:
|
||||
args.append(f"prorahntee={self._prorahntee}")
|
||||
if not args:
|
||||
args.append("0")
|
||||
return f"{self.__class__.__qualname__}" f"({', '.join(args)})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
prorahn = self._pahrtahvotee * 3125 + self._prorahntee
|
||||
go, pro = divmod(prorahn, 25)
|
||||
tahvo, go = divmod(go, 25)
|
||||
pahr, tahvo = divmod(tahvo, 5)
|
||||
output = f"{pahr}:{tahvo:02}:{go:02}:{pro:02}"
|
||||
if self._yahrtee:
|
||||
plural = "tee" if self._yahrtee != 1 else ""
|
||||
output = f"{self._yahrtee} yahr{plural}, " + output
|
||||
return output
|
||||
|
||||
def total_prorahntee(self) -> int:
|
||||
"""Total number of prorahntee in the duration."""
|
||||
return (self._yahrtee * 78125) + (self._pahrtahvotee * 3125) + self._prorahntee
|
||||
|
||||
# Read-only field accessors
|
||||
@property
|
||||
def yahrtee(self) -> int:
|
||||
"""yahrtee"""
|
||||
return self._yahrtee
|
||||
|
||||
@property
|
||||
def pahrtahvotee(self) -> int:
|
||||
"""pahrtahvotee"""
|
||||
return self._pahrtahvotee
|
||||
|
||||
@property
|
||||
def prorahntee(self) -> int:
|
||||
"""prorahntee"""
|
||||
return self._prorahntee
|
||||
|
||||
# Arithmetic functions
|
||||
def __add__(self, other: Any) -> "DniTimedelta":
|
||||
if isinstance(other, (timedelta, DniTimedelta)):
|
||||
if isinstance(other, timedelta):
|
||||
other = self.from_timedelta(other)
|
||||
return DniTimedelta(
|
||||
self._yahrtee + other._yahrtee,
|
||||
self._pahrtahvotee + other._pahrtahvotee,
|
||||
self._prorahntee + other._prorahntee,
|
||||
)
|
||||
return NotImplemented
|
||||
|
||||
__radd__ = __add__
|
||||
|
||||
def __sub__(self, other: Any) -> "DniTimedelta":
|
||||
if isinstance(other, (timedelta, DniTimedelta)):
|
||||
if isinstance(other, timedelta):
|
||||
other = self.from_timedelta(other)
|
||||
return DniTimedelta(
|
||||
self._yahrtee - other._yahrtee,
|
||||
self._pahrtahvotee - other._pahrtahvotee,
|
||||
self._prorahntee - other._prorahntee,
|
||||
)
|
||||
return NotImplemented
|
||||
|
||||
def __rsub__(self, other: Any) -> "DniTimedelta":
|
||||
if isinstance(other, (timedelta, DniTimedelta)):
|
||||
if isinstance(other, timedelta):
|
||||
other = self.from_timedelta(other)
|
||||
return -self + other
|
||||
return NotImplemented
|
||||
|
||||
def __neg__(self) -> "DniTimedelta":
|
||||
return DniTimedelta(-self._yahrtee, -self._pahrtahvotee, -self._prorahntee)
|
||||
|
||||
def __pos__(self) -> "DniTimedelta":
|
||||
return self
|
||||
|
||||
def __abs__(self) -> "DniTimedelta":
|
||||
if self._yahrtee < 0:
|
||||
return -self
|
||||
return self
|
||||
|
||||
def __mul__(self, other: Any) -> "DniTimedelta":
|
||||
if isinstance(other, int):
|
||||
return DniTimedelta(
|
||||
self._yahrtee * other,
|
||||
self._pahrtahvotee * other,
|
||||
self._prorahntee * other,
|
||||
)
|
||||
if isinstance(other, float):
|
||||
pro = self.total_prorahntee()
|
||||
numer, denom = other.as_integer_ratio()
|
||||
return DniTimedelta(0, 0, divide_and_round(pro * numer, denom))
|
||||
return NotImplemented
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __floordiv__(self, other: Any) -> Union["DniTimedelta", int]:
|
||||
if not isinstance(other, (int, DniTimedelta)):
|
||||
return NotImplemented
|
||||
pro = self.total_prorahntee()
|
||||
if isinstance(other, DniTimedelta):
|
||||
return pro // other.total_prorahntee()
|
||||
if isinstance(other, int):
|
||||
return DniTimedelta(0, 0, pro // other)
|
||||
|
||||
def __truediv__(self, other: Any) -> Union["DniTimedelta", float]:
|
||||
if not isinstance(other, (float, int, DniTimedelta)):
|
||||
return NotImplemented
|
||||
pro = self.total_prorahntee()
|
||||
if isinstance(other, DniTimedelta):
|
||||
return pro / other.total_prorahntee()
|
||||
if isinstance(other, float):
|
||||
numer, denom = other.as_integer_ratio()
|
||||
return DniTimedelta(0, 0, divide_and_round(denom * pro, numer))
|
||||
if isinstance(other, int):
|
||||
return DniTimedelta(0, 0, divide_and_round(pro, other))
|
||||
|
||||
def __mod__(self, other: Any) -> "DniTimedelta":
|
||||
if isinstance(other, DniTimedelta):
|
||||
rem = self.total_prorahntee() % other.total_prorahntee()
|
||||
return DniTimedelta(0, 0, rem)
|
||||
return NotImplemented
|
||||
|
||||
def __divmod__(self, other: Any) -> Tuple[int, "DniTimedelta"]:
|
||||
if isinstance(other, DniTimedelta):
|
||||
quot, rem = divmod(self.total_prorahntee(), other.total_prorahntee())
|
||||
return quot, DniTimedelta(0, 0, rem)
|
||||
return NotImplemented
|
||||
|
||||
# Rich comparisons
|
||||
def _cmp(self, other: Any) -> int:
|
||||
"""Helper function for rich comparison of DniTimedelta objects."""
|
||||
assert isinstance(other, DniTimedelta)
|
||||
return cmp(self.total_prorahntee(), other.total_prorahntee())
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniTimedelta):
|
||||
return self._cmp(other) == 0
|
||||
return NotImplemented
|
||||
|
||||
def __le__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniTimedelta):
|
||||
return self._cmp(other) <= 0
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniTimedelta):
|
||||
return self._cmp(other) < 0
|
||||
return NotImplemented
|
||||
|
||||
def __ge__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniTimedelta):
|
||||
return self._cmp(other) >= 0
|
||||
return NotImplemented
|
||||
|
||||
def __gt__(self, other: Any) -> bool:
|
||||
if isinstance(other, DniTimedelta):
|
||||
return self._cmp(other) > 0
|
||||
return NotImplemented
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self._yahrtee != 0 or self._pahrtahvotee != 0 or self._prorahntee != 0
|
||||
|
||||
@classmethod
|
||||
def from_timedelta(cls, delta: timedelta) -> "DniTimedelta":
|
||||
"""Converts a normal timedelta to a D'Ni timedelta."""
|
||||
prorahntee = delta.total_seconds() * 1000 / MS_PER_PRORAHN
|
||||
return cls(prorahntee=prorahntee)
|
||||
|
||||
def to_timedelta(self) -> timedelta:
|
||||
"""Converts the D'Ni timedelta to a normal timedelta."""
|
||||
milliseconds = self.total_prorahntee() * MS_PER_PRORAHN
|
||||
try:
|
||||
delta = timedelta(milliseconds=milliseconds)
|
||||
return delta
|
||||
except OverflowError as exc:
|
||||
# This is needed for precision issues in conversion.
|
||||
if self >= self.max:
|
||||
return timedelta.max
|
||||
if self <= self.min:
|
||||
return timedelta.min
|
||||
raise exc
|
||||
|
||||
|
||||
# Converted from timedelta.min
|
||||
DniTimedelta.min = DniTimedelta(yahrtee=-793993715, pahrtahvotee=17, prorahntee=768)
|
||||
|
||||
# Converted from timedelta.max
|
||||
DniTimedelta.max = DniTimedelta(yahrtee=793993715, pahrtahvotee=2, prorahntee=1887)
|
||||
|
||||
# Smallest measurement of change.
|
||||
DniTimedelta.resolution = DniTimedelta(prorahntee=1)
|
137
src/dnidatetime/utils.py
Normal file
137
src/dnidatetime/utils.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
"""Helper functions for all classes/functions of the module."""
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Optional, Tuple, Union
|
||||
|
||||
from .constants import DNI_UNITS, EARTH_EPOCH, LEAP_MS_FROM_EPOCH
|
||||
|
||||
|
||||
def cmp(x: Any, y: Any) -> int:
|
||||
"""Compare objects and return:
|
||||
|
||||
x < y -> -1
|
||||
x == y -> 0
|
||||
x > y -> 1
|
||||
"""
|
||||
return 0 if x == y else 1 if x > y else -1
|
||||
|
||||
|
||||
def divide_and_round(a: Union[float, int], b: Union[float, int]) -> int:
|
||||
"""Divide a by b and round result to the nearest integer. When the ratio
|
||||
is exactly half-way between two integers, the even integer is returned.
|
||||
"""
|
||||
quot, rem = divmod(a, b)
|
||||
rem *= 2
|
||||
greater_than_half = rem > b if b > 0 else rem < b
|
||||
if greater_than_half or rem == b and quot % 2 == 1:
|
||||
quot += 1
|
||||
|
||||
return int(quot)
|
||||
|
||||
|
||||
def check_dni_date_fields(
|
||||
hahr: int = DNI_UNITS["hahr"]["min"],
|
||||
vailee: int = DNI_UNITS["vailee"]["min"],
|
||||
yahr: int = DNI_UNITS["yahr"]["min"],
|
||||
) -> Tuple[int, int, int]:
|
||||
"""Verifies the D'Ni date fields are within proper boundaries."""
|
||||
if not DNI_UNITS["hahr"]["min"] <= hahr <= DNI_UNITS["hahr"]["max"]:
|
||||
raise ValueError(
|
||||
f"Hahr must be in {DNI_UNITS['hahr']['min']}..{DNI_UNITS['hahr']['max']}",
|
||||
hahr,
|
||||
)
|
||||
if not DNI_UNITS["vailee"]["min"] <= vailee <= DNI_UNITS["vailee"]["max"]:
|
||||
raise ValueError(
|
||||
f"Vailee must be in {DNI_UNITS['vailee']['min']}..{DNI_UNITS['vailee']['max']}",
|
||||
vailee,
|
||||
)
|
||||
if not DNI_UNITS["yahr"]["min"] <= yahr <= DNI_UNITS["yahr"]["max"]:
|
||||
raise ValueError(
|
||||
f"Yahr must be in {DNI_UNITS['yahr']['min']}..{DNI_UNITS['yahr']['max']}",
|
||||
yahr,
|
||||
)
|
||||
return hahr, vailee, yahr
|
||||
|
||||
|
||||
def check_dni_time_fields(
|
||||
gahrtahvo: int = DNI_UNITS["gahrtahvo"]["min"],
|
||||
tahvo: int = DNI_UNITS["tahvo"]["min"],
|
||||
gorahn: int = DNI_UNITS["gorahn"]["min"],
|
||||
prorahn: int = DNI_UNITS["prorahn"]["min"],
|
||||
) -> Tuple[int, int, int, int]:
|
||||
"""Verifies the D'Ni time fields are within proper boundaries."""
|
||||
if (
|
||||
not DNI_UNITS["gahrtahvo"]["min"]
|
||||
<= gahrtahvo
|
||||
<= DNI_UNITS["gahrtahvo"]["max"] - 1
|
||||
):
|
||||
raise ValueError(
|
||||
f"Gahrtahvo must be in {DNI_UNITS['gahrtahvo']['min']}..{DNI_UNITS['gahrtahvo']['max'] - 1}",
|
||||
gahrtahvo,
|
||||
)
|
||||
if not DNI_UNITS["tahvo"]["min"] <= tahvo <= DNI_UNITS["tahvo"]["max"] - 1:
|
||||
raise ValueError(
|
||||
f"Tahvo must be in {DNI_UNITS['tahvo']['min']}..{DNI_UNITS['tahvo']['max'] - 1}",
|
||||
tahvo,
|
||||
)
|
||||
if (
|
||||
not DNI_UNITS["gorahn"]["min"]
|
||||
<= gorahn
|
||||
<= DNI_UNITS["gorahn"]["max"] - 1
|
||||
):
|
||||
raise ValueError(
|
||||
f"Gorahn must be in {DNI_UNITS['gorahn']['min']}..{DNI_UNITS['gorahn']['max'] - 1}",
|
||||
gorahn,
|
||||
)
|
||||
if (
|
||||
not DNI_UNITS["prorahn"]["min"]
|
||||
<= prorahn
|
||||
<= DNI_UNITS["prorahn"]["max"] - 1
|
||||
):
|
||||
raise ValueError(
|
||||
f"Prorahn must be in {DNI_UNITS['prorahn']['min']}..{DNI_UNITS['prorahn']['max'] - 1}",
|
||||
prorahn,
|
||||
)
|
||||
return gahrtahvo, tahvo, gorahn, prorahn
|
||||
|
||||
|
||||
def add_leap_seconds(timestamp: int) -> int:
|
||||
"""Adds leap seconds to a timestamp."""
|
||||
leap_seconds = 0
|
||||
for leap in LEAP_MS_FROM_EPOCH:
|
||||
if timestamp >= leap:
|
||||
leap_seconds += 1
|
||||
|
||||
# Adjust for the 10 leap seconds that started in 1972.
|
||||
if leap_seconds > 0:
|
||||
leap_seconds += 9
|
||||
|
||||
return timestamp + (leap_seconds * 1000)
|
||||
|
||||
|
||||
def get_ms_from_epoch(date_time: Optional[datetime] = None) -> int:
|
||||
"""
|
||||
Returns the amount of milliseconds since the UNIX epoch (1-1-1970).
|
||||
"""
|
||||
# Thanks to the examples from here:
|
||||
# https://stackoverflow.com/questions/5395872/
|
||||
if not date_time:
|
||||
date = datetime.now(timezone.utc)
|
||||
else:
|
||||
date = date_time.replace(tzinfo=timezone.utc)
|
||||
return ((date - EARTH_EPOCH) // timedelta(microseconds=1)) // 1000
|
||||
|
||||
|
||||
def get_adj_ms_from_epoch(date_time: Optional[datetime] = None) -> int:
|
||||
"""
|
||||
Returns the amount of milliseconds since the UNIX epoch (1-1-1970),
|
||||
but also accounting for leap seconds.
|
||||
"""
|
||||
return add_leap_seconds(get_ms_from_epoch(date_time))
|
||||
|
||||
|
||||
def is_timezone_aware(dt: datetime) -> bool:
|
||||
"""Returns whether a datetime object has timezone info or not."""
|
||||
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
93
tests/test_conversions.py
Normal file
93
tests/test_conversions.py
Normal file
|
@ -0,0 +1,93 @@
|
|||
"""Tests for the helper functions in conversions.py"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from dnidatetime.conversions import (
|
||||
hvy2ord,
|
||||
ord2hvy,
|
||||
earth_to_dni,
|
||||
dni_to_earth
|
||||
)
|
||||
|
||||
|
||||
def test_hvy2ord_8765_4_29():
|
||||
assert hvy2ord(8765, 4, 29) == 322306
|
||||
|
||||
|
||||
def test_hvy2ord_8765_5_1():
|
||||
assert hvy2ord(8765, 5, 1) == hvy2ord(8765, 4, 29) + 1
|
||||
|
||||
|
||||
def test_hvy2ord_8765_10_29():
|
||||
assert hvy2ord(8765, 10, 29) == 322480
|
||||
|
||||
|
||||
def test_hvy2ord_8766_1_1():
|
||||
assert hvy2ord(8766, 1, 1) == hvy2ord(8765, 10, 29) + 1
|
||||
|
||||
|
||||
def test_ord2hvy_too_low():
|
||||
with pytest.raises(ValueError, match="Ordinal"):
|
||||
ord2hvy(-1)
|
||||
|
||||
|
||||
def test_ord2hvy_too_high():
|
||||
with pytest.raises(ValueError, match="Ordinal"):
|
||||
ord2hvy(2900500)
|
||||
|
||||
|
||||
def test_ord2hvy_322306():
|
||||
assert ord2hvy(322306) == (8765, 4, 29)
|
||||
|
||||
|
||||
def test_ord2hvy_322307():
|
||||
assert ord2hvy(322307) == (8765, 5, 1)
|
||||
|
||||
|
||||
def test_ord2hvy_322480():
|
||||
assert ord2hvy(322480) == (8765, 10, 29)
|
||||
|
||||
|
||||
def test_ord2hvy_322481():
|
||||
assert ord2hvy(322481) == (8766, 1, 1)
|
||||
|
||||
|
||||
def test_earth_to_dni_no_timezone():
|
||||
with pytest.raises(ValueError, match="timezone"):
|
||||
earth_to_dni(datetime.now())
|
||||
|
||||
|
||||
def test_earth_to_dni_dniepoch():
|
||||
assert earth_to_dni(
|
||||
datetime(1991, 4, 21, 16, 54, 0, tzinfo=timezone.utc)
|
||||
) == (9647, 1, 1, 0, 0, 0, 0)
|
||||
|
||||
|
||||
def test_earth_to_dni_2020():
|
||||
assert earth_to_dni(
|
||||
datetime(2020, 8, 27, 10, 51, 0, tzinfo=timezone.utc)
|
||||
) == (9676, 4, 16, 1, 0, 20, 5)
|
||||
|
||||
|
||||
def test_dni_to_earth_not_tuple():
|
||||
with pytest.raises(ValueError, match="timetuple"):
|
||||
dni_to_earth([9647, 1, 1, 0, 0, 0, 0])
|
||||
|
||||
|
||||
def test_dni_to_earth_bad_tuple():
|
||||
with pytest.raises(ValueError, match="malformed"):
|
||||
dni_to_earth((9647, 1, 1))
|
||||
|
||||
|
||||
def test_dni_to_earth_dniepoch():
|
||||
assert dni_to_earth(
|
||||
(9647, 1, 1, 0, 0, 0, 0)
|
||||
) == datetime(1991, 4, 21, 16, 54, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def test_dni_to_earth_2020():
|
||||
assert dni_to_earth(
|
||||
(9676, 4, 16, 1, 0, 20, 5)
|
||||
) == datetime(2020, 8, 27, 10, 50, 59, 668172, tzinfo=timezone.utc)
|
154
tests/test_utils.py
Normal file
154
tests/test_utils.py
Normal file
|
@ -0,0 +1,154 @@
|
|||
"""Tests for the helper functions in util.py"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from dnidatetime.utils import (
|
||||
cmp,
|
||||
divide_and_round,
|
||||
check_dni_date_fields,
|
||||
check_dni_time_fields,
|
||||
add_leap_seconds,
|
||||
get_ms_from_epoch,
|
||||
get_adj_ms_from_epoch,
|
||||
is_timezone_aware
|
||||
)
|
||||
|
||||
|
||||
def test_cmp_lessthan():
|
||||
assert cmp(1, 10) == -1
|
||||
|
||||
|
||||
def test_cmp_equal():
|
||||
assert cmp(5, 5) == 0
|
||||
|
||||
|
||||
def test_cmp_greaterthan():
|
||||
assert cmp(10, 1) == 1
|
||||
|
||||
|
||||
def test_dar_down():
|
||||
assert divide_and_round(5, 2) == 2
|
||||
|
||||
|
||||
def test_dar_up():
|
||||
assert divide_and_round(7, 2) == 4
|
||||
|
||||
|
||||
def test_dar_no_rem():
|
||||
assert divide_and_round(6, 2) == 3
|
||||
|
||||
|
||||
def test_check_dnidate_hahr_low():
|
||||
with pytest.raises(ValueError, match="Hahr"):
|
||||
check_dni_date_fields(7000, 1, 1)
|
||||
|
||||
|
||||
def test_check_dnidate_hahr_high():
|
||||
with pytest.raises(ValueError, match="Hahr"):
|
||||
check_dni_date_fields(20000, 1, 1)
|
||||
|
||||
|
||||
def test_check_dnidate_vailee_low():
|
||||
with pytest.raises(ValueError, match="Vailee"):
|
||||
check_dni_date_fields(7654, -1, 1)
|
||||
|
||||
|
||||
def test_check_dnidate_vailee_high():
|
||||
with pytest.raises(ValueError, match="Vailee"):
|
||||
check_dni_date_fields(7654, 12, 1)
|
||||
|
||||
|
||||
def test_check_dnidate_yahr_low():
|
||||
with pytest.raises(ValueError, match="Yahr"):
|
||||
check_dni_date_fields(7654, 1, -1)
|
||||
|
||||
|
||||
def test_check_dnidate_yahr_high():
|
||||
with pytest.raises(ValueError, match="Yahr"):
|
||||
check_dni_date_fields(7654, 1, 31)
|
||||
|
||||
|
||||
def test_check_dnitime_gahrtahvo_low():
|
||||
with pytest.raises(ValueError, match="Gahrtahvo"):
|
||||
check_dni_time_fields(-1, 0, 0, 0)
|
||||
|
||||
|
||||
def test_check_dnitime_gahrtahvo_high():
|
||||
with pytest.raises(ValueError, match="Gahrtahvo"):
|
||||
check_dni_time_fields(10, 0, 0, 0)
|
||||
|
||||
|
||||
def test_check_dnitime_tahvo_low():
|
||||
with pytest.raises(ValueError, match="Tahvo"):
|
||||
check_dni_time_fields(0, -1, 0, 0)
|
||||
|
||||
|
||||
def test_check_dnitime_tahvo_high():
|
||||
with pytest.raises(ValueError, match="Tahvo"):
|
||||
check_dni_time_fields(0, 30, 0, 0)
|
||||
|
||||
|
||||
def test_check_dnitime_gorahn_low():
|
||||
with pytest.raises(ValueError, match="Gorahn"):
|
||||
check_dni_time_fields(0, 0, -1, 0)
|
||||
|
||||
|
||||
def test_check_dnitime_gorahn_high():
|
||||
with pytest.raises(ValueError, match="Gorahn"):
|
||||
check_dni_time_fields(0, 0, 30, 0)
|
||||
|
||||
|
||||
def test_check_dnitime_prorahn_low():
|
||||
with pytest.raises(ValueError, match="Prorahn"):
|
||||
check_dni_time_fields(0, 0, 0, -1)
|
||||
|
||||
|
||||
def test_check_dnitime_prorahn_high():
|
||||
with pytest.raises(ValueError, match="Prorahn"):
|
||||
check_dni_time_fields(0, 0, 0, 30)
|
||||
|
||||
|
||||
def test_add_leap_seconds_epoch():
|
||||
assert add_leap_seconds(0) == 0
|
||||
|
||||
|
||||
def test_add_leap_seconds_1984():
|
||||
assert add_leap_seconds(455328000000) == 455328022000
|
||||
|
||||
|
||||
def test_add_leap_seconds_2020():
|
||||
assert add_leap_seconds(1598486400000) == 1598486437000
|
||||
|
||||
|
||||
def test_get_ms_from_epoch_base():
|
||||
assert get_ms_from_epoch(datetime(1970, 1, 1)) == 0
|
||||
|
||||
|
||||
def test_get_ms_from_epoch_1984():
|
||||
assert get_ms_from_epoch(datetime(1984, 6, 6)) == 455328000000
|
||||
|
||||
|
||||
def test_get_ms_from_epoch_2020():
|
||||
assert get_ms_from_epoch(datetime(2020, 8, 27)) == 1598486400000
|
||||
|
||||
|
||||
def test_get_adj_ms_from_epoch_1959():
|
||||
assert get_adj_ms_from_epoch(datetime(1959, 1, 17)) == -345772800000
|
||||
|
||||
|
||||
def test_get_adj_ms_from_epoch_1981():
|
||||
assert get_adj_ms_from_epoch(datetime(1981, 9, 14)) == 369273620000
|
||||
|
||||
|
||||
def test_get_adj_ms_from_epoch_2023():
|
||||
assert get_adj_ms_from_epoch(datetime(2023, 12, 8)) == 1701993637000
|
||||
|
||||
|
||||
def test_is_timezone_aware_true():
|
||||
assert is_timezone_aware(datetime.now().astimezone()) is True
|
||||
|
||||
|
||||
def test_is_timezone_aware_false():
|
||||
assert is_timezone_aware(datetime.now()) is False
|
Loading…
Reference in a new issue