Skip to content

Commit

Permalink
feat: port PreCalculatedDateTimeZone (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisimcevoy authored May 26, 2024
1 parent 430b920 commit 8e3326c
Show file tree
Hide file tree
Showing 4 changed files with 468 additions and 2 deletions.
42 changes: 40 additions & 2 deletions pyoda_time/time_zones/_precalculated_date_time_zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pyoda_time.time_zones._i_zone_interval_map import _IZoneIntervalMap
from pyoda_time.time_zones._standard_daylight_alternating_map import _StandardDaylightAlternatingMap
from pyoda_time.time_zones.io._i_date_time_zone_reader import _IDateTimeZoneReader
from pyoda_time.time_zones.io._i_date_time_zone_writer import _IDateTimeZoneWriter
from pyoda_time.utility._csharp_compatibility import _sealed, _towards_zero_division
from pyoda_time.utility._preconditions import _Preconditions

Expand Down Expand Up @@ -53,10 +54,17 @@ def __init__(self, id_: str, intervals: list[ZoneInterval], tail_zone: _IZoneInt
)
else:
self.__first_tail_zone_interval = None
self.__validate_periods(intervals, tail_zone)
self._validate_periods(intervals, tail_zone)

@staticmethod
def __validate_periods(periods: list[ZoneInterval], tail_zone: _IZoneIntervalMap | None) -> None:
def _validate_periods(periods: list[ZoneInterval], tail_zone: _IZoneIntervalMap | None) -> None:
"""Validates that all the periods before the tail zone make sense. We have to start at the beginning of time,
and then have adjoining periods. This is only called in the constructors.
This is only called from the constructors, but is internal to make it easier to test.
:raises ValueError: The periods specified are invalid.
"""
_Preconditions._check_argument(len(periods) > 0, "periods", "No periods specified in precalculated time zone")
_Preconditions._check_argument(
not periods[0].has_start,
Expand Down Expand Up @@ -112,6 +120,36 @@ def get_zone_interval(self, instant: Instant) -> ZoneInterval:

# region I/O

def _write(self, writer: _IDateTimeZoneWriter) -> None:
"""Writes the time zone to the specified writer.
:param writer: The writer to write to.
"""
_Preconditions._check_not_null(writer, "writer")

# We used to create a pool of strings just for this zone. This was more efficient
# for some zones, as it meant that each string would be written out with just a single
# byte after the pooling. Optimizing the string pool globally instead allows for
# roughly the same efficiency, and simpler code here.
writer.write_count(len(self.__periods))
previous: Instant | None = None
for period in self.__periods:
writer.write_zone_interval_transition(previous, previous := period._raw_start)
writer.write_string(period.name)
writer.write_offset(period.wall_offset)
writer.write_offset(period.savings)

writer.write_zone_interval_transition(previous, self.__tail_zone_start)
# We could just check whether we've got to the end of the stream, but this
# feels slightly safer.
writer.write_byte(0 if self.__tail_zone is None else 1)
if self.__tail_zone is not None:
# TODO: In Noda Time, this is done with a cast...
# This is the only kind of zone we support in the new format. Enforce that...
if not isinstance(self.__tail_zone, _StandardDaylightAlternatingMap):
raise RuntimeError(f"Only {_StandardDaylightAlternatingMap.__name__} is supported")
self.__tail_zone._write(writer)

@classmethod
def _read(cls, reader: _IDateTimeZoneReader, id_: str) -> DateTimeZone:
"""Reads a time zone from the specified reader.
Expand Down
3 changes: 3 additions & 0 deletions tests/testing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright 2024 The Pyoda Time Authors. All rights reserved.
# Use of this source code is governed by the Apache License 2.0,
# as found in the LICENSE.txt file.
65 changes: 65 additions & 0 deletions tests/testing/single_transition_date_time_zone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Copyright 2024 The Pyoda Time Authors. All rights reserved.
# Use of this source code is governed by the Apache License 2.0,
# as found in the LICENSE.txt file.
from pyoda_time import DateTimeZone, Instant, Offset
from pyoda_time.time_zones import ZoneInterval


class SingleTransitionDateTimeZone(DateTimeZone):
"""Time zone with a single transition between two offsets.
This provides a simple way to test behaviour across a transition.
"""

@property
def early_interval(self) -> ZoneInterval:
"""Gets the ``ZoneInterval`` for the period before the transition, starting at the beginning of time.
:return: The zone interval for the period before the transition, starting at the beginning of time.
"""
return self.__early_interval

@property
def late_interval(self) -> ZoneInterval:
"""Gets the ``ZoneInterval`` for the period after the transition, ending at the end of time.
:return: The zone interval for the period after the transition, ending at the end of time.
"""
return self.__late_interval

@property
def transition(self) -> Instant:
"""Gets the transition instant of the zone.
:return: The transition instant of the zone.
"""
return self.early_interval.end

def __init__(
self,
transition_point: Instant,
offset_before: Offset | int,
offset_after: Offset | int,
id_: str = "Single",
) -> None:
"""Creates a zone with a single transition between two offsets.
:param transition_point: The transition point as an ``Instant``.
:param offset_before: The offset of local time from UTC before the transition.
:param offset_after: The offset of local time from UTC before the transition.
:param id_:
"""
if isinstance(offset_before, int):
offset_before = Offset.from_hours(offset_before)
if isinstance(offset_after, int):
offset_after = Offset.from_hours(offset_after)
super().__init__(id_, False, min(offset_before, offset_after), max(offset_before, offset_after))
self.__early_interval = ZoneInterval(
name=id_ + "-Early", start=None, end=transition_point, wall_offset=offset_before, savings=Offset.zero
)
self.__late_interval = ZoneInterval(
name=id_ + "-Late", start=transition_point, end=None, wall_offset=offset_after, savings=Offset.zero
)

def get_zone_interval(self, instant: Instant) -> ZoneInterval:
return self.early_interval if instant in self.early_interval else self.late_interval
Loading

0 comments on commit 8e3326c

Please sign in to comment.