123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994 |
- # Copyright (c) "Neo4j"
- # Neo4j Sweden AB [https://neo4j.com]
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # https://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- """
- Fundamental temporal types for interchange with the DBMS.
- This module contains the fundamental types used for temporal accounting as well
- as a number of utility functions.
- """
- from __future__ import annotations
- import re
- import typing as t
- from datetime import (
- date,
- datetime,
- time,
- timedelta,
- timezone,
- tzinfo as _tzinfo,
- )
- from functools import total_ordering
- from re import compile as re_compile
- from time import (
- gmtime,
- mktime,
- struct_time,
- )
- if t.TYPE_CHECKING:
- import typing_extensions as te
- from ._arithmetic import (
- round_half_to_even,
- symmetric_divmod,
- )
- from ._metaclasses import (
- DateTimeType,
- DateType,
- TimeType,
- )
- __all__ = [
- "MAX_INT64",
- "MAX_YEAR",
- "MIN_INT64",
- "MIN_YEAR",
- "Date",
- "DateTime",
- "Duration",
- "Midday",
- "Midnight",
- "Never",
- "Time",
- "UnixEpoch",
- "ZeroDate",
- ]
- MIN_INT64 = -(2**63)
- MAX_INT64 = (2**63) - 1
- #: The smallest year number allowed in a :class:`.Date` or :class:`.DateTime`
- #: object to be compatible with :class:`datetime.date` and
- #: :class:`datetime.datetime`.
- MIN_YEAR: te.Final[int] = 1
- #: The largest year number allowed in a :class:`.Date` or :class:`.DateTime`
- #: object to be compatible with :class:`datetime.date` and
- #: :class:`datetime.datetime`.
- MAX_YEAR: te.Final[int] = 9999
- DATE_ISO_PATTERN = re_compile(r"^(\d{4})-(\d{2})-(\d{2})$")
- TIME_ISO_PATTERN = re_compile(
- r"^(\d{2})(:(\d{2})(:((\d{2})"
- r"(\.\d*)?))?)?(([+-])(\d{2}):(\d{2})(:((\d{2})(\.\d*)?))?)?$"
- )
- DURATION_ISO_PATTERN = re_compile(
- r"^P((\d+)Y)?((\d+)M)?((\d+)D)?"
- r"(T((\d+)H)?((\d+)M)?(((\d+)(\.\d+)?)?S)?)?$"
- )
- NANO_SECONDS = 1000000000
- AVERAGE_SECONDS_IN_MONTH = 2629746
- AVERAGE_SECONDS_IN_DAY = 86400
- FORMAT_F_REPLACE = re.compile(r"(?<!%)%f")
- def _is_leap_year(year):
- if year % 4 != 0:
- return False
- if year % 100 != 0:
- return True
- return year % 400 == 0
- IS_LEAP_YEAR = {
- year: _is_leap_year(year) for year in range(MIN_YEAR, MAX_YEAR + 1)
- }
- def _days_in_year(year):
- return 366 if IS_LEAP_YEAR[year] else 365
- DAYS_IN_YEAR = {
- year: _days_in_year(year) for year in range(MIN_YEAR, MAX_YEAR + 1)
- }
- def _days_in_month(year, month):
- if month in {9, 4, 6, 11}:
- return 30
- elif month != 2:
- return 31
- else:
- return 29 if IS_LEAP_YEAR[year] else 28
- DAYS_IN_MONTH = {
- (year, month): _days_in_month(year, month)
- for year in range(MIN_YEAR, MAX_YEAR + 1)
- for month in range(1, 13)
- }
- def _normalize_day(year, month, day):
- """
- Coerce the day of the month to an internal value.
- That value may or may not match the "public" value.
- Except for the last three days of every month, all days are stored as-is.
- The last three days are instead stored as -1 (the last), -2 (the second to
- last) and -3 (the third to last).
- Therefore, for a 28-day month, the last week is as follows:
- Day | 22 23 24 25 26 27 28
- Value | 22 23 24 25 -3 -2 -1
- For a 29-day month, the last week is as follows:
- Day | 23 24 25 26 27 28 29
- Value | 23 24 25 26 -3 -2 -1
- For a 30-day month, the last week is as follows:
- Day | 24 25 26 27 28 29 30
- Value | 24 25 26 27 -3 -2 -1
- For a 31-day month, the last week is as follows:
- Day | 25 26 27 28 29 30 31
- Value | 25 26 27 28 -3 -2 -1
- This slightly unintuitive system makes some temporal arithmetic
- produce a more desirable outcome.
- :param year:
- :param month:
- :param day:
- :returns:
- """
- if year < MIN_YEAR or year > MAX_YEAR:
- raise ValueError(f"Year out of range ({MIN_YEAR}..{MAX_YEAR})")
- if month < 1 or month > 12:
- raise ValueError("Month out of range (1..12)")
- days_in_month = DAYS_IN_MONTH[year, month]
- if day in {days_in_month, -1}:
- return year, month, -1
- if day in {days_in_month - 1, -2}:
- return year, month, -2
- if day in {days_in_month - 2, -3}:
- return year, month, -3
- if 1 <= day <= days_in_month - 3:
- return year, month, int(day)
- # TODO improve this error message
- raise ValueError(
- f"Day {day} out of range (1..{days_in_month}, -1, -2 ,-3)"
- )
- class ClockTime(tuple):
- """
- A count of `seconds` and `nanoseconds`.
- This class can be used to mark a particular point in time, relative to an
- externally-specified epoch.
- The `seconds` and `nanoseconds` values provided to the constructor can
- can have any sign but will be normalized internally into a positive or
- negative `seconds` value along with a positive `nanoseconds` value
- between `0` and `999,999,999`. Therefore, ``ClockTime(-1, -1)`` is
- normalized to ``ClockTime(-2, 999999999)``.
- Note that the structure of a :class:`.ClockTime` object is similar to
- the ``timespec`` struct in C.
- """
- def __new__(cls, seconds: float = 0, nanoseconds: int = 0) -> ClockTime:
- seconds, nanoseconds = divmod(
- int(NANO_SECONDS * seconds) + int(nanoseconds), NANO_SECONDS
- )
- return tuple.__new__(cls, (seconds, nanoseconds))
- def __add__(self, other):
- if isinstance(other, (int, float)):
- other = ClockTime(other)
- if isinstance(other, ClockTime):
- return ClockTime(
- self.seconds + other.seconds,
- self.nanoseconds + other.nanoseconds,
- )
- if isinstance(other, Duration):
- if other.months or other.days:
- raise ValueError("Cannot add Duration with months or days")
- return ClockTime(
- self.seconds + other.seconds,
- self.nanoseconds + int(other.nanoseconds),
- )
- return NotImplemented
- def __sub__(self, other):
- if isinstance(other, (int, float)):
- other = ClockTime(other)
- if isinstance(other, ClockTime):
- return ClockTime(
- self.seconds - other.seconds,
- self.nanoseconds - other.nanoseconds,
- )
- if isinstance(other, Duration):
- if other.months or other.days:
- raise ValueError(
- "Cannot subtract Duration with months or days"
- )
- return ClockTime(
- self.seconds - other.seconds,
- self.nanoseconds - int(other.nanoseconds),
- )
- return NotImplemented
- def __repr__(self):
- s, ns = self
- return f"ClockTime(seconds={s!r}, nanoseconds={ns!r})"
- @property
- def seconds(self):
- return self[0]
- @property
- def nanoseconds(self):
- return self[1]
- class Clock:
- """
- Accessor for time values.
- This class is fulfilled by implementations
- that subclass :class:`.Clock`. These implementations are contained within
- the ``neo4j.time.clock_implementations`` module, and are not intended to be
- accessed directly.
- Creating a new :class:`.Clock` instance will produce the highest
- precision clock implementation available.
- >>> clock = Clock()
- >>> type(clock) # doctest: +SKIP
- neo4j.time.clock_implementations.LibCClock
- >>> clock.local_time() # doctest: +SKIP
- ClockTime(seconds=1525265942, nanoseconds=506844026)
- """
- __implementations = None
- def __new__(cls):
- if cls.__implementations is None:
- # Find an available clock with the best precision
- import neo4j.time._clock_implementations # noqa: F401 needed to make subclasses available
- cls.__implementations = sorted(
- (
- clock
- for clock in Clock.__subclasses__()
- if clock.available()
- ),
- key=lambda clock: clock.precision(),
- reverse=True,
- )
- if not cls.__implementations:
- raise RuntimeError("No clock implementations available")
- return object.__new__(cls.__implementations[0])
- @classmethod
- def precision(cls):
- """
- Return precision (decimal places) of this clock implementation.
- The precision of this clock implementation, represented as a
- number of decimal places. Therefore, for a nanosecond precision
- clock, this function returns `9`.
- """
- raise NotImplementedError("No clock implementation selected")
- @classmethod
- def available(cls):
- """
- Indicate whether this clock implementation is available.
- A boolean flag to indicate whether this clock implementation is
- available on this platform.
- """
- raise NotImplementedError("No clock implementation selected")
- @classmethod
- def local_offset(cls):
- """
- Get the UTC offset of the local time.
- The offset from UTC for local time read from this clock.
- This may raise OverflowError if not supported, because of platform
- dependent C libraries.
- :returns:
- :rtype:
- :raises OverflowError:
- """
- # Adding and subtracting two days to avoid passing a pre-epoch time to
- # `mktime`, which can cause a `OverflowError` on some platforms (e.g.,
- # Windows).
- return ClockTime(-int(mktime(gmtime(172800))) + 172800)
- def local_time(self):
- """
- Get the current local time relative to the Unix Epoch.
- Read and return the current local time from this clock, measured
- relative to the Unix Epoch. This may raise OverflowError if not
- supported, because of platform dependent C libraries.
- :returns:
- :rtype:
- :raises OverflowError:
- """
- return self.utc_time() + self.local_offset()
- def utc_time(self):
- """
- Read and return the current UTC time from this clock.
- Time is measured relative to the Unix Epoch.
- """
- raise NotImplementedError("No clock implementation selected")
- if t.TYPE_CHECKING:
- # make typechecker believe that Duration subclasses datetime.timedelta
- # https://github.com/python/typeshed/issues/8409#issuecomment-1197704527
- duration_base_class = timedelta
- else:
- duration_base_class = object
- class Duration( # type: ignore[misc]
- t.Tuple[int, int, int, int], duration_base_class
- ):
- r"""
- A difference between two points in time.
- A :class:`.Duration` represents the difference between two points in time.
- Duration objects store a composite value of `months`, `days`, `seconds`,
- and `nanoseconds`. Unlike :class:`datetime.timedelta` however, days, and
- seconds/nanoseconds are never interchanged. All values except seconds and
- nanoseconds are applied separately in calculations (element-wise).
- A :class:`.Duration` stores four primary instance attributes internally:
- ``months``, ``days``, ``seconds`` and ``nanoseconds``. These are maintained
- as individual values and are immutable. Each of these four attributes can
- carry its own sign, except for ``nanoseconds``, which always has the same
- sign as ``seconds``. The constructor will establish this state, should the
- duration be initialized with conflicting ``seconds`` and ``nanoseconds``
- signs. This structure allows the modelling of durations such as
- ``3 months minus 2 days``.
- To determine if a :class:`Duration` ``d`` is overflowing the accepted
- values of the database, first, all ``nanoseconds`` outside the range
- -999_999_999 and 999_999_999 are transferred into the seconds field. Then,
- ``months``, ``days``, and ``seconds`` are summed up like so:
- ``months * 2629746 + days * 86400 + d.seconds
- + d.nanoseconds // 1000000000``.
- (Like the integer division in Python, this one is to be understood as
- rounding down rather than towards 0.)
- This value must be between -(2\ :sup:`63`) and (2\ :sup:`63` - 1)
- inclusive.
- :param years: will be added times 12 to `months`
- :param months: will be truncated to :class:`int` (`int(months)`)
- :param weeks: will be added times 7 to `days`
- :param days: will be truncated to :class:`int` (`int(days)`)
- :param hours: will be added times 3,600,000,000,000 to `nanoseconds`
- :param minutes: will be added times 60,000,000,000 to `nanoseconds`
- :param seconds: will be added times 1,000,000,000 to `nanoseconds``
- :param milliseconds: will be added times 1,000,000 to `nanoseconds`
- :param microseconds: will be added times 1,000 to `nanoseconds`
- :param nanoseconds: will be truncated to :class:`int` (`int(nanoseconds)`)
- :raises ValueError: the components exceed the limits as described above.
- """
- # i64: i64:i64: i32
- min: te.Final[Duration] = None # type: ignore
- """The lowest duration value possible."""
- max: te.Final[Duration] = None # type: ignore
- """The highest duration value possible."""
- def __new__(
- cls,
- years: float = 0,
- months: float = 0,
- weeks: float = 0,
- days: float = 0,
- hours: float = 0,
- minutes: float = 0,
- seconds: float = 0,
- milliseconds: float = 0,
- microseconds: float = 0,
- nanoseconds: float = 0,
- ) -> Duration:
- mo = int(12 * years + months)
- if mo < MIN_INT64 or mo > MAX_INT64:
- raise ValueError("Months value out of range")
- d = int(7 * weeks + days)
- ns = (
- int(3600000000000 * hours)
- + int(60000000000 * minutes)
- + int(1000000000 * seconds)
- + int(1000000 * milliseconds)
- + int(1000 * microseconds)
- + int(nanoseconds)
- )
- s, ns = symmetric_divmod(ns, NANO_SECONDS)
- tuple_ = (mo, d, s, ns)
- avg_total_seconds = (
- mo * AVERAGE_SECONDS_IN_MONTH
- + d * AVERAGE_SECONDS_IN_DAY
- + s
- - (1 if ns < 0 else 0)
- )
- if not MIN_INT64 <= avg_total_seconds <= MAX_INT64:
- raise ValueError(f"Duration value out of range: {tuple_!r}")
- # TODO: 6.0 - remove type ignore when support for Python 3.7 is dropped
- return tuple.__new__(cls, tuple_) # type: ignore[type-var]
- def __bool__(self) -> bool:
- """Falsy if all primary instance attributes are."""
- return any(map(bool, self))
- __nonzero__ = __bool__
- def __add__( # type: ignore[override]
- self, other: Duration | timedelta
- ) -> Duration:
- """Add a :class:`.Duration` or :class:`datetime.timedelta`."""
- if isinstance(other, Duration):
- return Duration(
- months=self[0] + int(other.months),
- days=self[1] + int(other.days),
- seconds=self[2] + int(other.seconds),
- nanoseconds=self[3] + int(other.nanoseconds),
- )
- if isinstance(other, timedelta):
- return Duration(
- months=self[0],
- days=self[1] + other.days,
- seconds=self[2] + other.seconds,
- nanoseconds=self[3] + other.microseconds * 1000,
- )
- return NotImplemented
- def __sub__(self, other: Duration | timedelta) -> Duration:
- """Subtract a :class:`.Duration` or :class:`datetime.timedelta`."""
- if isinstance(other, Duration):
- return Duration(
- months=self[0] - int(other.months),
- days=self[1] - int(other.days),
- seconds=self[2] - int(other.seconds),
- nanoseconds=self[3] - int(other.nanoseconds),
- )
- if isinstance(other, timedelta):
- return Duration(
- months=self[0],
- days=self[1] - other.days,
- seconds=self[2] - other.seconds,
- nanoseconds=self[3] - other.microseconds * 1000,
- )
- return NotImplemented
- def __mul__(self, other: float) -> Duration: # type: ignore[override]
- """
- Multiply by an :class:`int` or :class:`float`.
- The operation is performed element-wise on
- ``(months, days, nanaoseconds)`` where
- * years go into months,
- * weeks go into days,
- * seconds and all sub-second units go into nanoseconds.
- Each element will be rounded to the nearest integer (.5 towards even).
- """
- if isinstance(other, (int, float)):
- return Duration(
- months=round_half_to_even(self[0] * other),
- days=round_half_to_even(self[1] * other),
- nanoseconds=round_half_to_even(
- self[2] * NANO_SECONDS * other + self[3] * other
- ),
- )
- return NotImplemented
- def __floordiv__(self, other: int) -> Duration: # type: ignore[override]
- """
- Integer division by an :class:`int`.
- The operation is performed element-wise on
- ``(months, days, nanaoseconds)`` where
- * years go into months,
- * weeks go into days,
- * seconds and all sub-second units go into nanoseconds.
- Each element will be rounded towards -inf.
- """
- if isinstance(other, int):
- return Duration(
- months=self[0] // other,
- days=self[1] // other,
- nanoseconds=(self[2] * NANO_SECONDS + self[3]) // other,
- )
- return NotImplemented
- def __mod__(self, other: int) -> Duration: # type: ignore[override]
- """
- Modulo operation by an :class:`int`.
- The operation is performed element-wise on
- ``(months, days, nanaoseconds)`` where
- * years go into months,
- * weeks go into days,
- * seconds and all sub-second units go into nanoseconds.
- """
- if isinstance(other, int):
- return Duration(
- months=self[0] % other,
- days=self[1] % other,
- nanoseconds=(self[2] * NANO_SECONDS + self[3]) % other,
- )
- return NotImplemented
- def __divmod__( # type: ignore[override]
- self, other: int
- ) -> tuple[Duration, Duration]:
- """
- Division and modulo operation by an :class:`int`.
- See :meth:`__floordiv__` and :meth:`__mod__`.
- """
- if isinstance(other, int):
- return self.__floordiv__(other), self.__mod__(other)
- return NotImplemented
- def __truediv__(self, other: float) -> Duration: # type: ignore[override]
- """
- Division by an :class:`int` or :class:`float`.
- The operation is performed element-wise on
- ``(months, days, nanaoseconds)`` where
- * years go into months,
- * weeks go into days,
- * seconds and all sub-second units go into nanoseconds.
- Each element will be rounded to the nearest integer (.5 towards even).
- """
- if isinstance(other, (int, float)):
- return Duration(
- months=round_half_to_even(self[0] / other),
- days=round_half_to_even(self[1] / other),
- nanoseconds=round_half_to_even(
- self[2] * NANO_SECONDS / other + self[3] / other
- ),
- )
- return NotImplemented
- def __pos__(self) -> Duration:
- return self
- def __neg__(self) -> Duration:
- return Duration(
- months=-self[0],
- days=-self[1],
- seconds=-self[2],
- nanoseconds=-self[3],
- )
- def __abs__(self) -> Duration:
- return Duration(
- months=abs(self[0]),
- days=abs(self[1]),
- seconds=abs(self[2]),
- nanoseconds=abs(self[3]),
- )
- def __repr__(self) -> str:
- mo, day, sec, ns = self
- return (
- f"Duration(months={mo!r}, days={day!r}, seconds={sec!r}, "
- f"nanoseconds={ns!r})"
- )
- def __str__(self) -> str:
- return self.iso_format()
- def __reduce__(self):
- return type(self)._restore, (tuple(self), self.__dict__)
- @classmethod
- def _restore(cls, elements, dict_):
- instance = tuple.__new__(cls, elements)
- if dict_:
- instance.__dict__.update(dict_)
- return instance
- @classmethod
- def from_iso_format(cls, s: str) -> Duration:
- """
- Parse a ISO formatted duration string.
- Accepted formats (all lowercase letters are placeholders):
- 'P', a zero length duration
- 'PyY', y being a number of years
- 'PmM', m being a number of months
- 'PdD', d being a number of days
- Any combination of the above, e.g., 'P25Y1D' for 25 years and 1
- day.
- 'PThH', h being a number of hours
- 'PTmM', h being a number of minutes
- 'PTsS', h being a number of seconds
- 'PTs.sss...S', h being a fractional number of seconds
- Any combination of the above, e.g. 'PT5H1.2S' for 5 hours and 1.2
- seconds.
- Any combination of all options, e.g. 'P13MT100M' for 13 months and
- 100 minutes.
- :param s: String to parse
- :raises ValueError: if the string does not match the required format.
- """
- match = DURATION_ISO_PATTERN.match(s)
- if match:
- ns = 0
- if match.group(15):
- ns = int(match.group(15)[1:10].ljust(9, "0"))
- return cls(
- years=int(match.group(2) or 0),
- months=int(match.group(4) or 0),
- days=int(match.group(6) or 0),
- hours=int(match.group(9) or 0),
- minutes=int(match.group(11) or 0),
- seconds=int(match.group(14) or 0),
- nanoseconds=ns,
- )
- raise ValueError("Duration string must be in ISO format")
- fromisoformat = from_iso_format
- def iso_format(self, sep: str = "T") -> str:
- """
- Return the :class:`Duration` as ISO formatted string.
- :param sep: the separator before the time components.
- """
- parts = []
- hours, minutes, seconds, nanoseconds = (
- self.hours_minutes_seconds_nanoseconds
- )
- if hours:
- parts.append(f"{hours}H")
- if minutes:
- parts.append(f"{minutes}M")
- if nanoseconds:
- if seconds >= 0 and nanoseconds >= 0:
- ns_str = str(nanoseconds).rjust(9, "0").rstrip("0")
- parts.append(f"{seconds}.{ns_str}S")
- elif seconds <= 0 and nanoseconds <= 0:
- ns_str = str(abs(nanoseconds)).rjust(9, "0").rstrip("0")
- parts.append(f"-{abs(seconds)}.{ns_str}S")
- else:
- raise AssertionError("Please report this issue")
- elif seconds:
- parts.append(f"{seconds}S")
- if parts:
- parts.insert(0, sep)
- years, months, days = self.years_months_days
- if days:
- parts.insert(0, f"{days}D")
- if months:
- parts.insert(0, f"{months}M")
- if years:
- parts.insert(0, f"{years}Y")
- if parts:
- parts.insert(0, "P")
- return "".join(parts)
- else:
- return "PT0S"
- @property
- def months(self) -> int:
- """The months of the :class:`Duration`."""
- return self[0]
- @property
- def days(self) -> int:
- """The days of the :class:`Duration`."""
- return self[1]
- @property
- def seconds(self) -> int:
- """The seconds of the :class:`Duration`."""
- return self[2]
- @property
- def nanoseconds(self) -> int:
- """The nanoseconds of the :class:`Duration`."""
- return self[3]
- @property
- def years_months_days(self) -> tuple[int, int, int]:
- """
- Months and days components as a 3-tuple.
- tuple of years, months and days.
- """
- years, months = symmetric_divmod(self[0], 12)
- return years, months, self[1]
- @property
- def hours_minutes_seconds_nanoseconds(self) -> tuple[int, int, int, int]:
- """
- Seconds and nanoseconds components as a 4-tuple.
- tuple of hours, minutes, seconds and nanoseconds.
- """
- minutes, seconds = symmetric_divmod(self[2], 60)
- hours, minutes = symmetric_divmod(minutes, 60)
- return hours, minutes, seconds, self[3]
- Duration.min = Duration( # type: ignore
- seconds=MIN_INT64, nanoseconds=0
- )
- Duration.max = Duration( # type: ignore
- seconds=MAX_INT64, nanoseconds=999999999
- )
- if t.TYPE_CHECKING:
- # make typechecker believe that Date subclasses datetime.date
- # https://github.com/python/typeshed/issues/8409#issuecomment-1197704527
- date_base_class = date
- else:
- date_base_class = object
- class Date(date_base_class, metaclass=DateType):
- """
- Idealized date representation.
- A :class:`.Date` object represents a date (year, month, and day) in the
- `proleptic Gregorian Calendar
- <https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar>`_.
- Years between `0001` and `9999` are supported, with additional support for
- the "zero date" used in some contexts.
- Each date is based on a proleptic Gregorian ordinal, which models
- 1 Jan 0001 as `day 1` and counts each subsequent day up to, and including,
- 31 Dec 9999. The standard `year`, `month` and `day` value of each date is
- also available.
- Internally, the day of the month is always stored as-is, except for the
- last three days of that month. These are always stored as -1, -2 and -3
- (counting from the last day). This system allows some temporal
- arithmetic (particularly adding or subtracting months) to produce a more
- desirable outcome than would otherwise be produced. Externally, the day
- number is always the same as would be written on a calendar.
- :param year: the year. Minimum :data:`.MIN_YEAR` (0001), maximum
- :data:`.MAX_YEAR` (9999).
- :type year: int
- :param month: the month. Minimum 1, maximum 12.
- :type month: int
- :param day: the day. Minimum 1, maximum
- :attr:`Date.days_in_month(year, month) <Date.days_in_month>`.
- :type day: int
- A zero date can also be acquired by passing all zeroes to the
- :class:`.Date` constructor or by using the :data:`ZeroDate`
- constant.
- """
- # CONSTRUCTOR #
- def __new__(cls, year: int, month: int, day: int) -> Date:
- # TODO: 6.0 - remove the __new__ magic and ZeroDate being a singleton.
- # It's fine to remain as constant. Instead, simply use
- # __init__ and simplify pickle/copy (remove __reduce__).
- # N.B. this is a breaking change and must be treated as
- # such. Also consider introducing __slots__. Potentially
- # apply similar treatment to other temporal types as well
- # as spatial types.
- if year == month == day == 0:
- return ZeroDate
- year, month, day = _normalize_day(year, month, day)
- ordinal = cls.__calc_ordinal(year, month, day)
- return cls.__new(ordinal, year, month, day)
- @classmethod
- def __new(cls, ordinal: int, year: int, month: int, day: int) -> Date:
- instance = object.__new__(cls)
- instance.__ordinal = int(ordinal)
- instance.__year = int(year)
- instance.__month = int(month)
- instance.__day = int(day)
- return instance
- # CLASS METHODS #
- @classmethod
- def today(cls, tz: _tzinfo | None = None) -> Date:
- """
- Get the current date.
- :param tz: timezone or None to get the local :class:`.Date`.
- :raises OverflowError: if the timestamp is out of the range of values
- supported by the platform C localtime() function. It’s common for
- this to be restricted to years from 1970 through 2038.
- """
- if tz is None:
- return cls.from_clock_time(Clock().local_time(), UnixEpoch)
- else:
- return (
- DateTime.utc_now()
- .replace(tzinfo=timezone.utc)
- .astimezone(tz)
- .date()
- )
- @classmethod
- def utc_today(cls) -> Date:
- """Get the current date as UTC local date."""
- return cls.from_clock_time(Clock().utc_time(), UnixEpoch)
- @classmethod
- def from_timestamp(
- cls, timestamp: float, tz: _tzinfo | None = None
- ) -> Date:
- """
- Construct :class:`.Date` from a time stamp (seconds since unix epoch).
- :param timestamp: the unix timestamp (seconds since unix epoch).
- :param tz: timezone. Set to None to create a local :class:`.Date`.
- :raises OverflowError: if the timestamp is out of the range of values
- supported by the platform C localtime() function. It’s common for
- this to be restricted to years from 1970 through 2038.
- """
- return cls.from_native(datetime.fromtimestamp(timestamp, tz))
- @classmethod
- def utc_from_timestamp(cls, timestamp: float) -> Date:
- """
- Construct :class:`.Date` from a time stamp (seconds since unix epoch).
- :returns: the `Date` as local date `Date` in UTC.
- """
- return cls.from_clock_time((timestamp, 0), UnixEpoch)
- @classmethod
- def from_ordinal(cls, ordinal: int) -> Date:
- """
- Construct :class:`.Date` from the proleptic Gregorian ordinal.
- `0001-01-01` has ordinal 1 and `9999-12-31` has ordinal 3,652,059.
- Values outside of this range trigger a :exc:`ValueError`.
- The corresponding instance method for the reverse date-to-ordinal
- transformation is :meth:`.to_ordinal`.
- The ordinal 0 has a special semantic and will return :attr:`ZeroDate`.
- :raises ValueError: if the ordinal is outside the range [0, 3652059]
- (both values included).
- """
- if ordinal == 0:
- return ZeroDate
- elif ordinal < 0 or ordinal > 3652059:
- raise ValueError("Ordinal out of range (0..3652059)")
- d = datetime.fromordinal(ordinal)
- year, month, day = _normalize_day(d.year, d.month, d.day)
- return cls.__new(ordinal, year, month, day)
- @classmethod
- def parse(cls, s: str) -> Date:
- """
- Parse a string to produce a :class:`.Date`.
- Accepted formats:
- 'Y-M-D'
- :param s: the string to be parsed.
- :raises ValueError: if the string could not be parsed.
- """
- try:
- numbers = list(map(int, s.split("-")))
- except (ValueError, AttributeError):
- raise ValueError(
- "Date string must be in format YYYY-MM-DD"
- ) from None
- else:
- if len(numbers) == 3:
- return cls(*numbers)
- raise ValueError("Date string must be in format YYYY-MM-DD")
- @classmethod
- def from_iso_format(cls, s: str) -> Date:
- """
- Parse a ISO formatted Date string.
- Accepted formats:
- 'YYYY-MM-DD'
- :param s: the string to be parsed.
- :raises ValueError: if the string could not be parsed.
- """
- m = DATE_ISO_PATTERN.match(s)
- if m:
- year = int(m.group(1))
- month = int(m.group(2))
- day = int(m.group(3))
- return cls(year, month, day)
- raise ValueError("Date string must be in format YYYY-MM-DD")
- @classmethod
- def from_native(cls, d: date) -> Date:
- """
- Convert from a native Python `datetime.date` value.
- :param d: the date to convert.
- """
- return Date.from_ordinal(d.toordinal())
- @classmethod
- def from_clock_time(
- cls,
- clock_time: ClockTime | tuple[float, int],
- epoch: DateTime,
- ) -> Date:
- """
- Convert from a ClockTime relative to a given epoch.
- :param clock_time: the clock time as :class:`.ClockTime` or as tuple of
- (seconds, nanoseconds)
- :param epoch: the epoch to which `clock_time` is relative
- """
- try:
- clock_time = ClockTime(*clock_time)
- except (TypeError, ValueError):
- raise ValueError(
- "Clock time must be a 2-tuple of (s, ns)"
- ) from None
- else:
- ordinal = clock_time.seconds // 86400
- return Date.from_ordinal(ordinal + epoch.date().to_ordinal())
- @classmethod
- def is_leap_year(cls, year: int) -> bool:
- """
- Indicate whether `year` is a leap year.
- :param year: the year to look up
- :raises ValueError: if `year` is out of range:
- :attr:`MIN_YEAR` <= year <= :attr:`MAX_YEAR`
- """
- if year < MIN_YEAR or year > MAX_YEAR:
- raise ValueError(f"Year out of range ({MIN_YEAR}..{MAX_YEAR})")
- return IS_LEAP_YEAR[year]
- @classmethod
- def days_in_year(cls, year: int) -> int:
- """
- Return the number of days in `year`.
- :param year: the year to look up
- :raises ValueError: if `year` is out of range:
- :attr:`MIN_YEAR` <= year <= :attr:`MAX_YEAR`
- """
- if year < MIN_YEAR or year > MAX_YEAR:
- raise ValueError(f"Year out of range ({MIN_YEAR}..{MAX_YEAR})")
- return DAYS_IN_YEAR[year]
- @classmethod
- def days_in_month(cls, year: int, month: int) -> int:
- """
- Return the number of days in `month` of `year`.
- :param year: the year to look up
- :param month: the month to look up
- :raises ValueError: if `year` or `month` is out of range:
- :attr:`MIN_YEAR` <= year <= :attr:`MAX_YEAR`;
- 1 <= year <= 12
- """
- if year < MIN_YEAR or year > MAX_YEAR:
- raise ValueError(f"Year out of range ({MIN_YEAR}..{MAX_YEAR})")
- if month < 1 or month > 12:
- raise ValueError("Month out of range (1..12)")
- return DAYS_IN_MONTH[year, month]
- @classmethod
- def __calc_ordinal(cls, year, month, day):
- if day < 0:
- day = cls.days_in_month(year, month) + int(day) + 1
- # The built-in date class does this faster than a
- # long-hand pure Python algorithm could
- return date(year, month, day).toordinal()
- # CLASS METHOD ALIASES #
- if t.TYPE_CHECKING:
- @classmethod
- def fromisoformat(cls, s: str) -> Date: ...
- @classmethod
- def fromordinal(cls, ordinal: int) -> Date: ...
- @classmethod
- def fromtimestamp(
- cls, timestamp: float, tz: _tzinfo | None = None
- ) -> Date: ...
- @classmethod
- def utcfromtimestamp(cls, timestamp: float) -> Date: ...
- # CLASS ATTRIBUTES #
- min: te.Final[Date] = None # type: ignore
- """The earliest date value possible."""
- max: te.Final[Date] = None # type: ignore
- """The latest date value possible."""
- resolution: te.Final[Duration] = None # type: ignore
- """The minimum resolution supported."""
- # INSTANCE ATTRIBUTES #
- __ordinal = 0
- __year = 0
- __month = 0
- __day = 0
- @property
- def year(self) -> int:
- """
- The year of the date.
- :type: int
- """
- return self.__year
- @property
- def month(self) -> int:
- """
- The month of the date.
- :type: int
- """
- return self.__month
- @property
- def day(self) -> int:
- """
- The day of the date.
- :type: int
- """
- if self.__day == 0:
- return 0
- if self.__day >= 1:
- return self.__day
- return self.days_in_month(self.__year, self.__month) + self.__day + 1
- @property
- def year_month_day(self) -> tuple[int, int, int]:
- """3-tuple of (year, month, day) describing the date."""
- return self.year, self.month, self.day
- @property
- def year_week_day(self) -> tuple[int, int, int]:
- """
- 3-tuple of (year, week_of_year, day_of_week) describing the date.
- `day_of_week` will be 1 for Monday and 7 for Sunday.
- """
- ordinal = self.__ordinal
- year = self.__year
- def day_of_week(o):
- return ((o - 1) % 7) + 1
- def iso_week_1(y):
- j4 = Date(y, 1, 4)
- return j4 + Duration(days=(1 - day_of_week(j4.to_ordinal())))
- if ordinal >= Date(year, 12, 29).to_ordinal():
- week1 = iso_week_1(year + 1)
- if ordinal < week1.to_ordinal():
- week1 = iso_week_1(year)
- else:
- year += 1
- else:
- week1 = iso_week_1(year)
- if ordinal < week1.to_ordinal():
- year -= 1
- week1 = iso_week_1(year)
- return (
- year,
- int((ordinal - week1.to_ordinal()) / 7 + 1),
- day_of_week(ordinal),
- )
- @property
- def year_day(self) -> tuple[int, int]:
- """
- 2-tuple of (year, day_of_the_year) describing the date.
- This is the number of the day relative to the start of the year,
- with `1 Jan` corresponding to `1`.
- """
- return (
- self.__year,
- self.toordinal() - Date(self.__year, 1, 1).toordinal() + 1,
- )
- # OPERATIONS #
- def __hash__(self):
- return hash(self.toordinal())
- def __eq__(self, other: object) -> bool:
- """``==`` comparison with :class:`.Date` or :class:`datetime.date`."""
- if not isinstance(other, (Date, date)):
- # TODO: 6.0 - return NotImplemented for non-Date objects
- # return NotImplemented
- return False
- return self.toordinal() == other.toordinal()
- def __ne__(self, other: object) -> bool:
- """``!=`` comparison with :class:`.Date` or :class:`datetime.date`."""
- # TODO: 6.0 - return NotImplemented for non-Date objects
- # if not isinstance(other, (Date, date)):
- # return NotImplemented
- return not self.__eq__(other)
- def __lt__(self, other: Date | date) -> bool:
- """``<`` comparison with :class:`.Date` or :class:`datetime.date`."""
- if not isinstance(other, (Date, date)):
- # TODO: 6.0 - return NotImplemented for non-Date objects
- # return NotImplemented
- raise TypeError(
- "'<' not supported between instances of 'Date' and "
- f"{type(other).__name__!r}"
- )
- return self.toordinal() < other.toordinal()
- def __le__(self, other: Date | date) -> bool:
- """``<=`` comparison with :class:`.Date` or :class:`datetime.date`."""
- if not isinstance(other, (Date, date)):
- # TODO: 6.0 - return NotImplemented for non-Date objects
- # return NotImplemented
- raise TypeError(
- "'<=' not supported between instances of 'Date' and "
- f"{type(other).__name__!r}"
- )
- return self.toordinal() <= other.toordinal()
- def __ge__(self, other: Date | date) -> bool:
- """``>=`` comparison with :class:`.Date` or :class:`datetime.date`."""
- if not isinstance(other, (Date, date)):
- # TODO: 6.0 - return NotImplemented for non-Date objects
- # return NotImplemented
- raise TypeError(
- "'>=' not supported between instances of 'Date' and "
- f"{type(other).__name__!r}"
- )
- return self.toordinal() >= other.toordinal()
- def __gt__(self, other: Date | date) -> bool:
- """``>`` comparison with :class:`.Date` or :class:`datetime.date`."""
- if not isinstance(other, (Date, date)):
- # TODO: 6.0 - return NotImplemented for non-Date objects
- # return NotImplemented
- raise TypeError(
- "'>' not supported between instances of 'Date' and "
- f"{type(other).__name__!r}"
- )
- return self.toordinal() > other.toordinal()
- def __add__(self, other: Duration) -> Date: # type: ignore[override]
- """
- Add a :class:`.Duration`.
- :raises ValueError: if the added duration has a time component.
- """
- def add_months(d, months):
- overflow_years, month = divmod(months + d.__month - 1, 12)
- d.__year += overflow_years
- d.__month = month + 1
- def add_days(d, days):
- assert 1 <= d.__day <= 28 or -28 <= d.__day <= -1
- if d.__day >= 1:
- new_days = d.__day + days
- if 1 <= new_days <= 27:
- d.__day = new_days
- return
- d0 = Date.from_ordinal(d.__ordinal + days)
- d.__year, d.__month, d.__day = d0.__year, d0.__month, d0.__day
- if isinstance(other, Duration):
- if other.seconds or other.nanoseconds:
- raise ValueError(
- "Cannot add a Duration with seconds or nanoseconds to a "
- "Date"
- )
- if other.months == other.days == 0:
- return self
- new_date = self.replace()
- # Add days before months as the former sometimes
- # requires the current ordinal to be correct.
- if other.days:
- add_days(new_date, other.days)
- if other.months:
- add_months(new_date, other.months)
- new_date.__ordinal = self.__calc_ordinal(
- new_date.year, new_date.month, new_date.day
- )
- return new_date
- return NotImplemented
- @t.overload # type: ignore[override]
- def __sub__(self, other: Date | date) -> Duration: ...
- @t.overload
- def __sub__(self, other: Duration) -> Date: ...
- def __sub__(self, other):
- """
- Subtract a :class:`.Date` or :class:`.Duration`.
- :returns: If a :class:`.Date` is subtracted, the time between the two
- dates is returned as :class:`.Duration`. If a :class:`.Duration` is
- subtracted, a new :class:`.Date` is returned.
- :rtype: Date or Duration
- :raises ValueError: if the added duration has a time component.
- """
- if isinstance(other, (Date, date)):
- return Duration(days=(self.toordinal() - other.toordinal()))
- try:
- return self.__add__(-other)
- except TypeError:
- return NotImplemented
- def __reduce__(self):
- if self is ZeroDate:
- return "ZeroDate"
- return type(self)._restore, (self.__dict__,)
- @classmethod
- def _restore(cls, dict_) -> Date:
- instance = object.__new__(cls)
- if dict_:
- instance.__dict__.update(dict_)
- return instance
- # INSTANCE METHODS #
- if t.TYPE_CHECKING:
- def replace(
- self,
- year: te.SupportsIndex = ...,
- month: te.SupportsIndex = ...,
- day: te.SupportsIndex = ...,
- **kwargs: object,
- ) -> Date: ...
- else:
- def replace(self, **kwargs) -> Date:
- """
- Return a :class:`.Date` with one or more components replaced.
- :Keyword Arguments:
- * **year** (:class:`typing.SupportsIndex`):
- overwrite the year - default: `self.year`
- * **month** (:class:`typing.SupportsIndex`):
- overwrite the month - default: `self.month`
- * **day** (:class:`typing.SupportsIndex`):
- overwrite the day - default: `self.day`
- """
- return Date(
- int(kwargs.get("year", self.__year)),
- int(kwargs.get("month", self.__month)),
- int(kwargs.get("day", self.__day)),
- )
- def time_tuple(self) -> struct_time:
- """Convert the date to :class:`time.struct_time`."""
- _, _, day_of_week = self.year_week_day
- _, day_of_year = self.year_day
- return struct_time(
- (
- self.year,
- self.month,
- self.day,
- 0,
- 0,
- 0,
- day_of_week - 1,
- day_of_year,
- -1,
- )
- )
- def to_ordinal(self) -> int:
- """
- Get the date's proleptic Gregorian ordinal.
- The corresponding class method for the reverse ordinal-to-date
- transformation is :meth:`.Date.from_ordinal`.
- """
- return self.__ordinal
- def to_clock_time(self, epoch: Date | DateTime) -> ClockTime:
- """
- Convert the date to :class:`ClockTime` relative to `epoch`.
- :param epoch: the epoch to which the date is relative
- """
- try:
- return ClockTime(86400 * (self.to_ordinal() - epoch.to_ordinal()))
- except AttributeError:
- raise TypeError("Epoch has no ordinal value") from None
- def to_native(self) -> date:
- """Convert to a native Python :class:`datetime.date` value."""
- return date.fromordinal(self.to_ordinal())
- def weekday(self) -> int:
- """Get the day of the week where Monday is 0 and Sunday is 6."""
- return self.year_week_day[2] - 1
- def iso_weekday(self) -> int:
- """Get the day of the week where Monday is 1 and Sunday is 7."""
- return self.year_week_day[2]
- def iso_calendar(self) -> tuple[int, int, int]:
- """Return :attr:`.year_week_day`."""
- return self.year_week_day
- def iso_format(self) -> str:
- """Return the :class:`.Date` as ISO formatted string."""
- if self.__ordinal == 0:
- return "0000-00-00"
- year, month, day = self.year_month_day
- return f"{year:04d}-{month:02d}-{day:02d}"
- def __repr__(self) -> str:
- if self.__ordinal == 0:
- return "neo4j.time.ZeroDate"
- year, month, day = self.year_month_day
- return f"neo4j.time.Date({year!r}, {month!r}, {day!r})"
- def __str__(self) -> str:
- return self.iso_format()
- def __format__(self, format_spec):
- if not format_spec:
- return self.iso_format()
- format_spec = FORMAT_F_REPLACE.sub("000000000", format_spec)
- return self.to_native().__format__(format_spec)
- # INSTANCE METHOD ALIASES #
- def __getattr__(self, name):
- """
- Get attribute by name.
- Map standard library attribute names to local attribute names,
- for compatibility.
- """
- try:
- return {
- "isocalendar": self.iso_calendar,
- "isoformat": self.iso_format,
- "isoweekday": self.iso_weekday,
- "strftime": self.__format__,
- "toordinal": self.to_ordinal,
- "timetuple": self.time_tuple,
- }[name]
- except KeyError:
- raise AttributeError(f"Date has no attribute {name!r}") from None
- if t.TYPE_CHECKING:
- def iso_calendar(self) -> tuple[int, int, int]: ...
- isoformat = iso_format
- isoweekday = iso_weekday
- strftime = __format__
- toordinal = to_ordinal
- timetuple = time_tuple
- Date.min = Date.from_ordinal(1) # type: ignore
- Date.max = Date.from_ordinal(3652059) # type: ignore
- Date.resolution = Duration(days=1) # type: ignore
- #: A :class:`neo4j.time.Date` instance set to `0000-00-00`.
- #: This has an ordinal value of `0`.
- ZeroDate = object.__new__(Date)
- if t.TYPE_CHECKING:
- # make typechecker believe that Time subclasses datetime.time
- # https://github.com/python/typeshed/issues/8409#issuecomment-1197704527
- time_base_class = time
- else:
- time_base_class = object
- def _dst(
- tz: _tzinfo | None = None, dt: DateTime | None = None
- ) -> timedelta | None:
- if tz is None:
- return None
- try:
- value = tz.dst(dt)
- except TypeError:
- if dt is None:
- raise
- # For timezone implementations not compatible with the custom
- # datetime implementations, we can't do better than this.
- value = tz.dst(dt.to_native()) # type: ignore
- if value is None:
- return None
- if isinstance(value, timedelta):
- if value.days != 0:
- raise ValueError("dst must be less than a day")
- if value.seconds % 60 != 0 or value.microseconds != 0:
- raise ValueError("dst must be a whole number of minutes")
- return value
- raise TypeError("dst must be a timedelta")
- def _tz_name(tz: _tzinfo | None, dt: DateTime | None) -> str | None:
- if tz is None:
- return None
- try:
- return tz.tzname(dt)
- except TypeError:
- if dt is None:
- raise
- # For timezone implementations not compatible with the custom
- # datetime implementations, we can't do better than this.
- return tz.tzname(dt.to_native())
- class Time(time_base_class, metaclass=TimeType):
- """
- Time of day.
- The :class:`.Time` class is a nanosecond-precision drop-in replacement for
- the standard library :class:`datetime.time` class.
- A high degree of API compatibility with the standard library classes is
- provided.
- :class:`neo4j.time.Time` objects introduce the concept of ``ticks``.
- This is simply a count of the number of nanoseconds since midnight,
- in many ways analogous to the :class:`neo4j.time.Date` ordinal.
- `ticks` values are integers, with a minimum value of `0` and a maximum
- of `86_399_999_999_999`.
- Local times are represented by :class:`.Time` with no ``tzinfo``.
- :param hour: the hour of the time. Must be in range 0 <= hour < 24.
- :param minute: the minute of the time. Must be in range 0 <= minute < 60.
- :param second: the second of the time. Must be in range 0 <= second < 60.
- :param nanosecond: the nanosecond of the time.
- Must be in range 0 <= nanosecond < 999999999.
- :param tzinfo: timezone or None to get a local :class:`.Time`.
- :raises ValueError: if one of the parameters is out of range.
- .. versionchanged:: 5.0
- The parameter ``second`` no longer accepts :class:`float` values.
- """
- # CONSTRUCTOR #
- def __init__(
- self,
- hour: int = 0,
- minute: int = 0,
- second: int = 0,
- nanosecond: int = 0,
- tzinfo: _tzinfo | None = None,
- ) -> None:
- hour, minute, second, nanosecond = self.__normalize_nanosecond(
- hour, minute, second, nanosecond
- )
- ticks = (
- 3600000000000 * hour
- + 60000000000 * minute
- + 1000000000 * second
- + nanosecond
- )
- self.__unchecked_init(ticks, hour, minute, second, nanosecond, tzinfo)
- @classmethod
- def __unchecked_new(
- cls,
- ticks: int,
- hour: int,
- minutes: int,
- second: int,
- nano: int,
- tz: _tzinfo | None,
- ) -> Time:
- instance = object.__new__(Time)
- instance.__unchecked_init(ticks, hour, minutes, second, nano, tz)
- return instance
- def __unchecked_init(
- self,
- ticks: int,
- hour: int,
- minutes: int,
- second: int,
- nano: int,
- tz: _tzinfo | None,
- ) -> None:
- self.__ticks = ticks
- self.__hour = hour
- self.__minute = minutes
- self.__second = second
- self.__nanosecond = nano
- self.__tzinfo = tz
- # CLASS METHODS #
- @classmethod
- def now(cls, tz: _tzinfo | None = None) -> Time:
- """
- Get the current time.
- :param tz: optional timezone
- :raises OverflowError: if the timestamp is out of the range of values
- supported by the platform C localtime() function. It’s common for
- this to be restricted to years from 1970 through 2038.
- """
- if tz is None:
- return cls.from_clock_time(Clock().local_time(), UnixEpoch)
- else:
- return (
- DateTime.utc_now()
- .replace(tzinfo=timezone.utc)
- .astimezone(tz)
- .timetz()
- )
- @classmethod
- def utc_now(cls) -> Time:
- """Get the current time as UTC local time."""
- return cls.from_clock_time(Clock().utc_time(), UnixEpoch)
- @classmethod
- def from_iso_format(cls, s: str) -> Time:
- """
- Parse a ISO formatted time string.
- Accepted formats:
- Local times:
- 'hh'
- 'hh:mm'
- 'hh:mm:ss'
- 'hh:mm:ss.ssss...'
- Times with timezones (UTC offset):
- '<local time>+hh:mm'
- '<local time>+hh:mm:ss'
- '<local time>+hh:mm:ss.ssss....'
- '<local time>-hh:mm'
- '<local time>-hh:mm:ss'
- '<local time>-hh:mm:ss.ssss....'
- Where the UTC offset will only respect hours and minutes.
- Seconds and sub-seconds are ignored.
- :param s: String to parse
- :raises ValueError: if the string does not match the required format.
- """
- from pytz import FixedOffset # type: ignore
- m = TIME_ISO_PATTERN.match(s)
- if m:
- hour = int(m.group(1))
- minute = int(m.group(3) or 0)
- second = int(m.group(6) or 0)
- nanosecond = m.group(7)
- if nanosecond:
- nanosecond = int(nanosecond[1:10].ljust(9, "0"))
- else:
- nanosecond = 0
- if m.group(8) is None:
- return cls(hour, minute, second, nanosecond)
- else:
- offset_multiplier = 1 if m.group(9) == "+" else -1
- offset_hour = int(m.group(10))
- offset_minute = int(m.group(11))
- # pytz only supports offsets of minute resolution
- # so we can ignore this part
- # offset_second = float(m.group(13) or 0.0)
- offset = 60 * offset_hour + offset_minute
- tz = FixedOffset(offset_multiplier * offset)
- return cls(hour, minute, second, nanosecond, tzinfo=tz)
- raise ValueError("Time string is not in ISO format")
- @classmethod
- def from_ticks(cls, ticks: int, tz: _tzinfo | None = None) -> Time:
- """
- Create a time from ticks (nanoseconds since midnight).
- :param ticks: nanoseconds since midnight
- :param tz: optional timezone
- :raises ValueError: if ticks is out of bounds
- (0 <= ticks < 86400000000000)
- .. versionchanged:: 5.0
- The parameter ``ticks`` no longer accepts :class:`float` values
- but only :class:`int`. It's now nanoseconds since midnight instead
- of seconds.
- """
- if not isinstance(ticks, int):
- raise TypeError("Ticks must be int")
- if 0 <= ticks < 86400000000000:
- second, nanosecond = divmod(ticks, NANO_SECONDS)
- minute, second = divmod(second, 60)
- hour, minute = divmod(minute, 60)
- return cls.__unchecked_new(
- ticks, hour, minute, second, nanosecond, tz
- )
- raise ValueError("Ticks out of range (0..86400000000000)")
- @classmethod
- def from_native(cls, t: time) -> Time:
- """
- Convert from a native Python :class:`datetime.time` value.
- :param t: time to convert from
- """
- nanosecond = t.microsecond * 1000
- return Time(t.hour, t.minute, t.second, nanosecond, t.tzinfo)
- @classmethod
- def from_clock_time(
- cls,
- clock_time: ClockTime | tuple[float, int],
- epoch: DateTime,
- ) -> Time:
- """
- Convert from a :class:`.ClockTime` relative to a given epoch.
- This method, in contrast to most others of this package, assumes days
- of exactly 24 hours.
- :param clock_time: the clock time as :class:`.ClockTime` or as tuple of
- (seconds, nanoseconds)
- :param epoch: the epoch to which `clock_time` is relative
- """
- clock_time = ClockTime(*clock_time)
- ts = clock_time.seconds % 86400
- nanoseconds = int(NANO_SECONDS * ts + clock_time.nanoseconds)
- ticks = (epoch.time().ticks + nanoseconds) % (86400 * NANO_SECONDS)
- return Time.from_ticks(ticks)
- @classmethod
- def __normalize_hour(cls, hour):
- hour = int(hour)
- if 0 <= hour < 24:
- return hour
- raise ValueError("Hour out of range (0..23)")
- @classmethod
- def __normalize_minute(cls, hour, minute):
- hour = cls.__normalize_hour(hour)
- minute = int(minute)
- if 0 <= minute < 60:
- return hour, minute
- raise ValueError("Minute out of range (0..59)")
- @classmethod
- def __normalize_second(cls, hour, minute, second):
- hour, minute = cls.__normalize_minute(hour, minute)
- second = int(second)
- if 0 <= second < 60:
- return hour, minute, second
- raise ValueError("Second out of range (0..59)")
- @classmethod
- def __normalize_nanosecond(cls, hour, minute, second, nanosecond):
- hour, minute, second = cls.__normalize_second(hour, minute, second)
- if 0 <= nanosecond < NANO_SECONDS:
- return hour, minute, second, nanosecond
- raise ValueError(f"Nanosecond out of range (0..{NANO_SECONDS - 1})")
- # CLASS METHOD ALIASES #
- if t.TYPE_CHECKING:
- @classmethod
- def from_iso_format(cls, s: str) -> Time: ...
- @classmethod
- def utc_now(cls) -> Time: ...
- # CLASS ATTRIBUTES #
- min: te.Final[Time] = None # type: ignore
- """The earliest time value possible."""
- max: te.Final[Time] = None # type: ignore
- """The latest time value possible."""
- resolution: te.Final[Duration] = None # type: ignore
- """The minimum resolution supported."""
- # INSTANCE ATTRIBUTES #
- __ticks = 0
- __hour = 0
- __minute = 0
- __second = 0
- __nanosecond = 0
- __tzinfo: _tzinfo | None = None
- @property
- def ticks(self) -> int:
- """
- The total number of nanoseconds since midnight.
- .. versionchanged:: 5.0
- The property's type changed from :class:`float` to :class:`int`.
- It's now nanoseconds since midnight instead of seconds.
- """
- return self.__ticks
- @property
- def hour(self) -> int:
- """The hours of the time."""
- return self.__hour
- @property
- def minute(self) -> int:
- """The minutes of the time."""
- return self.__minute
- @property
- def second(self) -> int:
- """
- The seconds of the time.
- .. versionchanged:: 4.4
- The property's type changed from :class:`float` to
- :class:`decimal.Decimal` to mitigate rounding issues.
- .. versionchanged:: 5.0
- The property's type changed from :class:`decimal.Decimal` to
- :class:`int`. It does not longer cary sub-second information.
- Use :attr:`nanosecond` instead.
- """
- return self.__second
- @property
- def nanosecond(self) -> int:
- """The nanoseconds of the time."""
- return self.__nanosecond
- @property
- def hour_minute_second_nanosecond(self) -> tuple[int, int, int, int]:
- """The time as a tuple of (hour, minute, second, nanosecond)."""
- return self.__hour, self.__minute, self.__second, self.__nanosecond
- @property
- def tzinfo(self) -> _tzinfo | None:
- """The timezone of this time."""
- return self.__tzinfo
- # OPERATIONS #
- def _get_both_normalized_ticks(self, other: object, strict=True):
- if isinstance(other, (time, Time)) and (
- (self.utc_offset() is None) ^ (other.utcoffset() is None)
- ):
- if strict:
- raise TypeError(
- "can't compare offset-naive and offset-aware times"
- )
- else:
- return None, None
- other_ticks: int
- if isinstance(other, Time):
- other_ticks = other.__ticks
- elif isinstance(other, time):
- other_ticks = int(
- 3600000000000 * other.hour
- + 60000000000 * other.minute
- + NANO_SECONDS * other.second
- + 1000 * other.microsecond
- )
- else:
- return None, None
- assert isinstance(other, (Time, time))
- utc_offset: timedelta | None = other.utcoffset()
- if utc_offset is not None:
- other_ticks -= int(utc_offset.total_seconds() * NANO_SECONDS)
- self_ticks = self.__ticks
- utc_offset = self.utc_offset()
- if utc_offset is not None:
- self_ticks -= int(utc_offset.total_seconds() * NANO_SECONDS)
- return self_ticks, other_ticks
- def __hash__(self):
- if self.__nanosecond % 1000 == 0:
- return hash(self.to_native())
- self_ticks = self.__ticks
- if self.utc_offset() is not None:
- self_ticks -= self.utc_offset().total_seconds() * NANO_SECONDS
- return hash(self_ticks)
- def __eq__(self, other: object) -> bool:
- """`==` comparison with :class:`.Time` or :class:`datetime.time`."""
- self_ticks, other_ticks = self._get_both_normalized_ticks(
- other, strict=False
- )
- if self_ticks is None:
- return False
- return self_ticks == other_ticks
- def __ne__(self, other: object) -> bool:
- """`!=` comparison with :class:`.Time` or :class:`datetime.time`."""
- return not self.__eq__(other)
- def __lt__(self, other: Time | time) -> bool:
- """`<` comparison with :class:`.Time` or :class:`datetime.time`."""
- self_ticks, other_ticks = self._get_both_normalized_ticks(other)
- if self_ticks is None:
- return NotImplemented
- return self_ticks < other_ticks
- def __le__(self, other: Time | time) -> bool:
- """`<=` comparison with :class:`.Time` or :class:`datetime.time`."""
- self_ticks, other_ticks = self._get_both_normalized_ticks(other)
- if self_ticks is None:
- return NotImplemented
- return self_ticks <= other_ticks
- def __ge__(self, other: Time | time) -> bool:
- """`>=` comparison with :class:`.Time` or :class:`datetime.time`."""
- self_ticks, other_ticks = self._get_both_normalized_ticks(other)
- if self_ticks is None:
- return NotImplemented
- return self_ticks >= other_ticks
- def __gt__(self, other: Time | time) -> bool:
- """`>` comparison with :class:`.Time` or :class:`datetime.time`."""
- self_ticks, other_ticks = self._get_both_normalized_ticks(other)
- if self_ticks is None:
- return NotImplemented
- return self_ticks > other_ticks
- # INSTANCE METHODS #
- if t.TYPE_CHECKING:
- def replace( # type: ignore[override]
- self,
- hour: te.SupportsIndex = ...,
- minute: te.SupportsIndex = ...,
- second: te.SupportsIndex = ...,
- nanosecond: te.SupportsIndex = ...,
- tzinfo: _tzinfo | None = ...,
- **kwargs: object,
- ) -> Time: ...
- else:
- def replace(self, **kwargs) -> Time:
- """
- Return a :class:`.Time` with one or more components replaced.
- :Keyword Arguments:
- * **hour** (:class:`typing.SupportsIndex`):
- overwrite the hour - default: `self.hour`
- * **minute** (:class:`typing.SupportsIndex`):
- overwrite the minute - default: `self.minute`
- * **second** (:class:`typing.SupportsIndex`):
- overwrite the second - default: `int(self.second)`
- * **nanosecond** (:class:`typing.SupportsIndex`):
- overwrite the nanosecond - default: `self.nanosecond`
- * **tzinfo** (:class:`datetime.tzinfo` or `None`):
- overwrite the timezone - default: `self.tzinfo`
- """
- return Time(
- hour=int(kwargs.get("hour", self.__hour)),
- minute=int(kwargs.get("minute", self.__minute)),
- second=int(kwargs.get("second", self.__second)),
- nanosecond=int(kwargs.get("nanosecond", self.__nanosecond)),
- tzinfo=kwargs.get("tzinfo", self.__tzinfo),
- )
- def _utc_offset(self, dt=None):
- if self.tzinfo is None:
- return None
- try:
- value = self.tzinfo.utcoffset(dt)
- except TypeError:
- # For timezone implementations not compatible with the custom
- # datetime implementations, we can't do better than this.
- value = self.tzinfo.utcoffset(dt.to_native())
- if value is None:
- return None
- if isinstance(value, timedelta):
- s = value.total_seconds()
- if not (-86400 < s < 86400):
- raise ValueError("utcoffset must be less than a day")
- if s % 60 != 0 or value.microseconds != 0:
- raise ValueError("utcoffset must be a whole number of minutes")
- return value
- raise TypeError("utcoffset must be a timedelta")
- def utc_offset(self) -> timedelta | None:
- """
- Return the UTC offset of this time.
- :returns: None if this is a local time (:attr:`.tzinfo` is None), else
- returns `self.tzinfo.utcoffset(self)`.
- :raises ValueError: if `self.tzinfo.utcoffset(self)` is not None and a
- :class:`timedelta` with a magnitude greater equal 1 day or that is
- not a whole number of minutes.
- :raises TypeError: if `self.tzinfo.utcoffset(self)` does return
- anything but :data:`None` or a :class:`datetime.timedelta`.
- """
- return self._utc_offset()
- def dst(self) -> timedelta | None:
- """
- Get the daylight saving time adjustment (DST).
- :returns: None if this is a local time (:attr:`.tzinfo` is None), else
- returns `self.tzinfo.dst(self)`.
- :raises ValueError: if `self.tzinfo.dst(self)` is not None and a
- :class:`timedelta` with a magnitude greater equal 1 day or that is
- not a whole number of minutes.
- :raises TypeError: if `self.tzinfo.dst(self)` does return anything but
- None or a :class:`datetime.timedelta`.
- """
- return _dst(self.tzinfo, None)
- def tzname(self) -> str | None:
- """
- Get the name of the :class:`.Time`'s timezone.
- :returns: None if the time is local (i.e., has no timezone), else
- return `self.tzinfo.tzname(self)`
- """
- return _tz_name(self.tzinfo, None)
- def to_clock_time(self) -> ClockTime:
- """Convert to :class:`.ClockTime`."""
- seconds, nanoseconds = divmod(self.ticks, NANO_SECONDS)
- return ClockTime(seconds, nanoseconds)
- def to_native(self) -> time:
- """
- Convert to a native Python `datetime.time` value.
- This conversion is lossy as the native time implementation only
- supports a resolution of microseconds instead of nanoseconds.
- """
- h, m, s, ns = self.hour_minute_second_nanosecond
- µs = round_half_to_even(ns / 1000)
- tz = self.tzinfo
- return time(h, m, s, µs, tz)
- def iso_format(self) -> str:
- """Return the :class:`.Time` as ISO formatted string."""
- h, m, s, ns = self.hour_minute_second_nanosecond
- res = f"{h:02d}:{m:02d}:{s:02d}.{ns:09d}"
- offset = self.utc_offset()
- if offset is not None:
- offset_hours, offset_minutes = divmod(
- int(offset.total_seconds() // 60), 60
- )
- res += f"{offset_hours:+03d}:{offset_minutes:02d}"
- return res
- def __repr__(self) -> str:
- fields: tuple[str, ...]
- if self.tzinfo is None:
- fields = tuple(map(repr, self.hour_minute_second_nanosecond))
- else:
- fields = (
- *map(repr, self.hour_minute_second_nanosecond),
- f"tzinfo={self.tzinfo!r}",
- )
- return f"neo4j.time.Time({', '.join(fields)})"
- def __str__(self) -> str:
- return self.iso_format()
- def __format__(self, format_spec):
- if not format_spec:
- return self.iso_format()
- format_spec = FORMAT_F_REPLACE.sub(
- f"{self.__nanosecond:09}", format_spec
- )
- return self.to_native().__format__(format_spec)
- # INSTANCE METHOD ALIASES #
- def __getattr__(self, name):
- """
- Get attribute by name.
- Map standard library attribute names to local attribute names,
- for compatibility.
- """
- try:
- return {
- "isoformat": self.iso_format,
- "utcoffset": self.utc_offset,
- }[name]
- except KeyError:
- raise AttributeError(f"Date has no attribute {name!r}") from None
- if t.TYPE_CHECKING:
- def isoformat(self) -> str: # type: ignore[override]
- ...
- utcoffset = utc_offset
- Time.min = Time( # type: ignore
- hour=0, minute=0, second=0, nanosecond=0
- )
- Time.max = Time( # type: ignore
- hour=23, minute=59, second=59, nanosecond=999999999
- )
- Time.resolution = Duration( # type: ignore
- nanoseconds=1
- )
- #: A :class:`.Time` instance set to `00:00:00`.
- #: This has a :attr:`.ticks` value of `0`.
- Midnight: te.Final[Time] = Time.min
- #: A :class:`.Time` instance set to `12:00:00`.
- #: This has a :attr:`.ticks` value of `43200000000000`.
- Midday: te.Final[Time] = Time(hour=12)
- if t.TYPE_CHECKING:
- # make typechecker believe that DateTime subclasses datetime.datetime
- # https://github.com/python/typeshed/issues/8409#issuecomment-1197704527
- date_time_base_class = datetime
- else:
- date_time_base_class = object
- @total_ordering
- class DateTime(date_time_base_class, metaclass=DateTimeType):
- """
- A point in time represented as a date and a time.
- The :class:`.DateTime` class is a nanosecond-precision drop-in replacement
- for the standard library :class:`datetime.datetime` class.
- As such, it contains both :class:`.Date` and :class:`.Time` information and
- draws functionality from those individual classes.
- A :class:`.DateTime` object is fully compatible with the Python time zone
- library `pytz <https://pypi.org/project/pytz/>`_. Functions such as
- `normalize` and `localize` can be used in the same way as they are with the
- standard library classes.
- Regular construction of a :class:`.DateTime` object requires at
- least the `year`, `month` and `day` arguments to be supplied. The
- optional `hour`, `minute` and `second` arguments default to zero and
- `tzinfo` defaults to :data:`None`.
- `year`, `month`, and `day` are passed to the constructor of :class:`.Date`.
- `hour`, `minute`, `second`, `nanosecond`, and `tzinfo` are passed to the
- constructor of :class:`.Time`. See their documentation for more details.
- >>> dt = DateTime(2018, 4, 30, 12, 34, 56, 789123456); dt
- neo4j.time.DateTime(2018, 4, 30, 12, 34, 56, 789123456)
- >>> dt.second
- 56
- """
- __date: Date
- __time: Time
- # CONSTRUCTOR #
- def __new__(
- cls,
- year: int,
- month: int,
- day: int,
- hour: int = 0,
- minute: int = 0,
- second: int = 0,
- nanosecond: int = 0,
- tzinfo: _tzinfo | None = None,
- ) -> DateTime:
- return cls.combine(
- Date(year, month, day),
- Time(hour, minute, second, nanosecond, tzinfo),
- )
- # CLASS METHODS #
- @classmethod
- def now(cls, tz: _tzinfo | None = None) -> DateTime:
- """
- Get the current date and time.
- :param tz: timezone. Set to None to create a local :class:`.DateTime`.
- :raises OverflowError: if the timestamp is out of the range of values
- supported by the platform C localtime() function. It’s common for
- this to be restricted to years from 1970 through 2038.
- """
- if tz is None:
- return cls.from_clock_time(Clock().local_time(), UnixEpoch)
- else:
- utc_now = cls.from_clock_time(
- Clock().utc_time(), UnixEpoch
- ).replace(tzinfo=tz)
- try:
- return tz.fromutc(utc_now) # type: ignore
- except TypeError:
- # For timezone implementations not compatible with the custom
- # datetime implementations, we can't do better than this.
- utc_now_native = utc_now.to_native()
- now_native = tz.fromutc(utc_now_native)
- now = cls.from_native(now_native)
- return now.replace(
- nanosecond=(
- now.nanosecond
- + utc_now.nanosecond
- - utc_now_native.microsecond * 1000
- )
- )
- @classmethod
- def utc_now(cls) -> DateTime:
- """Get the current date and time in UTC."""
- return cls.from_clock_time(Clock().utc_time(), UnixEpoch)
- @classmethod
- def from_iso_format(cls, s) -> DateTime:
- """
- Parse a ISO formatted date with time string.
- :param s: String to parse
- :raises ValueError: if the string does not match the ISO format.
- """
- try:
- return cls.combine(
- Date.from_iso_format(s[0:10]), Time.from_iso_format(s[11:])
- )
- except ValueError as e:
- raise ValueError("DateTime string is not in ISO format") from e
- @classmethod
- def from_timestamp(
- cls, timestamp: float, tz: _tzinfo | None = None
- ) -> DateTime:
- """
- :class:`.DateTime` from a time stamp (seconds since unix epoch).
- :param timestamp: the unix timestamp (seconds since unix epoch).
- :param tz: timezone. Set to None to create a local :class:`.DateTime`.
- :raises OverflowError: if the timestamp is out of the range of values
- supported by the platform C localtime() function. It’s common for
- this to be restricted to years from 1970 through 2038.
- """
- if tz is None:
- return cls.from_clock_time(
- ClockTime(timestamp) + Clock().local_offset(), UnixEpoch
- )
- else:
- return (
- cls.utc_from_timestamp(timestamp)
- .replace(tzinfo=timezone.utc)
- .astimezone(tz)
- )
- @classmethod
- def utc_from_timestamp(cls, timestamp: float) -> DateTime:
- """
- :class:`.DateTime` from a time stamp (seconds since unix epoch).
- Returns the `DateTime` as local date `DateTime` in UTC.
- """
- return cls.from_clock_time((timestamp, 0), UnixEpoch)
- @classmethod
- def from_ordinal(cls, ordinal: int) -> DateTime:
- """
- :class:`.DateTime` from an ordinal.
- For more info about ordinals see :meth:`.Date.from_ordinal`.
- """
- return cls.combine(Date.from_ordinal(ordinal), Midnight)
- @classmethod
- def combine( # type: ignore[override]
- cls, date: Date, time: Time
- ) -> DateTime:
- """
- Combine a :class:`.Date` and a :class:`.Time` to a :class:`DateTime`.
- :param date: the date
- :param time: the time
- :raises AssertionError: if the parameter types don't match.
- """
- assert isinstance(date, Date)
- assert isinstance(time, Time)
- return cls._combine(date, time)
- @classmethod
- def _combine(cls, date: Date, time: Time) -> DateTime:
- instance = object.__new__(cls)
- instance.__date = date
- instance.__time = time
- return instance
- @classmethod
- def parse(cls, date_string, format):
- raise NotImplementedError
- @classmethod
- def from_native(cls, dt: datetime) -> DateTime:
- """
- Convert from a native Python :class:`datetime.datetime` value.
- :param dt: the datetime to convert
- """
- return cls.combine(
- Date.from_native(dt.date()), Time.from_native(dt.timetz())
- )
- @classmethod
- def from_clock_time(
- cls,
- clock_time: ClockTime | tuple[float, int],
- epoch: DateTime,
- ) -> DateTime:
- """
- Convert from a :class:`ClockTime` relative to a given epoch.
- :param clock_time: the clock time as :class:`.ClockTime` or as tuple of
- (seconds, nanoseconds)
- :param epoch: the epoch to which `clock_time` is relative
- :raises ValueError: if `clock_time` is invalid.
- """
- try:
- seconds, nanoseconds = ClockTime(*clock_time)
- except (TypeError, ValueError) as e:
- raise ValueError("Clock time must be a 2-tuple of (s, ns)") from e
- else:
- ordinal, seconds = divmod(seconds, 86400)
- ticks = epoch.time().ticks + seconds * NANO_SECONDS + nanoseconds
- days, ticks = divmod(ticks, 86400 * NANO_SECONDS)
- ordinal += days
- date_ = Date.from_ordinal(ordinal + epoch.date().to_ordinal())
- time_ = Time.from_ticks(ticks)
- return cls.combine(date_, time_)
- # CLASS METHOD ALIASES #
- if t.TYPE_CHECKING:
- @classmethod
- def fromisoformat(cls, s) -> DateTime: ...
- @classmethod
- def fromordinal(cls, ordinal: int) -> DateTime: ...
- @classmethod
- def fromtimestamp(
- cls, timestamp: float, tz: _tzinfo | None = None
- ) -> DateTime: ...
- # alias of parse
- @classmethod
- def strptime(cls, date_string, format): ...
- # alias of now
- @classmethod
- def today(cls, tz: _tzinfo | None = None) -> DateTime: ...
- @classmethod
- def utcfromtimestamp(cls, timestamp: float) -> DateTime: ...
- @classmethod
- def utcnow(cls) -> DateTime: ...
- # CLASS ATTRIBUTES #
- min: te.Final[DateTime] = None # type: ignore
- """The earliest date time value possible."""
- max: te.Final[DateTime] = None # type: ignore
- """The latest date time value possible."""
- resolution: te.Final[Duration] = None # type: ignore
- """The minimum resolution supported."""
- # INSTANCE ATTRIBUTES #
- @property
- def year(self) -> int:
- """
- The year of the :class:`.DateTime`.
- See :attr:`.Date.year`.
- """
- return self.__date.year
- @property
- def month(self) -> int:
- """
- The year of the :class:`.DateTime`.
- See :attr:`.Date.year`.
- """
- return self.__date.month
- @property
- def day(self) -> int:
- """
- The day of the :class:`.DateTime`'s date.
- See :attr:`.Date.day`.
- """
- return self.__date.day
- @property
- def year_month_day(self) -> tuple[int, int, int]:
- """
- The year_month_day of the :class:`.DateTime`'s date.
- See :attr:`.Date.year_month_day`.
- """
- return self.__date.year_month_day
- @property
- def year_week_day(self) -> tuple[int, int, int]:
- """
- The year_week_day of the :class:`.DateTime`'s date.
- See :attr:`.Date.year_week_day`.
- """
- return self.__date.year_week_day
- @property
- def year_day(self) -> tuple[int, int]:
- """
- The year_day of the :class:`.DateTime`'s date.
- See :attr:`.Date.year_day`.
- """
- return self.__date.year_day
- @property
- def hour(self) -> int:
- """
- The hour of the :class:`.DateTime`'s time.
- See :attr:`.Time.hour`.
- """
- return self.__time.hour
- @property
- def minute(self) -> int:
- """
- The minute of the :class:`.DateTime`'s time.
- See :attr:`.Time.minute`.
- """
- return self.__time.minute
- @property
- def second(self) -> int:
- """
- The second of the :class:`.DateTime`'s time.
- See :attr:`.Time.second`.
- """
- return self.__time.second
- @property
- def nanosecond(self) -> int:
- """
- The nanosecond of the :class:`.DateTime`'s time.
- See :attr:`.Time.nanosecond`.
- """
- return self.__time.nanosecond
- @property
- def tzinfo(self) -> _tzinfo | None:
- """
- The tzinfo of the :class:`.DateTime`'s time.
- See :attr:`.Time.tzinfo`.
- """
- return self.__time.tzinfo
- @property
- def hour_minute_second_nanosecond(self) -> tuple[int, int, int, int]:
- """
- The hour_minute_second_nanosecond of the :class:`.DateTime`'s time.
- See :attr:`.Time.hour_minute_second_nanosecond`.
- """
- return self.__time.hour_minute_second_nanosecond
- # OPERATIONS #
- def _get_both_normalized(self, other, strict=True):
- if isinstance(other, (datetime, DateTime)) and (
- (self.utc_offset() is None) ^ (other.utcoffset() is None)
- ):
- if strict:
- raise TypeError(
- "can't compare offset-naive and offset-aware datetimes"
- )
- else:
- return None, None
- self_norm = self
- utc_offset = self.utc_offset()
- if utc_offset is not None:
- self_norm -= utc_offset
- self_norm = self_norm.replace(tzinfo=None)
- other_norm = other
- if isinstance(other, (datetime, DateTime)):
- utc_offset = other.utcoffset()
- if utc_offset is not None:
- other_norm -= utc_offset
- other_norm = other_norm.replace(tzinfo=None)
- else:
- return None, None
- return self_norm, other_norm
- def __hash__(self):
- if self.nanosecond % 1000 == 0:
- return hash(self.to_native())
- self_norm = self
- utc_offset = self.utc_offset()
- if utc_offset is not None:
- self_norm -= utc_offset
- return hash(self_norm.date()) ^ hash(self_norm.time())
- def __eq__(self, other: object) -> bool:
- """
- ``==`` comparison with another datetime.
- Accepts :class:`.DateTime` and :class:`datetime.datetime`.
- """
- if not isinstance(other, (datetime, DateTime)):
- return NotImplemented
- if self.utc_offset() == other.utcoffset():
- return self.date() == other.date() and self.time() == other.time()
- self_norm, other_norm = self._get_both_normalized(other, strict=False)
- if self_norm is None:
- return False
- return self_norm == other_norm
- def __ne__(self, other: object) -> bool:
- """
- ``!=`` comparison with another datetime.
- Accepts :class:`.DateTime` and :class:`datetime.datetime`.
- """
- if not isinstance(other, (DateTime, datetime)):
- return NotImplemented
- return not self.__eq__(other)
- def __lt__( # type: ignore[override]
- self, other: datetime | DateTime
- ) -> bool:
- """
- ``<`` comparison with another datetime.
- Accepts :class:`.DateTime` and :class:`datetime.datetime`.
- """
- if not isinstance(other, (datetime, DateTime)):
- return NotImplemented
- if self.utc_offset() == other.utcoffset():
- if self.date() == other.date():
- return self.time() < other.time()
- return self.date() < other.date()
- self_norm, other_norm = self._get_both_normalized(other)
- return (
- self_norm.date() < other_norm.date()
- or self_norm.time() < other_norm.time()
- )
- def __le__( # type: ignore[override]
- self, other: datetime | DateTime
- ) -> bool:
- """
- ``<=`` comparison with another datetime.
- Accepts :class:`.DateTime` and :class:`datetime.datetime`.
- """
- if not isinstance(other, (datetime, DateTime)):
- return NotImplemented
- if self.utc_offset() == other.utcoffset():
- if self.date() == other.date():
- return self.time() <= other.time()
- return self.date() <= other.date()
- self_norm, other_norm = self._get_both_normalized(other)
- return self_norm <= other_norm
- def __ge__( # type: ignore[override]
- self, other: datetime | DateTime
- ) -> bool:
- """
- ``>=`` comparison with another datetime.
- Accepts :class:`.DateTime` and :class:`datetime.datetime`.
- """
- if not isinstance(other, (datetime, DateTime)):
- return NotImplemented
- if self.utc_offset() == other.utcoffset():
- if self.date() == other.date():
- return self.time() >= other.time()
- return self.date() >= other.date()
- self_norm, other_norm = self._get_both_normalized(other)
- return self_norm >= other_norm
- def __gt__( # type: ignore[override]
- self, other: object
- ) -> bool:
- """
- ``>`` comparison with another datetime.
- Accepts :class:`.DateTime` and :class:`datetime.datetime`.
- """
- if not isinstance(other, (datetime, DateTime)):
- return NotImplemented
- if self.utc_offset() == other.utcoffset():
- if self.date() == other.date():
- return self.time() > other.time()
- return self.date() > other.date()
- self_norm, other_norm = self._get_both_normalized(other)
- return (
- self_norm.date() > other_norm.date()
- or self_norm.time() > other_norm.time()
- )
- def __add__(self, other: timedelta | Duration) -> DateTime:
- """Add a :class:`datetime.timedelta`."""
- if isinstance(other, Duration):
- if other == (0, 0, 0, 0):
- return self
- t = self.time().to_clock_time() + ClockTime(
- other.seconds, other.nanoseconds
- )
- days, seconds = symmetric_divmod(t.seconds, 86400)
- date_ = self.date() + Duration(
- months=other.months, days=days + other.days
- )
- time_ = Time.from_ticks(seconds * NANO_SECONDS + t.nanoseconds)
- return self.combine(date_, time_).replace(tzinfo=self.tzinfo)
- if isinstance(other, timedelta):
- if other.total_seconds() == 0:
- return self
- t = self.to_clock_time() + ClockTime(
- 86400 * other.days + other.seconds,
- other.microseconds * 1000,
- )
- days, seconds = symmetric_divmod(t.seconds, 86400)
- date_ = Date.from_ordinal(days + 1)
- time_ = Time.from_ticks(
- round_half_to_even(seconds * NANO_SECONDS + t.nanoseconds)
- )
- return self.combine(date_, time_).replace(tzinfo=self.tzinfo)
- return NotImplemented
- @t.overload # type: ignore[override]
- def __sub__(self, other: DateTime) -> Duration: ...
- @t.overload
- def __sub__(self, other: datetime) -> timedelta: ...
- @t.overload
- def __sub__(self, other: Duration | timedelta) -> DateTime: ...
- def __sub__(self, other):
- """
- Subtract a datetime/DateTime or a timedelta/Duration.
- Subtracting a :class:`.DateTime` yields the duration between the two
- as a :class:`.Duration`.
- Subtracting a :class:`datetime.datetime` yields the duration between
- the two as a :class:`datetime.timedelta`.
- Subtracting a :class:`datetime.timedelta` or a :class:`.Duration`
- yields the :class:`.DateTime` that's the given duration away.
- """
- if isinstance(other, DateTime):
- self_month_ordinal = 12 * (self.year - 1) + self.month
- other_month_ordinal = 12 * (other.year - 1) + other.month
- months = self_month_ordinal - other_month_ordinal
- days = self.day - other.day
- t = self.time().to_clock_time() - other.time().to_clock_time()
- return Duration(
- months=months,
- days=days,
- seconds=t.seconds,
- nanoseconds=t.nanoseconds,
- )
- if isinstance(other, datetime):
- days = self.to_ordinal() - other.toordinal()
- t = self.time().to_clock_time() - ClockTime(
- 3600 * other.hour + 60 * other.minute + other.second,
- other.microsecond * 1000,
- )
- return timedelta(
- days=days,
- seconds=t.seconds,
- microseconds=(t.nanoseconds // 1000),
- )
- if isinstance(other, Duration):
- return self.__add__(-other)
- if isinstance(other, timedelta):
- return self.__add__(-other)
- return NotImplemented
- def __reduce__(self):
- return type(self)._restore, (self.__dict__,)
- @classmethod
- def _restore(cls, dict_):
- instance = object.__new__(cls)
- if dict_:
- instance.__dict__.update(dict_)
- return instance
- # INSTANCE METHODS #
- def date(self) -> Date:
- """Get the date."""
- return self.__date
- def time(self) -> Time:
- """Get the time without timezone info."""
- return self.__time.replace(tzinfo=None)
- def timetz(self) -> Time:
- """Get the time with timezone info."""
- return self.__time
- if t.TYPE_CHECKING:
- def replace( # type: ignore[override]
- self,
- year: te.SupportsIndex = ...,
- month: te.SupportsIndex = ...,
- day: te.SupportsIndex = ...,
- hour: te.SupportsIndex = ...,
- minute: te.SupportsIndex = ...,
- second: te.SupportsIndex = ...,
- nanosecond: te.SupportsIndex = ...,
- tzinfo: _tzinfo | None = ...,
- **kwargs: object,
- ) -> DateTime: ...
- else:
- def replace(self, **kwargs) -> DateTime:
- """
- Return a ``DateTime`` with one or more components replaced.
- See :meth:`.Date.replace` and :meth:`.Time.replace` for available
- arguments.
- """
- date_ = self.__date.replace(**kwargs)
- time_ = self.__time.replace(**kwargs)
- return self.combine(date_, time_)
- def as_timezone(self, tz: _tzinfo) -> DateTime:
- """
- Convert this :class:`.DateTime` to another timezone.
- :param tz: the new timezone
- :returns: the same object if ``tz`` is :data:``None``.
- Else, a new :class:`.DateTime` that's the same point in time but in
- a different timezone.
- """
- if self.tzinfo is None:
- return self
- offset = t.cast(timedelta, self.utcoffset())
- utc = (self - offset).replace(tzinfo=tz)
- try:
- return tz.fromutc(utc) # type: ignore
- except TypeError:
- # For timezone implementations not compatible with the custom
- # datetime implementations, we can't do better than this.
- native_utc = utc.to_native()
- native_res = tz.fromutc(native_utc)
- res = self.from_native(native_res)
- ns = native_res.microsecond * 1000 + self.nanosecond % 1000
- return res.replace(nanosecond=ns)
- def utc_offset(self) -> timedelta | None:
- """
- Get the date times utc offset.
- See :meth:`.Time.utc_offset`.
- """
- return self.__time._utc_offset(self)
- def dst(self) -> timedelta | None:
- """
- Get the daylight saving time adjustment (DST).
- See :meth:`.Time.dst`.
- """
- return _dst(self.tzinfo, self)
- def tzname(self) -> str | None:
- """
- Get the timezone name.
- See :meth:`.Time.tzname`.
- """
- return _tz_name(self.tzinfo, self)
- def time_tuple(self):
- raise NotImplementedError
- def utc_time_tuple(self):
- raise NotImplementedError
- def to_ordinal(self) -> int:
- """
- Get the ordinal of the :class:`.DateTime`'s date.
- See :meth:`.Date.to_ordinal`
- """
- return self.__date.to_ordinal()
- def to_clock_time(self) -> ClockTime:
- """Convert to :class:`.ClockTime`."""
- ordinal_seconds = 86400 * (self.__date.to_ordinal() - 1)
- time_seconds, nanoseconds = divmod(self.__time.ticks, NANO_SECONDS)
- return ClockTime(ordinal_seconds + time_seconds, nanoseconds)
- def to_native(self) -> datetime:
- """
- Convert to a native Python :class:`datetime.datetime` value.
- This conversion is lossy as the native time implementation only
- supports a resolution of microseconds instead of nanoseconds.
- """
- y, mo, d = self.year_month_day
- h, m, s, ns = self.hour_minute_second_nanosecond
- ms = int(ns / 1000)
- tz = self.tzinfo
- return datetime(y, mo, d, h, m, s, ms, tz)
- def weekday(self) -> int:
- """
- Get the weekday.
- See :meth:`.Date.weekday`
- """
- return self.__date.weekday()
- def iso_weekday(self) -> int:
- """
- Get the ISO weekday.
- See :meth:`.Date.iso_weekday`
- """
- return self.__date.iso_weekday()
- def iso_calendar(self) -> tuple[int, int, int]:
- """
- Get date as ISO tuple.
- See :meth:`.Date.iso_calendar`
- """
- return self.__date.iso_calendar()
- def iso_format(self, sep: str = "T") -> str:
- """
- Return the :class:`.DateTime` as ISO formatted string.
- This method joins `self.date().iso_format()` (see
- :meth:`.Date.iso_format`) and `self.timetz().iso_format()` (see
- :meth:`.Time.iso_format`) with `sep` in between.
- :param sep: the separator between the formatted date and time.
- """
- s_date = self.date().iso_format()
- s_time = self.timetz().iso_format()
- s = f"{s_date}{sep}{s_time}"
- time_tz = self.timetz()
- offset = time_tz.utc_offset()
- if offset is not None:
- # the time component will have taken care of formatting the offset
- return s
- offset = self.utc_offset()
- if offset is not None:
- offset_hours, offset_minutes = divmod(
- int(offset.total_seconds() // 60), 60
- )
- s += f"{offset_hours:+03d}:{offset_minutes:02d}"
- return s
- def __repr__(self) -> str:
- fields: list[str] = [
- *map(repr, self.year_month_day),
- *map(repr, self.hour_minute_second_nanosecond),
- ]
- if self.tzinfo is not None:
- fields.append(f"tzinfo={self.tzinfo!r}")
- return f"neo4j.time.DateTime({', '.join(fields)})"
- def __str__(self) -> str:
- return self.iso_format()
- def __format__(self, format_spec):
- if not format_spec:
- return self.iso_format()
- format_spec = FORMAT_F_REPLACE.sub(
- f"{self.__time.nanosecond:09}", format_spec
- )
- return self.to_native().__format__(format_spec)
- # INSTANCE METHOD ALIASES #
- def __getattr__(self, name):
- """
- Get attribute by name.
- Map standard library attribute names to local attribute names,
- for compatibility.
- """
- try:
- return {
- "astimezone": self.as_timezone,
- "isocalendar": self.iso_calendar,
- "isoformat": self.iso_format,
- "isoweekday": self.iso_weekday,
- "strftime": self.__format__,
- "toordinal": self.to_ordinal,
- "timetuple": self.time_tuple,
- "utcoffset": self.utc_offset,
- "utctimetuple": self.utc_time_tuple,
- }[name]
- except KeyError:
- raise AttributeError(
- f"DateTime has no attribute {name!r}"
- ) from None
- if t.TYPE_CHECKING:
- def astimezone( # type: ignore[override]
- self, tz: _tzinfo
- ) -> DateTime: ...
- def isocalendar( # type: ignore[override]
- self,
- ) -> tuple[int, int, int]: ...
- def iso_format(self, sep: str = "T") -> str: # type: ignore[override]
- ...
- isoweekday = iso_weekday
- strftime = __format__
- toordinal = to_ordinal
- timetuple = time_tuple
- utcoffset = utc_offset
- utctimetuple = utc_time_tuple
- DateTime.min = DateTime.combine(Date.min, Time.min) # type: ignore
- DateTime.max = DateTime.combine(Date.max, Time.max) # type: ignore
- DateTime.resolution = Time.resolution # type: ignore
- #: A :class:`.DateTime` instance set to `0000-00-00T00:00:00`.
- #: This has a :class:`.Date` component equal to :attr:`ZeroDate` and a
- Never = DateTime.combine(ZeroDate, Midnight)
- #: A :class:`.DateTime` instance set to `1970-01-01T00:00:00`.
- UnixEpoch = DateTime(1970, 1, 1, 0, 0, 0)
|