Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions nion/utils/DateTime.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@

last_time: float = 0.0
last_time_lock = threading.RLock()
FILETIME_TICKS_PER_MICROSECOND = 10 # Hundreds of nanoseconds in a microsecond
FILETIME_TICKS_PER_SECOND = 10000000 # Hundreds of nanoseconds (0.1 microseconds) in a second
FILETIME_EPOCH = datetime.datetime(1601, 1, 1, tzinfo=datetime.timezone.utc)


class DateTimeUTC:
Expand Down Expand Up @@ -62,3 +65,32 @@ def utcnow() -> datetime.datetime:

def now() -> datetime.datetime:
return datetime.datetime.now()

def get_datetime_from_windows_filetime(filetime: int) -> datetime.datetime:
"""Converts a windows filetime to a datetime in UTC

Windows file time is: the time in hundreds of nanoseconds since January 1st 1601 UTC
Since datetime objects only have 1 microsecond precision the exact filetime is not fully preserved.
"""
try:
total_microseconds, remainder = divmod(filetime, FILETIME_TICKS_PER_MICROSECOND) # regular division would introduce floating point issues
total_microseconds += 0 if remainder < 5 else 1 # we can manually do rounding to avoid the floating point issues
return FILETIME_EPOCH + datetime.timedelta(microseconds=total_microseconds)
except OverflowError:
if filetime < 0:
return datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)
else:
return datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)


def get_windows_filetime_from_datetime(time_dt: datetime.datetime) -> int:
"""Converts a datetime to a Windows file time.

If the datetime's timezone is None it is assumed to be UTC.
"""
if time_dt.tzinfo is None:
time_dt = time_dt.replace(tzinfo=datetime.timezone.utc)

delta = time_dt.astimezone(datetime.timezone.utc) - FILETIME_EPOCH
file_time_ticks = (delta.days * 24 * 3600 + delta.seconds) * FILETIME_TICKS_PER_SECOND + delta.microseconds * FILETIME_TICKS_PER_MICROSECOND
return file_time_ticks
70 changes: 70 additions & 0 deletions nion/utils/test/DateTime_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import unittest
import datetime
from nion.utils import DateTime

class Test(unittest.TestCase):
def test_datetime_to_filetime(self) -> None:
test_datetimes = [
(datetime.datetime(2025, 1, 1), 133801632000000000),
(datetime.datetime(2025, 1, 1, 12, 30, 45), 133802082450000000),
(datetime.datetime(2025, 12, 31, 23, 59, 59, 999999), 134116991999999990), # max microsecond
(datetime.datetime(1970, 1, 1, 0, 0, 0), 116444736000000000), # Unix epoch
(datetime.datetime(9999, 12, 31, 23, 59, 59, 999999), 2650467743999999990), # Python max datetime.datetime
(datetime.datetime(2024, 2, 29, 15, 0), 133536924000000000), # leap day
(datetime.datetime(2000, 2, 29, 23, 59, 59), 125963423990000000), # leap year divisible by 400
(datetime.datetime(2025, 6, 1, 12, 0, tzinfo=datetime.timezone.utc), 133932528000000000),# UTC aware
(datetime.datetime(2025, 6, 1, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(hours=5, minutes=30))),133932330000000000), # India Standard Time
(datetime.datetime(1950, 5, 17, 8, 20, 0, tzinfo=datetime.timezone.utc), 110251020000000000), # negative time stamp
(datetime.datetime(1066, 1, 1, 0, 0, 0), -168829920000000000), # before windows filetime start
(datetime.datetime(2025, 7, 15, 10, 5, 30, 123456), 133970475301234560), # microseconds
(datetime.datetime(2025, 7, 15, 10, 5, 30, 0), 133970475300000000), # zero microseconds
(datetime.datetime(2035, 8, 1, 9, 0, tzinfo=datetime.timezone.utc), 137140452000000000), # 10 years ahead
(datetime.datetime(2100, 1, 1, 0, 0, 0),157469184000000000), # non‑leap century year
(datetime.datetime(2025, 3, 30, 0, 30, tzinfo=datetime.timezone(datetime.timedelta(hours=-1))), 133877718000000000), # before daylight savings
(datetime.datetime(2025, 3, 30, 1, 30, tzinfo=datetime.timezone(datetime.timedelta(hours=0))), 133877718000000000), # ambiguous transition
(datetime.datetime(2025, 10, 26, 1, 30, tzinfo=datetime.timezone(datetime.timedelta(hours=0))), 134059158000000000), # repeated hour
(datetime.datetime(2025, 6, 1, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(hours=4))), 133932384000000000), # US Eastern
(datetime.datetime(2025, 6, 1, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(hours=9))), 133932204000000000), # Japan time
]
for datetime_in, expected_filetime in test_datetimes:
with self.subTest(f"Datetime to filetime {datetime_in.isoformat()} expects {expected_filetime}"):
filetime = DateTime.get_windows_filetime_from_datetime(datetime_in)
print(filetime)
self.assertEqual(filetime, expected_filetime)
datetime_out = DateTime.get_datetime_from_windows_filetime(filetime)
if datetime_in.tzinfo is None:

time_in_utc = datetime_in.replace(tzinfo=datetime.timezone.utc)
else:
time_in_utc = datetime_in.astimezone(tz=datetime.timezone.utc)
self.assertEqual(time_in_utc, datetime_out)

with self.subTest(f"Filetime to datetime {datetime_in} expects {expected_filetime}"):
datetime_out = DateTime.get_datetime_from_windows_filetime(expected_filetime)
if datetime_in.tzinfo is None:

time_in_utc = datetime_in.replace(tzinfo=datetime.timezone.utc)
else:
time_in_utc = datetime_in.astimezone(tz=datetime.timezone.utc)
self.assertEqual(time_in_utc, datetime_out)
filetime_out = DateTime.get_windows_filetime_from_datetime(datetime_out)
self.assertEqual(filetime_out, expected_filetime)

def test_invalid_times(self) -> None:
test_filetimes = [
(datetime.datetime.max, 2650467744999999990), # Above the max datetime
(datetime.datetime.max, 9223372036854775807), # Int max,
(datetime.datetime.min, -9223372036854775808), # int min
]
for i, (expected_datetime, filetime_in) in enumerate(test_filetimes):
with self.subTest(f"Invalid filetime test: {expected_datetime} expects {filetime_in}"):
datetime_out = DateTime.get_datetime_from_windows_filetime(filetime_in)
if expected_datetime.tzinfo is None:

time_in_utc = expected_datetime.replace(tzinfo=datetime.timezone.utc)
else:
time_in_utc = expected_datetime.astimezone(tz=datetime.timezone.utc)
self.assertEqual(datetime_out, time_in_utc)

if __name__ == '__main__':
unittest.main()