././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1614648795.4551008 aniso8601-9.0.1/0000775000175000017500000000000000000000000013334 5ustar00nielsenbnielsenb././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1613696531.0 aniso8601-9.0.1/LICENSE0000664000175000017500000000273500000000000014350 0ustar00nielsenbnielsenbCopyright (c) 2021, Brandon Nielsen All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1613696531.0 aniso8601-9.0.1/MANIFEST.in0000664000175000017500000000004300000000000015067 0ustar00nielsenbnielsenbinclude LICENSE include README.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1614648795.4551008 aniso8601-9.0.1/PKG-INFO0000664000175000017500000006456100000000000014445 0ustar00nielsenbnielsenbMetadata-Version: 2.1 Name: aniso8601 Version: 9.0.1 Summary: A library for parsing ISO 8601 strings. Home-page: https://bitbucket.org/nielsenb/aniso8601 Author: Brandon Nielsen Author-email: nielsenb@jetfuse.net License: UNKNOWN Project-URL: Documentation, https://aniso8601.readthedocs.io/ Project-URL: Source, https://bitbucket.org/nielsenb/aniso8601 Project-URL: Tracker, https://bitbucket.org/nielsenb/aniso8601/issues Description: aniso8601 ========= Another ISO 8601 parser for Python ---------------------------------- Features ======== * Pure Python implementation * Logical behavior - Parse a time, get a `datetime.time `_ - Parse a date, get a `datetime.date `_ - Parse a datetime, get a `datetime.datetime `_ - Parse a duration, get a `datetime.timedelta `_ - Parse an interval, get a tuple of dates or datetimes - Parse a repeating interval, get a date or datetime `generator `_ * UTC offset represented as fixed-offset tzinfo * Parser separate from representation, allowing parsing to different datetime representations (see `Builders`_) * No regular expressions Installation ============ The recommended installation method is to use pip:: $ pip install aniso8601 Alternatively, you can download the source (git repository hosted at `Bitbucket `_) and install directly:: $ python setup.py install Use === Parsing datetimes ----------------- *Consider* `datetime.datetime.fromisoformat `_ *for basic ISO 8601 datetime parsing* To parse a typical ISO 8601 datetime string:: >>> import aniso8601 >>> aniso8601.parse_datetime('1977-06-10T12:00:00Z') datetime.datetime(1977, 6, 10, 12, 0, tzinfo=+0:00:00 UTC) Alternative delimiters can be specified, for example, a space:: >>> aniso8601.parse_datetime('1977-06-10 12:00:00Z', delimiter=' ') datetime.datetime(1977, 6, 10, 12, 0, tzinfo=+0:00:00 UTC) UTC offsets are supported:: >>> aniso8601.parse_datetime('1979-06-05T08:00:00-08:00') datetime.datetime(1979, 6, 5, 8, 0, tzinfo=-8:00:00 UTC) If a UTC offset is not specified, the returned datetime will be naive:: >>> aniso8601.parse_datetime('1983-01-22T08:00:00') datetime.datetime(1983, 1, 22, 8, 0) Leap seconds are currently not supported and attempting to parse one raises a :code:`LeapSecondError`:: >>> aniso8601.parse_datetime('2018-03-06T23:59:60') Traceback (most recent call last): File "", line 1, in File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/time.py", line 196, in parse_datetime return builder.build_datetime(datepart, timepart) File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/python.py", line 237, in build_datetime cls._build_object(time)) File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/__init__.py", line 336, in _build_object return cls.build_time(hh=parsetuple.hh, mm=parsetuple.mm, File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/python.py", line 191, in build_time hh, mm, ss, tz = cls.range_check_time(hh, mm, ss, tz) File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/__init__.py", line 266, in range_check_time raise LeapSecondError('Leap seconds are not supported.') aniso8601.exceptions.LeapSecondError: Leap seconds are not supported. To get the resolution of an ISO 8601 datetime string:: >>> aniso8601.get_datetime_resolution('1977-06-10T12:00:00Z') == aniso8601.resolution.TimeResolution.Seconds True >>> aniso8601.get_datetime_resolution('1977-06-10T12:00') == aniso8601.resolution.TimeResolution.Minutes True >>> aniso8601.get_datetime_resolution('1977-06-10T12') == aniso8601.resolution.TimeResolution.Hours True Note that datetime resolutions map to :code:`TimeResolution` as a valid datetime must have at least one time member so the resolution mapping is equivalent. Parsing dates ------------- *Consider* `datetime.date.fromisoformat `_ *for basic ISO 8601 date parsing* To parse a date represented in an ISO 8601 string:: >>> import aniso8601 >>> aniso8601.parse_date('1984-04-23') datetime.date(1984, 4, 23) Basic format is supported as well:: >>> aniso8601.parse_date('19840423') datetime.date(1984, 4, 23) To parse a date using the ISO 8601 week date format:: >>> aniso8601.parse_date('1986-W38-1') datetime.date(1986, 9, 15) To parse an ISO 8601 ordinal date:: >>> aniso8601.parse_date('1988-132') datetime.date(1988, 5, 11) To get the resolution of an ISO 8601 date string:: >>> aniso8601.get_date_resolution('1981-04-05') == aniso8601.resolution.DateResolution.Day True >>> aniso8601.get_date_resolution('1981-04') == aniso8601.resolution.DateResolution.Month True >>> aniso8601.get_date_resolution('1981') == aniso8601.resolution.DateResolution.Year True Parsing times ------------- *Consider* `datetime.time.fromisoformat `_ *for basic ISO 8601 time parsing* To parse a time formatted as an ISO 8601 string:: >>> import aniso8601 >>> aniso8601.parse_time('11:31:14') datetime.time(11, 31, 14) As with all of the above, basic format is supported:: >>> aniso8601.parse_time('113114') datetime.time(11, 31, 14) A UTC offset can be specified for times:: >>> aniso8601.parse_time('17:18:19-02:30') datetime.time(17, 18, 19, tzinfo=-2:30:00 UTC) >>> aniso8601.parse_time('171819Z') datetime.time(17, 18, 19, tzinfo=+0:00:00 UTC) Reduced accuracy is supported:: >>> aniso8601.parse_time('21:42') datetime.time(21, 42) >>> aniso8601.parse_time('22') datetime.time(22, 0) A decimal fraction is always allowed on the lowest order element of an ISO 8601 formatted time:: >>> aniso8601.parse_time('22:33.5') datetime.time(22, 33, 30) >>> aniso8601.parse_time('23.75') datetime.time(23, 45) The decimal fraction can be specified with a comma instead of a full-stop:: >>> aniso8601.parse_time('22:33,5') datetime.time(22, 33, 30) >>> aniso8601.parse_time('23,75') datetime.time(23, 45) Leap seconds are currently not supported and attempting to parse one raises a :code:`LeapSecondError`:: >>> aniso8601.parse_time('23:59:60') Traceback (most recent call last): File "", line 1, in File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/time.py", line 174, in parse_time return builder.build_time(hh=hourstr, mm=minutestr, ss=secondstr, tz=tz) File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/python.py", line 191, in build_time hh, mm, ss, tz = cls.range_check_time(hh, mm, ss, tz) File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/__init__.py", line 266, in range_check_time raise LeapSecondError('Leap seconds are not supported.') aniso8601.exceptions.LeapSecondError: Leap seconds are not supported. To get the resolution of an ISO 8601 time string:: >>> aniso8601.get_time_resolution('11:31:14') == aniso8601.resolution.TimeResolution.Seconds True >>> aniso8601.get_time_resolution('11:31') == aniso8601.resolution.TimeResolution.Minutes True >>> aniso8601.get_time_resolution('11') == aniso8601.resolution.TimeResolution.Hours True Parsing durations ----------------- To parse a duration formatted as an ISO 8601 string:: >>> import aniso8601 >>> aniso8601.parse_duration('P1Y2M3DT4H54M6S') datetime.timedelta(428, 17646) Reduced accuracy is supported:: >>> aniso8601.parse_duration('P1Y') datetime.timedelta(365) A decimal fraction is allowed on the lowest order element:: >>> aniso8601.parse_duration('P1YT3.5M') datetime.timedelta(365, 210) The decimal fraction can be specified with a comma instead of a full-stop:: >>> aniso8601.parse_duration('P1YT3,5M') datetime.timedelta(365, 210) Parsing a duration from a combined date and time is supported as well:: >>> aniso8601.parse_duration('P0001-01-02T01:30:05') datetime.timedelta(397, 5405) To get the resolution of an ISO 8601 duration string:: >>> aniso8601.get_duration_resolution('P1Y2M3DT4H54M6S') == aniso8601.resolution.DurationResolution.Seconds True >>> aniso8601.get_duration_resolution('P1Y2M3DT4H54M') == aniso8601.resolution.DurationResolution.Minutes True >>> aniso8601.get_duration_resolution('P1Y2M3DT4H') == aniso8601.resolution.DurationResolution.Hours True >>> aniso8601.get_duration_resolution('P1Y2M3D') == aniso8601.resolution.DurationResolution.Days True >>> aniso8601.get_duration_resolution('P1Y2M') == aniso8601.resolution.DurationResolution.Months True >>> aniso8601.get_duration_resolution('P1Y') == aniso8601.resolution.DurationResolution.Years True The default :code:`PythonTimeBuilder` assumes years are 365 days, and months are 30 days. Where calendar level accuracy is required, a `RelativeTimeBuilder `_ can be used, see also `Builders`_. Parsing intervals ----------------- To parse an interval specified by a start and end:: >>> import aniso8601 >>> aniso8601.parse_interval('2007-03-01T13:00:00/2008-05-11T15:30:00') (datetime.datetime(2007, 3, 1, 13, 0), datetime.datetime(2008, 5, 11, 15, 30)) Intervals specified by a start time and a duration are supported:: >>> aniso8601.parse_interval('2007-03-01T13:00:00Z/P1Y2M10DT2H30M') (datetime.datetime(2007, 3, 1, 13, 0, tzinfo=+0:00:00 UTC), datetime.datetime(2008, 5, 9, 15, 30, tzinfo=+0:00:00 UTC)) A duration can also be specified by a duration and end time:: >>> aniso8601.parse_interval('P1M/1981-04-05') (datetime.date(1981, 4, 5), datetime.date(1981, 3, 6)) Notice that the result of the above parse is not in order from earliest to latest. If sorted intervals are required, simply use the :code:`sorted` keyword as shown below:: >>> sorted(aniso8601.parse_interval('P1M/1981-04-05')) [datetime.date(1981, 3, 6), datetime.date(1981, 4, 5)] The end of an interval is returned as a datetime when required to maintain the resolution specified by a duration, even if the duration start is given as a date:: >>> aniso8601.parse_interval('2014-11-12/PT4H54M6.5S') (datetime.date(2014, 11, 12), datetime.datetime(2014, 11, 12, 4, 54, 6, 500000)) >>> aniso8601.parse_interval('2007-03-01/P1.5D') (datetime.date(2007, 3, 1), datetime.datetime(2007, 3, 2, 12, 0)) Concise representations are supported:: >>> aniso8601.parse_interval('2020-01-01/02') (datetime.date(2020, 1, 1), datetime.date(2020, 1, 2)) >>> aniso8601.parse_interval('2007-12-14T13:30/15:30') (datetime.datetime(2007, 12, 14, 13, 30), datetime.datetime(2007, 12, 14, 15, 30)) >>> aniso8601.parse_interval('2008-02-15/03-14') (datetime.date(2008, 2, 15), datetime.date(2008, 3, 14)) >>> aniso8601.parse_interval('2007-11-13T09:00/15T17:00') (datetime.datetime(2007, 11, 13, 9, 0), datetime.datetime(2007, 11, 15, 17, 0)) Repeating intervals are supported as well, and return a `generator `_:: >>> aniso8601.parse_repeating_interval('R3/1981-04-05/P1D') >>> list(aniso8601.parse_repeating_interval('R3/1981-04-05/P1D')) [datetime.date(1981, 4, 5), datetime.date(1981, 4, 6), datetime.date(1981, 4, 7)] Repeating intervals are allowed to go in the reverse direction:: >>> list(aniso8601.parse_repeating_interval('R2/PT1H2M/1980-03-05T01:01:00')) [datetime.datetime(1980, 3, 5, 1, 1), datetime.datetime(1980, 3, 4, 23, 59)] Unbounded intervals are also allowed (Python 2):: >>> result = aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00') >>> result.next() datetime.datetime(1980, 3, 5, 1, 1) >>> result.next() datetime.datetime(1980, 3, 4, 23, 59) or for Python 3:: >>> result = aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00') >>> next(result) datetime.datetime(1980, 3, 5, 1, 1) >>> next(result) datetime.datetime(1980, 3, 4, 23, 59) Note that you should never try to convert a generator produced by an unbounded interval to a list:: >>> list(aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00')) Traceback (most recent call last): File "", line 1, in File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/python.py", line 560, in _date_generator_unbounded currentdate += timedelta OverflowError: date value out of range To get the resolution of an ISO 8601 interval string:: >>> aniso8601.get_interval_resolution('2007-03-01T13:00:00/2008-05-11T15:30:00') == aniso8601.resolution.IntervalResolution.Seconds True >>> aniso8601.get_interval_resolution('2007-03-01T13:00/2008-05-11T15:30') == aniso8601.resolution.IntervalResolution.Minutes True >>> aniso8601.get_interval_resolution('2007-03-01T13/2008-05-11T15') == aniso8601.resolution.IntervalResolution.Hours True >>> aniso8601.get_interval_resolution('2007-03-01/2008-05-11') == aniso8601.resolution.IntervalResolution.Day True >>> aniso8601.get_interval_resolution('2007-03/P1Y') == aniso8601.resolution.IntervalResolution.Month True >>> aniso8601.get_interval_resolution('2007/P1Y') == aniso8601.resolution.IntervalResolution.Year True And for repeating ISO 8601 interval strings:: >>> aniso8601.get_repeating_interval_resolution('R3/1981-04-05/P1D') == aniso8601.resolution.IntervalResolution.Day True >>> aniso8601.get_repeating_interval_resolution('R/PT1H2M/1980-03-05T01:01:00') == aniso8601.resolution.IntervalResolution.Seconds True Builders ======== Builders can be used to change the output format of a parse operation. All parse functions have a :code:`builder` keyword argument which accepts a builder class. Two builders are included. The :code:`PythonTimeBuilder` (the default) in the :code:`aniso8601.builders.python` module, and the :code:`TupleBuilder` which returns the parse result as a corresponding named tuple and is located in the :code:`aniso8601.builders` module. Information on writing a builder can be found in `BUILDERS `_. The following builders are available as separate projects: * `RelativeTimeBuilder `_ supports parsing to `datetutil relativedelta types `_ for calendar level accuracy * `AttoTimeBuilder `_ supports parsing directly to `attotime attodatetime and attotimedelta types `_ which support sub-nanosecond precision * `NumPyTimeBuilder `_ supports parsing directly to `NumPy datetime64 and timedelta64 types `_ TupleBuilder ------------ The :code:`TupleBuilder` returns parse results as `named tuples `_. It is located in the :code:`aniso8601.builders` module. Datetimes ^^^^^^^^^ Parsing a datetime returns a :code:`DatetimeTuple` containing :code:`Date` and :code:`Time` tuples . The date tuple contains the following parse components: :code:`YYYY`, :code:`MM`, :code:`DD`, :code:`Www`, :code:`D`, :code:`DDD`. The time tuple contains the following parse components :code:`hh`, :code:`mm`, :code:`ss`, :code:`tz`, where :code:`tz` itself is a tuple with the following components :code:`negative`, :code:`Z`, :code:`hh`, :code:`mm`, :code:`name` with :code:`negative` and :code:`Z` being booleans:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_datetime('1977-06-10T12:00:00', builder=TupleBuilder) Datetime(date=Date(YYYY='1977', MM='06', DD='10', Www=None, D=None, DDD=None), time=Time(hh='12', mm='00', ss='00', tz=None)) >>> aniso8601.parse_datetime('1979-06-05T08:00:00-08:00', builder=TupleBuilder) Datetime(date=Date(YYYY='1979', MM='06', DD='05', Www=None, D=None, DDD=None), time=Time(hh='08', mm='00', ss='00', tz=Timezone(negative=True, Z=None, hh='08', mm='00', name='-08:00'))) Dates ^^^^^ Parsing a date returns a :code:`DateTuple` containing the following parse components: :code:`YYYY`, :code:`MM`, :code:`DD`, :code:`Www`, :code:`D`, :code:`DDD`:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_date('1984-04-23', builder=TupleBuilder) Date(YYYY='1984', MM='04', DD='23', Www=None, D=None, DDD=None) >>> aniso8601.parse_date('1986-W38-1', builder=TupleBuilder) Date(YYYY='1986', MM=None, DD=None, Www='38', D='1', DDD=None) >>> aniso8601.parse_date('1988-132', builder=TupleBuilder) Date(YYYY='1988', MM=None, DD=None, Www=None, D=None, DDD='132') Times ^^^^^ Parsing a time returns a :code:`TimeTuple` containing following parse components: :code:`hh`, :code:`mm`, :code:`ss`, :code:`tz`, where :code:`tz` is a :code:`TimezoneTuple` with the following components :code:`negative`, :code:`Z`, :code:`hh`, :code:`mm`, :code:`name`, with :code:`negative` and :code:`Z` being booleans:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_time('11:31:14', builder=TupleBuilder) Time(hh='11', mm='31', ss='14', tz=None) >>> aniso8601.parse_time('171819Z', builder=TupleBuilder) Time(hh='17', mm='18', ss='19', tz=Timezone(negative=False, Z=True, hh=None, mm=None, name='Z')) >>> aniso8601.parse_time('17:18:19-02:30', builder=TupleBuilder) Time(hh='17', mm='18', ss='19', tz=Timezone(negative=True, Z=None, hh='02', mm='30', name='-02:30')) Durations ^^^^^^^^^ Parsing a duration returns a :code:`DurationTuple` containing the following parse components: :code:`PnY`, :code:`PnM`, :code:`PnW`, :code:`PnD`, :code:`TnH`, :code:`TnM`, :code:`TnS`:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_duration('P1Y2M3DT4H54M6S', builder=TupleBuilder) Duration(PnY='1', PnM='2', PnW=None, PnD='3', TnH='4', TnM='54', TnS='6') >>> aniso8601.parse_duration('P7W', builder=TupleBuilder) Duration(PnY=None, PnM=None, PnW='7', PnD=None, TnH=None, TnM=None, TnS=None) Intervals ^^^^^^^^^ Parsing an interval returns an :code:`IntervalTuple` containing the following parse components: :code:`start`, :code:`end`, :code:`duration`, :code:`start` and :code:`end` may both be datetime or date tuples, :code:`duration` is a duration tuple:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_interval('2007-03-01T13:00:00/2008-05-11T15:30:00', builder=TupleBuilder) Interval(start=Datetime(date=Date(YYYY='2007', MM='03', DD='01', Www=None, D=None, DDD=None), time=Time(hh='13', mm='00', ss='00', tz=None)), end=Datetime(date=Date(YYYY='2008', MM='05', DD='11', Www=None, D=None, DDD=None), time=Time(hh='15', mm='30', ss='00', tz=None)), duration=None) >>> aniso8601.parse_interval('2007-03-01T13:00:00Z/P1Y2M10DT2H30M', builder=TupleBuilder) Interval(start=Datetime(date=Date(YYYY='2007', MM='03', DD='01', Www=None, D=None, DDD=None), time=Time(hh='13', mm='00', ss='00', tz=Timezone(negative=False, Z=True, hh=None, mm=None, name='Z'))), end=None, duration=Duration(PnY='1', PnM='2', PnW=None, PnD='10', TnH='2', TnM='30', TnS=None)) >>> aniso8601.parse_interval('P1M/1981-04-05', builder=TupleBuilder) Interval(start=None, end=Date(YYYY='1981', MM='04', DD='05', Www=None, D=None, DDD=None), duration=Duration(PnY=None, PnM='1', PnW=None, PnD=None, TnH=None, TnM=None, TnS=None)) A repeating interval returns a :code:`RepeatingIntervalTuple` containing the following parse components: :code:`R`, :code:`Rnn`, :code:`interval`, where :code:`R` is a boolean, :code:`True` for an unbounded interval, :code:`False` otherwise.:: >>> aniso8601.parse_repeating_interval('R3/1981-04-05/P1D', builder=TupleBuilder) RepeatingInterval(R=False, Rnn='3', interval=Interval(start=Date(YYYY='1981', MM='04', DD='05', Www=None, D=None, DDD=None), end=None, duration=Duration(PnY=None, PnM=None, PnW=None, PnD='1', TnH=None, TnM=None, TnS=None))) >>> aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00', builder=TupleBuilder) RepeatingInterval(R=True, Rnn=None, interval=Interval(start=None, end=Datetime(date=Date(YYYY='1980', MM='03', DD='05', Www=None, D=None, DDD=None), time=Time(hh='01', mm='01', ss='00', tz=None)), duration=Duration(PnY=None, PnM=None, PnW=None, PnD=None, TnH='1', TnM='2', TnS=None))) Development =========== Setup ----- It is recommended to develop using a `virtualenv `_. Inside a virtualenv, development dependencies can be installed automatically:: $ pip install -e .[dev] `pre-commit `_ is used for managing pre-commit hooks:: $ pre-commit install To run the pre-commit hooks manually:: $ pre-commit run --all-files Tests ----- Tests can be run using the `unittest testing framework `_:: $ python -m unittest discover aniso8601 Contributing ============ aniso8601 is an open source project hosted on `Bitbucket `_. Any and all bugs are welcome on our `issue tracker `_. Of particular interest are valid ISO 8601 strings that don't parse, or invalid ones that do. At a minimum, bug reports should include an example of the misbehaving string, as well as the expected result. Of course patches containing unit tests (or fixed bugs) are welcome! References ========== * `ISO 8601:2004(E) `_ (Caution, PDF link) * `Wikipedia article on ISO 8601 `_ * `Discussion on alternative ISO 8601 parsers for Python `_ Keywords: iso8601 parser Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Software Development :: Libraries :: Python Modules Description-Content-Type: text/x-rst Provides-Extra: dev ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1613696531.0 aniso8601-9.0.1/README.rst0000664000175000017500000005261700000000000015036 0ustar00nielsenbnielsenbaniso8601 ========= Another ISO 8601 parser for Python ---------------------------------- Features ======== * Pure Python implementation * Logical behavior - Parse a time, get a `datetime.time `_ - Parse a date, get a `datetime.date `_ - Parse a datetime, get a `datetime.datetime `_ - Parse a duration, get a `datetime.timedelta `_ - Parse an interval, get a tuple of dates or datetimes - Parse a repeating interval, get a date or datetime `generator `_ * UTC offset represented as fixed-offset tzinfo * Parser separate from representation, allowing parsing to different datetime representations (see `Builders`_) * No regular expressions Installation ============ The recommended installation method is to use pip:: $ pip install aniso8601 Alternatively, you can download the source (git repository hosted at `Bitbucket `_) and install directly:: $ python setup.py install Use === Parsing datetimes ----------------- *Consider* `datetime.datetime.fromisoformat `_ *for basic ISO 8601 datetime parsing* To parse a typical ISO 8601 datetime string:: >>> import aniso8601 >>> aniso8601.parse_datetime('1977-06-10T12:00:00Z') datetime.datetime(1977, 6, 10, 12, 0, tzinfo=+0:00:00 UTC) Alternative delimiters can be specified, for example, a space:: >>> aniso8601.parse_datetime('1977-06-10 12:00:00Z', delimiter=' ') datetime.datetime(1977, 6, 10, 12, 0, tzinfo=+0:00:00 UTC) UTC offsets are supported:: >>> aniso8601.parse_datetime('1979-06-05T08:00:00-08:00') datetime.datetime(1979, 6, 5, 8, 0, tzinfo=-8:00:00 UTC) If a UTC offset is not specified, the returned datetime will be naive:: >>> aniso8601.parse_datetime('1983-01-22T08:00:00') datetime.datetime(1983, 1, 22, 8, 0) Leap seconds are currently not supported and attempting to parse one raises a :code:`LeapSecondError`:: >>> aniso8601.parse_datetime('2018-03-06T23:59:60') Traceback (most recent call last): File "", line 1, in File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/time.py", line 196, in parse_datetime return builder.build_datetime(datepart, timepart) File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/python.py", line 237, in build_datetime cls._build_object(time)) File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/__init__.py", line 336, in _build_object return cls.build_time(hh=parsetuple.hh, mm=parsetuple.mm, File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/python.py", line 191, in build_time hh, mm, ss, tz = cls.range_check_time(hh, mm, ss, tz) File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/__init__.py", line 266, in range_check_time raise LeapSecondError('Leap seconds are not supported.') aniso8601.exceptions.LeapSecondError: Leap seconds are not supported. To get the resolution of an ISO 8601 datetime string:: >>> aniso8601.get_datetime_resolution('1977-06-10T12:00:00Z') == aniso8601.resolution.TimeResolution.Seconds True >>> aniso8601.get_datetime_resolution('1977-06-10T12:00') == aniso8601.resolution.TimeResolution.Minutes True >>> aniso8601.get_datetime_resolution('1977-06-10T12') == aniso8601.resolution.TimeResolution.Hours True Note that datetime resolutions map to :code:`TimeResolution` as a valid datetime must have at least one time member so the resolution mapping is equivalent. Parsing dates ------------- *Consider* `datetime.date.fromisoformat `_ *for basic ISO 8601 date parsing* To parse a date represented in an ISO 8601 string:: >>> import aniso8601 >>> aniso8601.parse_date('1984-04-23') datetime.date(1984, 4, 23) Basic format is supported as well:: >>> aniso8601.parse_date('19840423') datetime.date(1984, 4, 23) To parse a date using the ISO 8601 week date format:: >>> aniso8601.parse_date('1986-W38-1') datetime.date(1986, 9, 15) To parse an ISO 8601 ordinal date:: >>> aniso8601.parse_date('1988-132') datetime.date(1988, 5, 11) To get the resolution of an ISO 8601 date string:: >>> aniso8601.get_date_resolution('1981-04-05') == aniso8601.resolution.DateResolution.Day True >>> aniso8601.get_date_resolution('1981-04') == aniso8601.resolution.DateResolution.Month True >>> aniso8601.get_date_resolution('1981') == aniso8601.resolution.DateResolution.Year True Parsing times ------------- *Consider* `datetime.time.fromisoformat `_ *for basic ISO 8601 time parsing* To parse a time formatted as an ISO 8601 string:: >>> import aniso8601 >>> aniso8601.parse_time('11:31:14') datetime.time(11, 31, 14) As with all of the above, basic format is supported:: >>> aniso8601.parse_time('113114') datetime.time(11, 31, 14) A UTC offset can be specified for times:: >>> aniso8601.parse_time('17:18:19-02:30') datetime.time(17, 18, 19, tzinfo=-2:30:00 UTC) >>> aniso8601.parse_time('171819Z') datetime.time(17, 18, 19, tzinfo=+0:00:00 UTC) Reduced accuracy is supported:: >>> aniso8601.parse_time('21:42') datetime.time(21, 42) >>> aniso8601.parse_time('22') datetime.time(22, 0) A decimal fraction is always allowed on the lowest order element of an ISO 8601 formatted time:: >>> aniso8601.parse_time('22:33.5') datetime.time(22, 33, 30) >>> aniso8601.parse_time('23.75') datetime.time(23, 45) The decimal fraction can be specified with a comma instead of a full-stop:: >>> aniso8601.parse_time('22:33,5') datetime.time(22, 33, 30) >>> aniso8601.parse_time('23,75') datetime.time(23, 45) Leap seconds are currently not supported and attempting to parse one raises a :code:`LeapSecondError`:: >>> aniso8601.parse_time('23:59:60') Traceback (most recent call last): File "", line 1, in File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/time.py", line 174, in parse_time return builder.build_time(hh=hourstr, mm=minutestr, ss=secondstr, tz=tz) File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/python.py", line 191, in build_time hh, mm, ss, tz = cls.range_check_time(hh, mm, ss, tz) File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/__init__.py", line 266, in range_check_time raise LeapSecondError('Leap seconds are not supported.') aniso8601.exceptions.LeapSecondError: Leap seconds are not supported. To get the resolution of an ISO 8601 time string:: >>> aniso8601.get_time_resolution('11:31:14') == aniso8601.resolution.TimeResolution.Seconds True >>> aniso8601.get_time_resolution('11:31') == aniso8601.resolution.TimeResolution.Minutes True >>> aniso8601.get_time_resolution('11') == aniso8601.resolution.TimeResolution.Hours True Parsing durations ----------------- To parse a duration formatted as an ISO 8601 string:: >>> import aniso8601 >>> aniso8601.parse_duration('P1Y2M3DT4H54M6S') datetime.timedelta(428, 17646) Reduced accuracy is supported:: >>> aniso8601.parse_duration('P1Y') datetime.timedelta(365) A decimal fraction is allowed on the lowest order element:: >>> aniso8601.parse_duration('P1YT3.5M') datetime.timedelta(365, 210) The decimal fraction can be specified with a comma instead of a full-stop:: >>> aniso8601.parse_duration('P1YT3,5M') datetime.timedelta(365, 210) Parsing a duration from a combined date and time is supported as well:: >>> aniso8601.parse_duration('P0001-01-02T01:30:05') datetime.timedelta(397, 5405) To get the resolution of an ISO 8601 duration string:: >>> aniso8601.get_duration_resolution('P1Y2M3DT4H54M6S') == aniso8601.resolution.DurationResolution.Seconds True >>> aniso8601.get_duration_resolution('P1Y2M3DT4H54M') == aniso8601.resolution.DurationResolution.Minutes True >>> aniso8601.get_duration_resolution('P1Y2M3DT4H') == aniso8601.resolution.DurationResolution.Hours True >>> aniso8601.get_duration_resolution('P1Y2M3D') == aniso8601.resolution.DurationResolution.Days True >>> aniso8601.get_duration_resolution('P1Y2M') == aniso8601.resolution.DurationResolution.Months True >>> aniso8601.get_duration_resolution('P1Y') == aniso8601.resolution.DurationResolution.Years True The default :code:`PythonTimeBuilder` assumes years are 365 days, and months are 30 days. Where calendar level accuracy is required, a `RelativeTimeBuilder `_ can be used, see also `Builders`_. Parsing intervals ----------------- To parse an interval specified by a start and end:: >>> import aniso8601 >>> aniso8601.parse_interval('2007-03-01T13:00:00/2008-05-11T15:30:00') (datetime.datetime(2007, 3, 1, 13, 0), datetime.datetime(2008, 5, 11, 15, 30)) Intervals specified by a start time and a duration are supported:: >>> aniso8601.parse_interval('2007-03-01T13:00:00Z/P1Y2M10DT2H30M') (datetime.datetime(2007, 3, 1, 13, 0, tzinfo=+0:00:00 UTC), datetime.datetime(2008, 5, 9, 15, 30, tzinfo=+0:00:00 UTC)) A duration can also be specified by a duration and end time:: >>> aniso8601.parse_interval('P1M/1981-04-05') (datetime.date(1981, 4, 5), datetime.date(1981, 3, 6)) Notice that the result of the above parse is not in order from earliest to latest. If sorted intervals are required, simply use the :code:`sorted` keyword as shown below:: >>> sorted(aniso8601.parse_interval('P1M/1981-04-05')) [datetime.date(1981, 3, 6), datetime.date(1981, 4, 5)] The end of an interval is returned as a datetime when required to maintain the resolution specified by a duration, even if the duration start is given as a date:: >>> aniso8601.parse_interval('2014-11-12/PT4H54M6.5S') (datetime.date(2014, 11, 12), datetime.datetime(2014, 11, 12, 4, 54, 6, 500000)) >>> aniso8601.parse_interval('2007-03-01/P1.5D') (datetime.date(2007, 3, 1), datetime.datetime(2007, 3, 2, 12, 0)) Concise representations are supported:: >>> aniso8601.parse_interval('2020-01-01/02') (datetime.date(2020, 1, 1), datetime.date(2020, 1, 2)) >>> aniso8601.parse_interval('2007-12-14T13:30/15:30') (datetime.datetime(2007, 12, 14, 13, 30), datetime.datetime(2007, 12, 14, 15, 30)) >>> aniso8601.parse_interval('2008-02-15/03-14') (datetime.date(2008, 2, 15), datetime.date(2008, 3, 14)) >>> aniso8601.parse_interval('2007-11-13T09:00/15T17:00') (datetime.datetime(2007, 11, 13, 9, 0), datetime.datetime(2007, 11, 15, 17, 0)) Repeating intervals are supported as well, and return a `generator `_:: >>> aniso8601.parse_repeating_interval('R3/1981-04-05/P1D') >>> list(aniso8601.parse_repeating_interval('R3/1981-04-05/P1D')) [datetime.date(1981, 4, 5), datetime.date(1981, 4, 6), datetime.date(1981, 4, 7)] Repeating intervals are allowed to go in the reverse direction:: >>> list(aniso8601.parse_repeating_interval('R2/PT1H2M/1980-03-05T01:01:00')) [datetime.datetime(1980, 3, 5, 1, 1), datetime.datetime(1980, 3, 4, 23, 59)] Unbounded intervals are also allowed (Python 2):: >>> result = aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00') >>> result.next() datetime.datetime(1980, 3, 5, 1, 1) >>> result.next() datetime.datetime(1980, 3, 4, 23, 59) or for Python 3:: >>> result = aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00') >>> next(result) datetime.datetime(1980, 3, 5, 1, 1) >>> next(result) datetime.datetime(1980, 3, 4, 23, 59) Note that you should never try to convert a generator produced by an unbounded interval to a list:: >>> list(aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00')) Traceback (most recent call last): File "", line 1, in File "/home/nielsenb/Jetfuse/aniso8601/aniso8601/aniso8601/builders/python.py", line 560, in _date_generator_unbounded currentdate += timedelta OverflowError: date value out of range To get the resolution of an ISO 8601 interval string:: >>> aniso8601.get_interval_resolution('2007-03-01T13:00:00/2008-05-11T15:30:00') == aniso8601.resolution.IntervalResolution.Seconds True >>> aniso8601.get_interval_resolution('2007-03-01T13:00/2008-05-11T15:30') == aniso8601.resolution.IntervalResolution.Minutes True >>> aniso8601.get_interval_resolution('2007-03-01T13/2008-05-11T15') == aniso8601.resolution.IntervalResolution.Hours True >>> aniso8601.get_interval_resolution('2007-03-01/2008-05-11') == aniso8601.resolution.IntervalResolution.Day True >>> aniso8601.get_interval_resolution('2007-03/P1Y') == aniso8601.resolution.IntervalResolution.Month True >>> aniso8601.get_interval_resolution('2007/P1Y') == aniso8601.resolution.IntervalResolution.Year True And for repeating ISO 8601 interval strings:: >>> aniso8601.get_repeating_interval_resolution('R3/1981-04-05/P1D') == aniso8601.resolution.IntervalResolution.Day True >>> aniso8601.get_repeating_interval_resolution('R/PT1H2M/1980-03-05T01:01:00') == aniso8601.resolution.IntervalResolution.Seconds True Builders ======== Builders can be used to change the output format of a parse operation. All parse functions have a :code:`builder` keyword argument which accepts a builder class. Two builders are included. The :code:`PythonTimeBuilder` (the default) in the :code:`aniso8601.builders.python` module, and the :code:`TupleBuilder` which returns the parse result as a corresponding named tuple and is located in the :code:`aniso8601.builders` module. Information on writing a builder can be found in `BUILDERS `_. The following builders are available as separate projects: * `RelativeTimeBuilder `_ supports parsing to `datetutil relativedelta types `_ for calendar level accuracy * `AttoTimeBuilder `_ supports parsing directly to `attotime attodatetime and attotimedelta types `_ which support sub-nanosecond precision * `NumPyTimeBuilder `_ supports parsing directly to `NumPy datetime64 and timedelta64 types `_ TupleBuilder ------------ The :code:`TupleBuilder` returns parse results as `named tuples `_. It is located in the :code:`aniso8601.builders` module. Datetimes ^^^^^^^^^ Parsing a datetime returns a :code:`DatetimeTuple` containing :code:`Date` and :code:`Time` tuples . The date tuple contains the following parse components: :code:`YYYY`, :code:`MM`, :code:`DD`, :code:`Www`, :code:`D`, :code:`DDD`. The time tuple contains the following parse components :code:`hh`, :code:`mm`, :code:`ss`, :code:`tz`, where :code:`tz` itself is a tuple with the following components :code:`negative`, :code:`Z`, :code:`hh`, :code:`mm`, :code:`name` with :code:`negative` and :code:`Z` being booleans:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_datetime('1977-06-10T12:00:00', builder=TupleBuilder) Datetime(date=Date(YYYY='1977', MM='06', DD='10', Www=None, D=None, DDD=None), time=Time(hh='12', mm='00', ss='00', tz=None)) >>> aniso8601.parse_datetime('1979-06-05T08:00:00-08:00', builder=TupleBuilder) Datetime(date=Date(YYYY='1979', MM='06', DD='05', Www=None, D=None, DDD=None), time=Time(hh='08', mm='00', ss='00', tz=Timezone(negative=True, Z=None, hh='08', mm='00', name='-08:00'))) Dates ^^^^^ Parsing a date returns a :code:`DateTuple` containing the following parse components: :code:`YYYY`, :code:`MM`, :code:`DD`, :code:`Www`, :code:`D`, :code:`DDD`:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_date('1984-04-23', builder=TupleBuilder) Date(YYYY='1984', MM='04', DD='23', Www=None, D=None, DDD=None) >>> aniso8601.parse_date('1986-W38-1', builder=TupleBuilder) Date(YYYY='1986', MM=None, DD=None, Www='38', D='1', DDD=None) >>> aniso8601.parse_date('1988-132', builder=TupleBuilder) Date(YYYY='1988', MM=None, DD=None, Www=None, D=None, DDD='132') Times ^^^^^ Parsing a time returns a :code:`TimeTuple` containing following parse components: :code:`hh`, :code:`mm`, :code:`ss`, :code:`tz`, where :code:`tz` is a :code:`TimezoneTuple` with the following components :code:`negative`, :code:`Z`, :code:`hh`, :code:`mm`, :code:`name`, with :code:`negative` and :code:`Z` being booleans:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_time('11:31:14', builder=TupleBuilder) Time(hh='11', mm='31', ss='14', tz=None) >>> aniso8601.parse_time('171819Z', builder=TupleBuilder) Time(hh='17', mm='18', ss='19', tz=Timezone(negative=False, Z=True, hh=None, mm=None, name='Z')) >>> aniso8601.parse_time('17:18:19-02:30', builder=TupleBuilder) Time(hh='17', mm='18', ss='19', tz=Timezone(negative=True, Z=None, hh='02', mm='30', name='-02:30')) Durations ^^^^^^^^^ Parsing a duration returns a :code:`DurationTuple` containing the following parse components: :code:`PnY`, :code:`PnM`, :code:`PnW`, :code:`PnD`, :code:`TnH`, :code:`TnM`, :code:`TnS`:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_duration('P1Y2M3DT4H54M6S', builder=TupleBuilder) Duration(PnY='1', PnM='2', PnW=None, PnD='3', TnH='4', TnM='54', TnS='6') >>> aniso8601.parse_duration('P7W', builder=TupleBuilder) Duration(PnY=None, PnM=None, PnW='7', PnD=None, TnH=None, TnM=None, TnS=None) Intervals ^^^^^^^^^ Parsing an interval returns an :code:`IntervalTuple` containing the following parse components: :code:`start`, :code:`end`, :code:`duration`, :code:`start` and :code:`end` may both be datetime or date tuples, :code:`duration` is a duration tuple:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_interval('2007-03-01T13:00:00/2008-05-11T15:30:00', builder=TupleBuilder) Interval(start=Datetime(date=Date(YYYY='2007', MM='03', DD='01', Www=None, D=None, DDD=None), time=Time(hh='13', mm='00', ss='00', tz=None)), end=Datetime(date=Date(YYYY='2008', MM='05', DD='11', Www=None, D=None, DDD=None), time=Time(hh='15', mm='30', ss='00', tz=None)), duration=None) >>> aniso8601.parse_interval('2007-03-01T13:00:00Z/P1Y2M10DT2H30M', builder=TupleBuilder) Interval(start=Datetime(date=Date(YYYY='2007', MM='03', DD='01', Www=None, D=None, DDD=None), time=Time(hh='13', mm='00', ss='00', tz=Timezone(negative=False, Z=True, hh=None, mm=None, name='Z'))), end=None, duration=Duration(PnY='1', PnM='2', PnW=None, PnD='10', TnH='2', TnM='30', TnS=None)) >>> aniso8601.parse_interval('P1M/1981-04-05', builder=TupleBuilder) Interval(start=None, end=Date(YYYY='1981', MM='04', DD='05', Www=None, D=None, DDD=None), duration=Duration(PnY=None, PnM='1', PnW=None, PnD=None, TnH=None, TnM=None, TnS=None)) A repeating interval returns a :code:`RepeatingIntervalTuple` containing the following parse components: :code:`R`, :code:`Rnn`, :code:`interval`, where :code:`R` is a boolean, :code:`True` for an unbounded interval, :code:`False` otherwise.:: >>> aniso8601.parse_repeating_interval('R3/1981-04-05/P1D', builder=TupleBuilder) RepeatingInterval(R=False, Rnn='3', interval=Interval(start=Date(YYYY='1981', MM='04', DD='05', Www=None, D=None, DDD=None), end=None, duration=Duration(PnY=None, PnM=None, PnW=None, PnD='1', TnH=None, TnM=None, TnS=None))) >>> aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00', builder=TupleBuilder) RepeatingInterval(R=True, Rnn=None, interval=Interval(start=None, end=Datetime(date=Date(YYYY='1980', MM='03', DD='05', Www=None, D=None, DDD=None), time=Time(hh='01', mm='01', ss='00', tz=None)), duration=Duration(PnY=None, PnM=None, PnW=None, PnD=None, TnH='1', TnM='2', TnS=None))) Development =========== Setup ----- It is recommended to develop using a `virtualenv `_. Inside a virtualenv, development dependencies can be installed automatically:: $ pip install -e .[dev] `pre-commit `_ is used for managing pre-commit hooks:: $ pre-commit install To run the pre-commit hooks manually:: $ pre-commit run --all-files Tests ----- Tests can be run using the `unittest testing framework `_:: $ python -m unittest discover aniso8601 Contributing ============ aniso8601 is an open source project hosted on `Bitbucket `_. Any and all bugs are welcome on our `issue tracker `_. Of particular interest are valid ISO 8601 strings that don't parse, or invalid ones that do. At a minimum, bug reports should include an example of the misbehaving string, as well as the expected result. Of course patches containing unit tests (or fixed bugs) are welcome! References ========== * `ISO 8601:2004(E) `_ (Caution, PDF link) * `Wikipedia article on ISO 8601 `_ * `Discussion on alternative ISO 8601 parsers for Python `_ ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1614648795.453101 aniso8601-9.0.1/aniso8601/0000775000175000017500000000000000000000000014764 5ustar00nielsenbnielsenb././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1614648385.0 aniso8601-9.0.1/aniso8601/__init__.py0000664000175000017500000000130000000000000017067 0ustar00nielsenbnielsenb# -*- coding: utf-8 -*- # Copyright (c) 2021, Brandon Nielsen # All rights reserved. # # This software may be modified and distributed under the terms # of the BSD license. See the LICENSE file for details. from aniso8601.date import get_date_resolution, parse_date from aniso8601.duration import get_duration_resolution, parse_duration from aniso8601.interval import ( get_interval_resolution, get_repeating_interval_resolution, parse_interval, parse_repeating_interval, ) # Import the main parsing functions so they are readily available from aniso8601.time import ( get_datetime_resolution, get_time_resolution, parse_datetime, parse_time, ) __version__ = "9.0.1" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1614648795.453101 aniso8601-9.0.1/aniso8601/builders/0000775000175000017500000000000000000000000016575 5ustar00nielsenbnielsenb././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1613696531.0 aniso8601-9.0.1/aniso8601/builders/__init__.py0000664000175000017500000004304600000000000020715 0ustar00nielsenbnielsenb# -*- coding: utf-8 -*- # Copyright (c) 2021, Brandon Nielsen # All rights reserved. # # This software may be modified and distributed under the terms # of the BSD license. See the LICENSE file for details. import calendar from collections import namedtuple from aniso8601.exceptions import ( DayOutOfBoundsError, HoursOutOfBoundsError, ISOFormatError, LeapSecondError, MidnightBoundsError, MinutesOutOfBoundsError, MonthOutOfBoundsError, SecondsOutOfBoundsError, WeekOutOfBoundsError, YearOutOfBoundsError, ) DateTuple = namedtuple("Date", ["YYYY", "MM", "DD", "Www", "D", "DDD"]) TimeTuple = namedtuple("Time", ["hh", "mm", "ss", "tz"]) DatetimeTuple = namedtuple("Datetime", ["date", "time"]) DurationTuple = namedtuple( "Duration", ["PnY", "PnM", "PnW", "PnD", "TnH", "TnM", "TnS"] ) IntervalTuple = namedtuple("Interval", ["start", "end", "duration"]) RepeatingIntervalTuple = namedtuple("RepeatingInterval", ["R", "Rnn", "interval"]) TimezoneTuple = namedtuple("Timezone", ["negative", "Z", "hh", "mm", "name"]) Limit = namedtuple( "Limit", [ "casterrorstring", "min", "max", "rangeexception", "rangeerrorstring", "rangefunc", ], ) def cast( value, castfunction, caughtexceptions=(ValueError,), thrownexception=ISOFormatError, thrownmessage=None, ): try: result = castfunction(value) except caughtexceptions: raise thrownexception(thrownmessage) return result def range_check(valuestr, limit): # Returns cast value if in range, raises defined exceptions on failure if valuestr is None: return None if "." in valuestr: castfunc = float else: castfunc = int value = cast(valuestr, castfunc, thrownmessage=limit.casterrorstring) if limit.min is not None and value < limit.min: raise limit.rangeexception(limit.rangeerrorstring) if limit.max is not None and value > limit.max: raise limit.rangeexception(limit.rangeerrorstring) return value class BaseTimeBuilder(object): # Limit tuple format cast function, cast error string, # lower limit, upper limit, limit error string DATE_YYYY_LIMIT = Limit( "Invalid year string.", 0000, 9999, YearOutOfBoundsError, "Year must be between 1..9999.", range_check, ) DATE_MM_LIMIT = Limit( "Invalid month string.", 1, 12, MonthOutOfBoundsError, "Month must be between 1..12.", range_check, ) DATE_DD_LIMIT = Limit( "Invalid day string.", 1, 31, DayOutOfBoundsError, "Day must be between 1..31.", range_check, ) DATE_WWW_LIMIT = Limit( "Invalid week string.", 1, 53, WeekOutOfBoundsError, "Week number must be between 1..53.", range_check, ) DATE_D_LIMIT = Limit( "Invalid weekday string.", 1, 7, DayOutOfBoundsError, "Weekday number must be between 1..7.", range_check, ) DATE_DDD_LIMIT = Limit( "Invalid ordinal day string.", 1, 366, DayOutOfBoundsError, "Ordinal day must be between 1..366.", range_check, ) TIME_HH_LIMIT = Limit( "Invalid hour string.", 0, 24, HoursOutOfBoundsError, "Hour must be between 0..24 with " "24 representing midnight.", range_check, ) TIME_MM_LIMIT = Limit( "Invalid minute string.", 0, 59, MinutesOutOfBoundsError, "Minute must be between 0..59.", range_check, ) TIME_SS_LIMIT = Limit( "Invalid second string.", 0, 60, SecondsOutOfBoundsError, "Second must be between 0..60 with " "60 representing a leap second.", range_check, ) TZ_HH_LIMIT = Limit( "Invalid timezone hour string.", 0, 23, HoursOutOfBoundsError, "Hour must be between 0..23.", range_check, ) TZ_MM_LIMIT = Limit( "Invalid timezone minute string.", 0, 59, MinutesOutOfBoundsError, "Minute must be between 0..59.", range_check, ) DURATION_PNY_LIMIT = Limit( "Invalid year duration string.", 0, None, ISOFormatError, "Duration years component must be positive.", range_check, ) DURATION_PNM_LIMIT = Limit( "Invalid month duration string.", 0, None, ISOFormatError, "Duration months component must be positive.", range_check, ) DURATION_PNW_LIMIT = Limit( "Invalid week duration string.", 0, None, ISOFormatError, "Duration weeks component must be positive.", range_check, ) DURATION_PND_LIMIT = Limit( "Invalid day duration string.", 0, None, ISOFormatError, "Duration days component must be positive.", range_check, ) DURATION_TNH_LIMIT = Limit( "Invalid hour duration string.", 0, None, ISOFormatError, "Duration hours component must be positive.", range_check, ) DURATION_TNM_LIMIT = Limit( "Invalid minute duration string.", 0, None, ISOFormatError, "Duration minutes component must be positive.", range_check, ) DURATION_TNS_LIMIT = Limit( "Invalid second duration string.", 0, None, ISOFormatError, "Duration seconds component must be positive.", range_check, ) INTERVAL_RNN_LIMIT = Limit( "Invalid duration repetition string.", 0, None, ISOFormatError, "Duration repetition count must be positive.", range_check, ) DATE_RANGE_DICT = { "YYYY": DATE_YYYY_LIMIT, "MM": DATE_MM_LIMIT, "DD": DATE_DD_LIMIT, "Www": DATE_WWW_LIMIT, "D": DATE_D_LIMIT, "DDD": DATE_DDD_LIMIT, } TIME_RANGE_DICT = {"hh": TIME_HH_LIMIT, "mm": TIME_MM_LIMIT, "ss": TIME_SS_LIMIT} DURATION_RANGE_DICT = { "PnY": DURATION_PNY_LIMIT, "PnM": DURATION_PNM_LIMIT, "PnW": DURATION_PNW_LIMIT, "PnD": DURATION_PND_LIMIT, "TnH": DURATION_TNH_LIMIT, "TnM": DURATION_TNM_LIMIT, "TnS": DURATION_TNS_LIMIT, } REPEATING_INTERVAL_RANGE_DICT = {"Rnn": INTERVAL_RNN_LIMIT} TIMEZONE_RANGE_DICT = {"hh": TZ_HH_LIMIT, "mm": TZ_MM_LIMIT} LEAP_SECONDS_SUPPORTED = False @classmethod def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None): raise NotImplementedError @classmethod def build_time(cls, hh=None, mm=None, ss=None, tz=None): raise NotImplementedError @classmethod def build_datetime(cls, date, time): raise NotImplementedError @classmethod def build_duration( cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None ): raise NotImplementedError @classmethod def build_interval(cls, start=None, end=None, duration=None): # start, end, and duration are all tuples raise NotImplementedError @classmethod def build_repeating_interval(cls, R=None, Rnn=None, interval=None): # interval is a tuple raise NotImplementedError @classmethod def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""): raise NotImplementedError @classmethod def range_check_date( cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None, rangedict=None ): if rangedict is None: rangedict = cls.DATE_RANGE_DICT if "YYYY" in rangedict: YYYY = rangedict["YYYY"].rangefunc(YYYY, rangedict["YYYY"]) if "MM" in rangedict: MM = rangedict["MM"].rangefunc(MM, rangedict["MM"]) if "DD" in rangedict: DD = rangedict["DD"].rangefunc(DD, rangedict["DD"]) if "Www" in rangedict: Www = rangedict["Www"].rangefunc(Www, rangedict["Www"]) if "D" in rangedict: D = rangedict["D"].rangefunc(D, rangedict["D"]) if "DDD" in rangedict: DDD = rangedict["DDD"].rangefunc(DDD, rangedict["DDD"]) if DD is not None: # Check calendar if DD > calendar.monthrange(YYYY, MM)[1]: raise DayOutOfBoundsError( "{0} is out of range for {1}-{2}".format(DD, YYYY, MM) ) if DDD is not None: if calendar.isleap(YYYY) is False and DDD == 366: raise DayOutOfBoundsError( "{0} is only valid for leap year.".format(DDD) ) return (YYYY, MM, DD, Www, D, DDD) @classmethod def range_check_time(cls, hh=None, mm=None, ss=None, tz=None, rangedict=None): # Used for midnight and leap second handling midnight = False # Handle hh = '24' specially if rangedict is None: rangedict = cls.TIME_RANGE_DICT if "hh" in rangedict: try: hh = rangedict["hh"].rangefunc(hh, rangedict["hh"]) except HoursOutOfBoundsError as e: if float(hh) > 24 and float(hh) < 25: raise MidnightBoundsError("Hour 24 may only represent midnight.") raise e if "mm" in rangedict: mm = rangedict["mm"].rangefunc(mm, rangedict["mm"]) if "ss" in rangedict: ss = rangedict["ss"].rangefunc(ss, rangedict["ss"]) if hh is not None and hh == 24: midnight = True # Handle midnight range if midnight is True and ( (mm is not None and mm != 0) or (ss is not None and ss != 0) ): raise MidnightBoundsError("Hour 24 may only represent midnight.") if cls.LEAP_SECONDS_SUPPORTED is True: if hh != 23 and mm != 59 and ss == 60: raise cls.TIME_SS_LIMIT.rangeexception( cls.TIME_SS_LIMIT.rangeerrorstring ) else: if hh == 23 and mm == 59 and ss == 60: # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is raise LeapSecondError("Leap seconds are not supported.") if ss == 60: raise cls.TIME_SS_LIMIT.rangeexception( cls.TIME_SS_LIMIT.rangeerrorstring ) return (hh, mm, ss, tz) @classmethod def range_check_duration( cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None, rangedict=None, ): if rangedict is None: rangedict = cls.DURATION_RANGE_DICT if "PnY" in rangedict: PnY = rangedict["PnY"].rangefunc(PnY, rangedict["PnY"]) if "PnM" in rangedict: PnM = rangedict["PnM"].rangefunc(PnM, rangedict["PnM"]) if "PnW" in rangedict: PnW = rangedict["PnW"].rangefunc(PnW, rangedict["PnW"]) if "PnD" in rangedict: PnD = rangedict["PnD"].rangefunc(PnD, rangedict["PnD"]) if "TnH" in rangedict: TnH = rangedict["TnH"].rangefunc(TnH, rangedict["TnH"]) if "TnM" in rangedict: TnM = rangedict["TnM"].rangefunc(TnM, rangedict["TnM"]) if "TnS" in rangedict: TnS = rangedict["TnS"].rangefunc(TnS, rangedict["TnS"]) return (PnY, PnM, PnW, PnD, TnH, TnM, TnS) @classmethod def range_check_repeating_interval( cls, R=None, Rnn=None, interval=None, rangedict=None ): if rangedict is None: rangedict = cls.REPEATING_INTERVAL_RANGE_DICT if "Rnn" in rangedict: Rnn = rangedict["Rnn"].rangefunc(Rnn, rangedict["Rnn"]) return (R, Rnn, interval) @classmethod def range_check_timezone( cls, negative=None, Z=None, hh=None, mm=None, name="", rangedict=None ): if rangedict is None: rangedict = cls.TIMEZONE_RANGE_DICT if "hh" in rangedict: hh = rangedict["hh"].rangefunc(hh, rangedict["hh"]) if "mm" in rangedict: mm = rangedict["mm"].rangefunc(mm, rangedict["mm"]) return (negative, Z, hh, mm, name) @classmethod def _build_object(cls, parsetuple): # Given a TupleBuilder tuple, build the correct object if type(parsetuple) is DateTuple: return cls.build_date( YYYY=parsetuple.YYYY, MM=parsetuple.MM, DD=parsetuple.DD, Www=parsetuple.Www, D=parsetuple.D, DDD=parsetuple.DDD, ) if type(parsetuple) is TimeTuple: return cls.build_time( hh=parsetuple.hh, mm=parsetuple.mm, ss=parsetuple.ss, tz=parsetuple.tz ) if type(parsetuple) is DatetimeTuple: return cls.build_datetime(parsetuple.date, parsetuple.time) if type(parsetuple) is DurationTuple: return cls.build_duration( PnY=parsetuple.PnY, PnM=parsetuple.PnM, PnW=parsetuple.PnW, PnD=parsetuple.PnD, TnH=parsetuple.TnH, TnM=parsetuple.TnM, TnS=parsetuple.TnS, ) if type(parsetuple) is IntervalTuple: return cls.build_interval( start=parsetuple.start, end=parsetuple.end, duration=parsetuple.duration ) if type(parsetuple) is RepeatingIntervalTuple: return cls.build_repeating_interval( R=parsetuple.R, Rnn=parsetuple.Rnn, interval=parsetuple.interval ) return cls.build_timezone( negative=parsetuple.negative, Z=parsetuple.Z, hh=parsetuple.hh, mm=parsetuple.mm, name=parsetuple.name, ) @classmethod def _is_interval_end_concise(cls, endtuple): if type(endtuple) is TimeTuple: return True if type(endtuple) is DatetimeTuple: enddatetuple = endtuple.date else: enddatetuple = endtuple if enddatetuple.YYYY is None: return True return False @classmethod def _combine_concise_interval_tuples(cls, starttuple, conciseendtuple): starttimetuple = None startdatetuple = None endtimetuple = None enddatetuple = None if type(starttuple) is DateTuple: startdatetuple = starttuple else: # Start is a datetime starttimetuple = starttuple.time startdatetuple = starttuple.date if type(conciseendtuple) is DateTuple: enddatetuple = conciseendtuple elif type(conciseendtuple) is DatetimeTuple: enddatetuple = conciseendtuple.date endtimetuple = conciseendtuple.time else: # Time endtimetuple = conciseendtuple if enddatetuple is not None: if enddatetuple.YYYY is None and enddatetuple.MM is None: newenddatetuple = DateTuple( YYYY=startdatetuple.YYYY, MM=startdatetuple.MM, DD=enddatetuple.DD, Www=enddatetuple.Www, D=enddatetuple.D, DDD=enddatetuple.DDD, ) else: newenddatetuple = DateTuple( YYYY=startdatetuple.YYYY, MM=enddatetuple.MM, DD=enddatetuple.DD, Www=enddatetuple.Www, D=enddatetuple.D, DDD=enddatetuple.DDD, ) if (starttimetuple is not None and starttimetuple.tz is not None) and ( endtimetuple is not None and endtimetuple.tz != starttimetuple.tz ): # Copy the timezone across endtimetuple = TimeTuple( hh=endtimetuple.hh, mm=endtimetuple.mm, ss=endtimetuple.ss, tz=starttimetuple.tz, ) if enddatetuple is not None and endtimetuple is None: return newenddatetuple if enddatetuple is not None and endtimetuple is not None: return TupleBuilder.build_datetime(newenddatetuple, endtimetuple) return TupleBuilder.build_datetime(startdatetuple, endtimetuple) class TupleBuilder(BaseTimeBuilder): # Builder used to return the arguments as a tuple, cleans up some parse methods @classmethod def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None): return DateTuple(YYYY, MM, DD, Www, D, DDD) @classmethod def build_time(cls, hh=None, mm=None, ss=None, tz=None): return TimeTuple(hh, mm, ss, tz) @classmethod def build_datetime(cls, date, time): return DatetimeTuple(date, time) @classmethod def build_duration( cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None ): return DurationTuple(PnY, PnM, PnW, PnD, TnH, TnM, TnS) @classmethod def build_interval(cls, start=None, end=None, duration=None): return IntervalTuple(start, end, duration) @classmethod def build_repeating_interval(cls, R=None, Rnn=None, interval=None): return RepeatingIntervalTuple(R, Rnn, interval) @classmethod def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""): return TimezoneTuple(negative, Z, hh, mm, name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1613696531.0 aniso8601-9.0.1/aniso8601/builders/python.py0000664000175000017500000005315200000000000020476 0ustar00nielsenbnielsenb# -*- coding: utf-8 -*- # Copyright (c) 2021, Brandon Nielsen # All rights reserved. # # This software may be modified and distributed under the terms # of the BSD license. See the LICENSE file for details. import datetime from collections import namedtuple from functools import partial from aniso8601.builders import ( BaseTimeBuilder, DatetimeTuple, DateTuple, Limit, TimeTuple, TupleBuilder, cast, range_check, ) from aniso8601.exceptions import ( DayOutOfBoundsError, HoursOutOfBoundsError, ISOFormatError, LeapSecondError, MidnightBoundsError, MinutesOutOfBoundsError, MonthOutOfBoundsError, SecondsOutOfBoundsError, WeekOutOfBoundsError, YearOutOfBoundsError, ) from aniso8601.utcoffset import UTCOffset DAYS_PER_YEAR = 365 DAYS_PER_MONTH = 30 DAYS_PER_WEEK = 7 HOURS_PER_DAY = 24 MINUTES_PER_HOUR = 60 MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY SECONDS_PER_MINUTE = 60 SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE MICROSECONDS_PER_SECOND = int(1e6) MICROSECONDS_PER_MINUTE = 60 * MICROSECONDS_PER_SECOND MICROSECONDS_PER_HOUR = 60 * MICROSECONDS_PER_MINUTE MICROSECONDS_PER_DAY = 24 * MICROSECONDS_PER_HOUR MICROSECONDS_PER_WEEK = 7 * MICROSECONDS_PER_DAY MICROSECONDS_PER_MONTH = DAYS_PER_MONTH * MICROSECONDS_PER_DAY MICROSECONDS_PER_YEAR = DAYS_PER_YEAR * MICROSECONDS_PER_DAY TIMEDELTA_MAX_DAYS = datetime.timedelta.max.days FractionalComponent = namedtuple( "FractionalComponent", ["principal", "microsecondremainder"] ) def year_range_check(valuestr, limit): YYYYstr = valuestr # Truncated dates, like '19', refer to 1900-1999 inclusive, # we simply parse to 1900 if len(valuestr) < 4: # Shift 0s in from the left to form complete year YYYYstr = valuestr.ljust(4, "0") return range_check(YYYYstr, limit) def fractional_range_check(conversion, valuestr, limit): if valuestr is None: return None if "." in valuestr: castfunc = partial(_cast_to_fractional_component, conversion) else: castfunc = int value = cast(valuestr, castfunc, thrownmessage=limit.casterrorstring) if type(value) is FractionalComponent: tocheck = float(valuestr) else: tocheck = int(valuestr) if limit.min is not None and tocheck < limit.min: raise limit.rangeexception(limit.rangeerrorstring) if limit.max is not None and tocheck > limit.max: raise limit.rangeexception(limit.rangeerrorstring) return value def _cast_to_fractional_component(conversion, floatstr): # Splits a string with a decimal point into an int, and # int representing the floating point remainder as a number # of microseconds, determined by multiplying by conversion intpart, floatpart = floatstr.split(".") intvalue = int(intpart) preconvertedvalue = int(floatpart) convertedvalue = (preconvertedvalue * conversion) // (10 ** len(floatpart)) return FractionalComponent(intvalue, convertedvalue) class PythonTimeBuilder(BaseTimeBuilder): # 0000 (1 BC) is not representable as a Python date DATE_YYYY_LIMIT = Limit( "Invalid year string.", datetime.MINYEAR, datetime.MAXYEAR, YearOutOfBoundsError, "Year must be between {0}..{1}.".format(datetime.MINYEAR, datetime.MAXYEAR), year_range_check, ) TIME_HH_LIMIT = Limit( "Invalid hour string.", 0, 24, HoursOutOfBoundsError, "Hour must be between 0..24 with " "24 representing midnight.", partial(fractional_range_check, MICROSECONDS_PER_HOUR), ) TIME_MM_LIMIT = Limit( "Invalid minute string.", 0, 59, MinutesOutOfBoundsError, "Minute must be between 0..59.", partial(fractional_range_check, MICROSECONDS_PER_MINUTE), ) TIME_SS_LIMIT = Limit( "Invalid second string.", 0, 60, SecondsOutOfBoundsError, "Second must be between 0..60 with " "60 representing a leap second.", partial(fractional_range_check, MICROSECONDS_PER_SECOND), ) DURATION_PNY_LIMIT = Limit( "Invalid year duration string.", None, None, YearOutOfBoundsError, None, partial(fractional_range_check, MICROSECONDS_PER_YEAR), ) DURATION_PNM_LIMIT = Limit( "Invalid month duration string.", None, None, MonthOutOfBoundsError, None, partial(fractional_range_check, MICROSECONDS_PER_MONTH), ) DURATION_PNW_LIMIT = Limit( "Invalid week duration string.", None, None, WeekOutOfBoundsError, None, partial(fractional_range_check, MICROSECONDS_PER_WEEK), ) DURATION_PND_LIMIT = Limit( "Invalid day duration string.", None, None, DayOutOfBoundsError, None, partial(fractional_range_check, MICROSECONDS_PER_DAY), ) DURATION_TNH_LIMIT = Limit( "Invalid hour duration string.", None, None, HoursOutOfBoundsError, None, partial(fractional_range_check, MICROSECONDS_PER_HOUR), ) DURATION_TNM_LIMIT = Limit( "Invalid minute duration string.", None, None, MinutesOutOfBoundsError, None, partial(fractional_range_check, MICROSECONDS_PER_MINUTE), ) DURATION_TNS_LIMIT = Limit( "Invalid second duration string.", None, None, SecondsOutOfBoundsError, None, partial(fractional_range_check, MICROSECONDS_PER_SECOND), ) DATE_RANGE_DICT = BaseTimeBuilder.DATE_RANGE_DICT DATE_RANGE_DICT["YYYY"] = DATE_YYYY_LIMIT TIME_RANGE_DICT = {"hh": TIME_HH_LIMIT, "mm": TIME_MM_LIMIT, "ss": TIME_SS_LIMIT} DURATION_RANGE_DICT = { "PnY": DURATION_PNY_LIMIT, "PnM": DURATION_PNM_LIMIT, "PnW": DURATION_PNW_LIMIT, "PnD": DURATION_PND_LIMIT, "TnH": DURATION_TNH_LIMIT, "TnM": DURATION_TNM_LIMIT, "TnS": DURATION_TNS_LIMIT, } @classmethod def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None): YYYY, MM, DD, Www, D, DDD = cls.range_check_date(YYYY, MM, DD, Www, D, DDD) if MM is None: MM = 1 if DD is None: DD = 1 if DDD is not None: return PythonTimeBuilder._build_ordinal_date(YYYY, DDD) if Www is not None: return PythonTimeBuilder._build_week_date(YYYY, Www, isoday=D) return datetime.date(YYYY, MM, DD) @classmethod def build_time(cls, hh=None, mm=None, ss=None, tz=None): # Builds a time from the given parts, handling fractional arguments # where necessary hours = 0 minutes = 0 seconds = 0 microseconds = 0 hh, mm, ss, tz = cls.range_check_time(hh, mm, ss, tz) if type(hh) is FractionalComponent: hours = hh.principal microseconds = hh.microsecondremainder elif hh is not None: hours = hh if type(mm) is FractionalComponent: minutes = mm.principal microseconds = mm.microsecondremainder elif mm is not None: minutes = mm if type(ss) is FractionalComponent: seconds = ss.principal microseconds = ss.microsecondremainder elif ss is not None: seconds = ss ( hours, minutes, seconds, microseconds, ) = PythonTimeBuilder._distribute_microseconds( microseconds, (hours, minutes, seconds), (MICROSECONDS_PER_HOUR, MICROSECONDS_PER_MINUTE, MICROSECONDS_PER_SECOND), ) # Move midnight into range if hours == 24: hours = 0 # Datetimes don't handle fractional components, so we use a timedelta if tz is not None: return ( datetime.datetime( 1, 1, 1, hour=hours, minute=minutes, tzinfo=cls._build_object(tz) ) + datetime.timedelta(seconds=seconds, microseconds=microseconds) ).timetz() return ( datetime.datetime(1, 1, 1, hour=hours, minute=minutes) + datetime.timedelta(seconds=seconds, microseconds=microseconds) ).time() @classmethod def build_datetime(cls, date, time): return datetime.datetime.combine( cls._build_object(date), cls._build_object(time) ) @classmethod def build_duration( cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None ): # PnY and PnM will be distributed to PnD, microsecond remainder to TnS PnY, PnM, PnW, PnD, TnH, TnM, TnS = cls.range_check_duration( PnY, PnM, PnW, PnD, TnH, TnM, TnS ) seconds = TnS.principal microseconds = TnS.microsecondremainder return datetime.timedelta( days=PnD, seconds=seconds, microseconds=microseconds, minutes=TnM, hours=TnH, weeks=PnW, ) @classmethod def build_interval(cls, start=None, end=None, duration=None): start, end, duration = cls.range_check_interval(start, end, duration) if start is not None and end is not None: # / startobject = cls._build_object(start) endobject = cls._build_object(end) return (startobject, endobject) durationobject = cls._build_object(duration) # Determine if datetime promotion is required datetimerequired = ( duration.TnH is not None or duration.TnM is not None or duration.TnS is not None or durationobject.seconds != 0 or durationobject.microseconds != 0 ) if end is not None: # / endobject = cls._build_object(end) # Range check if type(end) is DateTuple and datetimerequired is True: # is a date, and requires datetime resolution return ( endobject, cls.build_datetime(end, TupleBuilder.build_time()) - durationobject, ) return (endobject, endobject - durationobject) # / startobject = cls._build_object(start) # Range check if type(start) is DateTuple and datetimerequired is True: # is a date, and requires datetime resolution return ( startobject, cls.build_datetime(start, TupleBuilder.build_time()) + durationobject, ) return (startobject, startobject + durationobject) @classmethod def build_repeating_interval(cls, R=None, Rnn=None, interval=None): startobject = None endobject = None R, Rnn, interval = cls.range_check_repeating_interval(R, Rnn, interval) if interval.start is not None: startobject = cls._build_object(interval.start) if interval.end is not None: endobject = cls._build_object(interval.end) if interval.duration is not None: durationobject = cls._build_object(interval.duration) else: durationobject = endobject - startobject if R is True: if startobject is not None: return cls._date_generator_unbounded(startobject, durationobject) return cls._date_generator_unbounded(endobject, -durationobject) iterations = int(Rnn) if startobject is not None: return cls._date_generator(startobject, durationobject, iterations) return cls._date_generator(endobject, -durationobject, iterations) @classmethod def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""): negative, Z, hh, mm, name = cls.range_check_timezone(negative, Z, hh, mm, name) if Z is True: # Z -> UTC return UTCOffset(name="UTC", minutes=0) tzhour = int(hh) if mm is not None: tzminute = int(mm) else: tzminute = 0 if negative is True: return UTCOffset(name=name, minutes=-(tzhour * 60 + tzminute)) return UTCOffset(name=name, minutes=tzhour * 60 + tzminute) @classmethod def range_check_duration( cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None, rangedict=None, ): years = 0 months = 0 days = 0 weeks = 0 hours = 0 minutes = 0 seconds = 0 microseconds = 0 PnY, PnM, PnW, PnD, TnH, TnM, TnS = BaseTimeBuilder.range_check_duration( PnY, PnM, PnW, PnD, TnH, TnM, TnS, rangedict=cls.DURATION_RANGE_DICT ) if PnY is not None: if type(PnY) is FractionalComponent: years = PnY.principal microseconds = PnY.microsecondremainder else: years = PnY if years * DAYS_PER_YEAR > TIMEDELTA_MAX_DAYS: raise YearOutOfBoundsError("Duration exceeds maximum timedelta size.") if PnM is not None: if type(PnM) is FractionalComponent: months = PnM.principal microseconds = PnM.microsecondremainder else: months = PnM if months * DAYS_PER_MONTH > TIMEDELTA_MAX_DAYS: raise MonthOutOfBoundsError("Duration exceeds maximum timedelta size.") if PnW is not None: if type(PnW) is FractionalComponent: weeks = PnW.principal microseconds = PnW.microsecondremainder else: weeks = PnW if weeks * DAYS_PER_WEEK > TIMEDELTA_MAX_DAYS: raise WeekOutOfBoundsError("Duration exceeds maximum timedelta size.") if PnD is not None: if type(PnD) is FractionalComponent: days = PnD.principal microseconds = PnD.microsecondremainder else: days = PnD if days > TIMEDELTA_MAX_DAYS: raise DayOutOfBoundsError("Duration exceeds maximum timedelta size.") if TnH is not None: if type(TnH) is FractionalComponent: hours = TnH.principal microseconds = TnH.microsecondremainder else: hours = TnH if hours // HOURS_PER_DAY > TIMEDELTA_MAX_DAYS: raise HoursOutOfBoundsError("Duration exceeds maximum timedelta size.") if TnM is not None: if type(TnM) is FractionalComponent: minutes = TnM.principal microseconds = TnM.microsecondremainder else: minutes = TnM if minutes // MINUTES_PER_DAY > TIMEDELTA_MAX_DAYS: raise MinutesOutOfBoundsError( "Duration exceeds maximum timedelta size." ) if TnS is not None: if type(TnS) is FractionalComponent: seconds = TnS.principal microseconds = TnS.microsecondremainder else: seconds = TnS if seconds // SECONDS_PER_DAY > TIMEDELTA_MAX_DAYS: raise SecondsOutOfBoundsError( "Duration exceeds maximum timedelta size." ) ( years, months, weeks, days, hours, minutes, seconds, microseconds, ) = PythonTimeBuilder._distribute_microseconds( microseconds, (years, months, weeks, days, hours, minutes, seconds), ( MICROSECONDS_PER_YEAR, MICROSECONDS_PER_MONTH, MICROSECONDS_PER_WEEK, MICROSECONDS_PER_DAY, MICROSECONDS_PER_HOUR, MICROSECONDS_PER_MINUTE, MICROSECONDS_PER_SECOND, ), ) # Note that weeks can be handled without conversion to days totaldays = years * DAYS_PER_YEAR + months * DAYS_PER_MONTH + days # Check against timedelta limits if ( totaldays + weeks * DAYS_PER_WEEK + hours // HOURS_PER_DAY + minutes // MINUTES_PER_DAY + seconds // SECONDS_PER_DAY > TIMEDELTA_MAX_DAYS ): raise DayOutOfBoundsError("Duration exceeds maximum timedelta size.") return ( None, None, weeks, totaldays, hours, minutes, FractionalComponent(seconds, microseconds), ) @classmethod def range_check_interval(cls, start=None, end=None, duration=None): # Handles concise format, range checks any potential durations if start is not None and end is not None: # / # Handle concise format if cls._is_interval_end_concise(end) is True: end = cls._combine_concise_interval_tuples(start, end) return (start, end, duration) durationobject = cls._build_object(duration) if end is not None: # / endobject = cls._build_object(end) # Range check if type(end) is DateTuple: enddatetime = cls.build_datetime(end, TupleBuilder.build_time()) if enddatetime - datetime.datetime.min < durationobject: raise YearOutOfBoundsError("Interval end less than minimium date.") else: mindatetime = datetime.datetime.min if end.time.tz is not None: mindatetime = mindatetime.replace(tzinfo=endobject.tzinfo) if endobject - mindatetime < durationobject: raise YearOutOfBoundsError("Interval end less than minimium date.") else: # / startobject = cls._build_object(start) # Range check if type(start) is DateTuple: startdatetime = cls.build_datetime(start, TupleBuilder.build_time()) if datetime.datetime.max - startdatetime < durationobject: raise YearOutOfBoundsError( "Interval end greater than maximum date." ) else: maxdatetime = datetime.datetime.max if start.time.tz is not None: maxdatetime = maxdatetime.replace(tzinfo=startobject.tzinfo) if maxdatetime - startobject < durationobject: raise YearOutOfBoundsError( "Interval end greater than maximum date." ) return (start, end, duration) @staticmethod def _build_week_date(isoyear, isoweek, isoday=None): if isoday is None: return PythonTimeBuilder._iso_year_start(isoyear) + datetime.timedelta( weeks=isoweek - 1 ) return PythonTimeBuilder._iso_year_start(isoyear) + datetime.timedelta( weeks=isoweek - 1, days=isoday - 1 ) @staticmethod def _build_ordinal_date(isoyear, isoday): # Day of year to a date # https://stackoverflow.com/questions/2427555/python-question-year-and-day-of-year-to-date builtdate = datetime.date(isoyear, 1, 1) + datetime.timedelta(days=isoday - 1) return builtdate @staticmethod def _iso_year_start(isoyear): # Given an ISO year, returns the equivalent of the start of the year # on the Gregorian calendar (which is used by Python) # Stolen from: # http://stackoverflow.com/questions/304256/whats-the-best-way-to-find-the-inverse-of-datetime-isocalendar # Determine the location of the 4th of January, the first week of # the ISO year is the week containing the 4th of January # http://en.wikipedia.org/wiki/ISO_week_date fourth_jan = datetime.date(isoyear, 1, 4) # Note the conversion from ISO day (1 - 7) and Python day (0 - 6) delta = datetime.timedelta(days=fourth_jan.isoweekday() - 1) # Return the start of the year return fourth_jan - delta @staticmethod def _date_generator(startdate, timedelta, iterations): currentdate = startdate currentiteration = 0 while currentiteration < iterations: yield currentdate # Update the values currentdate += timedelta currentiteration += 1 @staticmethod def _date_generator_unbounded(startdate, timedelta): currentdate = startdate while True: yield currentdate # Update the value currentdate += timedelta @staticmethod def _distribute_microseconds(todistribute, recipients, reductions): # Given a number of microseconds as int, a tuple of ints length n # to distribute to, and a tuple of ints length n to divide todistribute # by (from largest to smallest), returns a tuple of length n + 1, with # todistribute divided across recipients using the reductions, with # the final remainder returned as the final tuple member results = [] remainder = todistribute for index, reduction in enumerate(reductions): additional, remainder = divmod(remainder, reduction) results.append(recipients[index] + additional) # Always return the remaining microseconds results.append(remainder) return tuple(results) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1614648795.4541008 aniso8601-9.0.1/aniso8601/builders/tests/0000775000175000017500000000000000000000000017737 5ustar00nielsenbnielsenb././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1613696531.0 aniso8601-9.0.1/aniso8601/builders/tests/__init__.py0000664000175000017500000000032100000000000022044 0ustar00nielsenbnielsenb# -*- coding: utf-8 -*- # Copyright (c) 2021, Brandon Nielsen # All rights reserved. # # This software may be modified and distributed under the terms # of the BSD license. See the LICENSE file for details. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1613696531.0 aniso8601-9.0.1/aniso8601/builders/tests/test_init.py0000664000175000017500000007245500000000000022330 0ustar00nielsenbnielsenb# -*- coding: utf-8 -*- # Copyright (c) 2021, Brandon Nielsen # All rights reserved. # # This software may be modified and distributed under the terms # of the BSD license. See the LICENSE file for details. import unittest import aniso8601 from aniso8601.builders import ( BaseTimeBuilder, DatetimeTuple, DateTuple, DurationTuple, IntervalTuple, RepeatingIntervalTuple, TimeTuple, TimezoneTuple, TupleBuilder, cast, ) from aniso8601.exceptions import ( DayOutOfBoundsError, HoursOutOfBoundsError, ISOFormatError, LeapSecondError, MidnightBoundsError, MinutesOutOfBoundsError, MonthOutOfBoundsError, SecondsOutOfBoundsError, WeekOutOfBoundsError, ) from aniso8601.tests.compat import mock class LeapSecondSupportingTestBuilder(BaseTimeBuilder): LEAP_SECONDS_SUPPORTED = True class TestBuilderFunctions(unittest.TestCase): def test_cast(self): self.assertEqual(cast("1", int), 1) self.assertEqual(cast("-2", int), -2) self.assertEqual(cast("3", float), float(3)) self.assertEqual(cast("-4", float), float(-4)) self.assertEqual(cast("5.6", float), 5.6) self.assertEqual(cast("-7.8", float), -7.8) def test_cast_exception(self): with self.assertRaises(ISOFormatError): cast("asdf", int) with self.assertRaises(ISOFormatError): cast("asdf", float) def test_cast_caughtexception(self): def tester(value): raise RuntimeError with self.assertRaises(ISOFormatError): cast("asdf", tester, caughtexceptions=(RuntimeError,)) def test_cast_thrownexception(self): with self.assertRaises(RuntimeError): cast("asdf", int, thrownexception=RuntimeError) class TestBaseTimeBuilder(unittest.TestCase): def test_build_date(self): with self.assertRaises(NotImplementedError): BaseTimeBuilder.build_date() def test_build_time(self): with self.assertRaises(NotImplementedError): BaseTimeBuilder.build_time() def test_build_datetime(self): with self.assertRaises(NotImplementedError): BaseTimeBuilder.build_datetime(None, None) def test_build_duration(self): with self.assertRaises(NotImplementedError): BaseTimeBuilder.build_duration() def test_build_interval(self): with self.assertRaises(NotImplementedError): BaseTimeBuilder.build_interval() def test_build_repeating_interval(self): with self.assertRaises(NotImplementedError): BaseTimeBuilder.build_repeating_interval() def test_build_timezone(self): with self.assertRaises(NotImplementedError): BaseTimeBuilder.build_timezone() def test_range_check_date(self): # Check the calendar for day ranges with self.assertRaises(DayOutOfBoundsError): BaseTimeBuilder.range_check_date(YYYY="0007", MM="02", DD="30") with self.assertRaises(DayOutOfBoundsError): BaseTimeBuilder.range_check_date(YYYY="0007", DDD="366") with self.assertRaises(MonthOutOfBoundsError): BaseTimeBuilder.range_check_date(YYYY="4333", MM="30", DD="30") # 0 isn't a valid week number with self.assertRaises(WeekOutOfBoundsError): BaseTimeBuilder.range_check_date(YYYY="2003", Www="00") # Week must not be larger than 53 with self.assertRaises(WeekOutOfBoundsError): BaseTimeBuilder.range_check_date(YYYY="2004", Www="54") # 0 isn't a valid day number with self.assertRaises(DayOutOfBoundsError): BaseTimeBuilder.range_check_date(YYYY="2001", Www="02", D="0") # Day must not be larger than 7 with self.assertRaises(DayOutOfBoundsError): BaseTimeBuilder.range_check_date(YYYY="2001", Www="02", D="8") with self.assertRaises(DayOutOfBoundsError): BaseTimeBuilder.range_check_date(YYYY="1981", DDD="000") # Day must be 365, or 366, not larger with self.assertRaises(DayOutOfBoundsError): BaseTimeBuilder.range_check_date(YYYY="1234", DDD="000") with self.assertRaises(DayOutOfBoundsError): BaseTimeBuilder.range_check_date(YYYY="1234", DDD="367") # https://bitbucket.org/nielsenb/aniso8601/issues/14/parsing-ordinal-dates-should-only-allow with self.assertRaises(DayOutOfBoundsError): BaseTimeBuilder.range_check_date(YYYY="1981", DDD="366") # Make sure Nones pass through unmodified self.assertEqual( BaseTimeBuilder.range_check_date(rangedict={}), (None, None, None, None, None, None), ) def test_range_check_time(self): # Leap seconds not supported # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is # https://bitbucket.org/nielsenb/aniso8601/issues/13/parsing-of-leap-second-gives-wildly with self.assertRaises(LeapSecondError): BaseTimeBuilder.range_check_time(hh="23", mm="59", ss="60") with self.assertRaises(SecondsOutOfBoundsError): BaseTimeBuilder.range_check_time(hh="00", mm="00", ss="60") with self.assertRaises(SecondsOutOfBoundsError): BaseTimeBuilder.range_check_time(hh="00", mm="00", ss="61") with self.assertRaises(MinutesOutOfBoundsError): BaseTimeBuilder.range_check_time(hh="00", mm="61") with self.assertRaises(MinutesOutOfBoundsError): BaseTimeBuilder.range_check_time(hh="00", mm="60") with self.assertRaises(MinutesOutOfBoundsError): BaseTimeBuilder.range_check_time(hh="00", mm="60.1") with self.assertRaises(HoursOutOfBoundsError): BaseTimeBuilder.range_check_time(hh="25") # Hour 24 can only represent midnight with self.assertRaises(MidnightBoundsError): BaseTimeBuilder.range_check_time(hh="24", mm="00", ss="01") with self.assertRaises(MidnightBoundsError): BaseTimeBuilder.range_check_time(hh="24", mm="00.1") with self.assertRaises(MidnightBoundsError): BaseTimeBuilder.range_check_time(hh="24", mm="01") with self.assertRaises(MidnightBoundsError): BaseTimeBuilder.range_check_time(hh="24.1") # Leap seconds not supported # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is # https://bitbucket.org/nielsenb/aniso8601/issues/13/parsing-of-leap-second-gives-wildly with self.assertRaises(LeapSecondError): BaseTimeBuilder.range_check_time(hh="23", mm="59", ss="60") # Make sure Nones pass through unmodified self.assertEqual( BaseTimeBuilder.range_check_time(rangedict={}), (None, None, None, None) ) def test_range_check_time_leap_seconds_supported(self): self.assertEqual( LeapSecondSupportingTestBuilder.range_check_time(hh="23", mm="59", ss="60"), (23, 59, 60, None), ) with self.assertRaises(SecondsOutOfBoundsError): LeapSecondSupportingTestBuilder.range_check_time(hh="01", mm="02", ss="60") def test_range_check_duration(self): self.assertEqual( BaseTimeBuilder.range_check_duration(), (None, None, None, None, None, None, None), ) self.assertEqual( BaseTimeBuilder.range_check_duration(rangedict={}), (None, None, None, None, None, None, None), ) def test_range_check_repeating_interval(self): self.assertEqual( BaseTimeBuilder.range_check_repeating_interval(), (None, None, None) ) self.assertEqual( BaseTimeBuilder.range_check_repeating_interval(rangedict={}), (None, None, None), ) def test_range_check_timezone(self): self.assertEqual( BaseTimeBuilder.range_check_timezone(), (None, None, None, None, "") ) self.assertEqual( BaseTimeBuilder.range_check_timezone(rangedict={}), (None, None, None, None, ""), ) def test_build_object(self): datetest = ( DateTuple("1", "2", "3", "4", "5", "6"), {"YYYY": "1", "MM": "2", "DD": "3", "Www": "4", "D": "5", "DDD": "6"}, ) timetest = ( TimeTuple("1", "2", "3", TimezoneTuple(False, False, "4", "5", "tz name")), { "hh": "1", "mm": "2", "ss": "3", "tz": TimezoneTuple(False, False, "4", "5", "tz name"), }, ) datetimetest = ( DatetimeTuple( DateTuple("1", "2", "3", "4", "5", "6"), TimeTuple( "7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name") ), ), ( DateTuple("1", "2", "3", "4", "5", "6"), TimeTuple( "7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name") ), ), ) durationtest = ( DurationTuple("1", "2", "3", "4", "5", "6", "7"), { "PnY": "1", "PnM": "2", "PnW": "3", "PnD": "4", "TnH": "5", "TnM": "6", "TnS": "7", }, ) intervaltests = ( ( IntervalTuple( DateTuple("1", "2", "3", "4", "5", "6"), DateTuple("7", "8", "9", "10", "11", "12"), None, ), { "start": DateTuple("1", "2", "3", "4", "5", "6"), "end": DateTuple("7", "8", "9", "10", "11", "12"), "duration": None, }, ), ( IntervalTuple( DateTuple("1", "2", "3", "4", "5", "6"), None, DurationTuple("7", "8", "9", "10", "11", "12", "13"), ), { "start": DateTuple("1", "2", "3", "4", "5", "6"), "end": None, "duration": DurationTuple("7", "8", "9", "10", "11", "12", "13"), }, ), ( IntervalTuple( None, TimeTuple( "1", "2", "3", TimezoneTuple(True, False, "4", "5", "tz name") ), DurationTuple("6", "7", "8", "9", "10", "11", "12"), ), { "start": None, "end": TimeTuple( "1", "2", "3", TimezoneTuple(True, False, "4", "5", "tz name") ), "duration": DurationTuple("6", "7", "8", "9", "10", "11", "12"), }, ), ) repeatingintervaltests = ( ( RepeatingIntervalTuple( True, None, IntervalTuple( DateTuple("1", "2", "3", "4", "5", "6"), DateTuple("7", "8", "9", "10", "11", "12"), None, ), ), { "R": True, "Rnn": None, "interval": IntervalTuple( DateTuple("1", "2", "3", "4", "5", "6"), DateTuple("7", "8", "9", "10", "11", "12"), None, ), }, ), ( RepeatingIntervalTuple( False, "1", IntervalTuple( DatetimeTuple( DateTuple("2", "3", "4", "5", "6", "7"), TimeTuple("8", "9", "10", None), ), DatetimeTuple( DateTuple("11", "12", "13", "14", "15", "16"), TimeTuple("17", "18", "19", None), ), None, ), ), { "R": False, "Rnn": "1", "interval": IntervalTuple( DatetimeTuple( DateTuple("2", "3", "4", "5", "6", "7"), TimeTuple("8", "9", "10", None), ), DatetimeTuple( DateTuple("11", "12", "13", "14", "15", "16"), TimeTuple("17", "18", "19", None), ), None, ), }, ), ) timezonetest = ( TimezoneTuple(False, False, "1", "2", "+01:02"), {"negative": False, "Z": False, "hh": "1", "mm": "2", "name": "+01:02"}, ) with mock.patch.object( aniso8601.builders.BaseTimeBuilder, "build_date" ) as mock_build: mock_build.return_value = datetest[0] result = BaseTimeBuilder._build_object(datetest[0]) self.assertEqual(result, datetest[0]) mock_build.assert_called_once_with(**datetest[1]) with mock.patch.object( aniso8601.builders.BaseTimeBuilder, "build_time" ) as mock_build: mock_build.return_value = timetest[0] result = BaseTimeBuilder._build_object(timetest[0]) self.assertEqual(result, timetest[0]) mock_build.assert_called_once_with(**timetest[1]) with mock.patch.object( aniso8601.builders.BaseTimeBuilder, "build_datetime" ) as mock_build: mock_build.return_value = datetimetest[0] result = BaseTimeBuilder._build_object(datetimetest[0]) self.assertEqual(result, datetimetest[0]) mock_build.assert_called_once_with(*datetimetest[1]) with mock.patch.object( aniso8601.builders.BaseTimeBuilder, "build_duration" ) as mock_build: mock_build.return_value = durationtest[0] result = BaseTimeBuilder._build_object(durationtest[0]) self.assertEqual(result, durationtest[0]) mock_build.assert_called_once_with(**durationtest[1]) for intervaltest in intervaltests: with mock.patch.object( aniso8601.builders.BaseTimeBuilder, "build_interval" ) as mock_build: mock_build.return_value = intervaltest[0] result = BaseTimeBuilder._build_object(intervaltest[0]) self.assertEqual(result, intervaltest[0]) mock_build.assert_called_once_with(**intervaltest[1]) for repeatingintervaltest in repeatingintervaltests: with mock.patch.object( aniso8601.builders.BaseTimeBuilder, "build_repeating_interval" ) as mock_build: mock_build.return_value = repeatingintervaltest[0] result = BaseTimeBuilder._build_object(repeatingintervaltest[0]) self.assertEqual(result, repeatingintervaltest[0]) mock_build.assert_called_once_with(**repeatingintervaltest[1]) with mock.patch.object( aniso8601.builders.BaseTimeBuilder, "build_timezone" ) as mock_build: mock_build.return_value = timezonetest[0] result = BaseTimeBuilder._build_object(timezonetest[0]) self.assertEqual(result, timezonetest[0]) mock_build.assert_called_once_with(**timezonetest[1]) def test_is_interval_end_concise(self): self.assertTrue( BaseTimeBuilder._is_interval_end_concise(TimeTuple("1", "2", "3", None)) ) self.assertTrue( BaseTimeBuilder._is_interval_end_concise( DateTuple(None, "2", "3", "4", "5", "6") ) ) self.assertTrue( BaseTimeBuilder._is_interval_end_concise( DatetimeTuple( DateTuple(None, "2", "3", "4", "5", "6"), TimeTuple("7", "8", "9", None), ) ) ) self.assertFalse( BaseTimeBuilder._is_interval_end_concise( DateTuple("1", "2", "3", "4", "5", "6") ) ) self.assertFalse( BaseTimeBuilder._is_interval_end_concise( DatetimeTuple( DateTuple("1", "2", "3", "4", "5", "6"), TimeTuple("7", "8", "9", None), ) ) ) def test_combine_concise_interval_tuples(self): testtuples = ( ( DateTuple("2020", "01", "01", None, None, None), DateTuple(None, None, "02", None, None, None), DateTuple("2020", "01", "02", None, None, None), ), ( DateTuple("2008", "02", "15", None, None, None), DateTuple(None, "03", "14", None, None, None), DateTuple("2008", "03", "14", None, None, None), ), ( DatetimeTuple( DateTuple("2007", "12", "14", None, None, None), TimeTuple("13", "30", None, None), ), TimeTuple("15", "30", None, None), DatetimeTuple( DateTuple("2007", "12", "14", None, None, None), TimeTuple("15", "30", None, None), ), ), ( DatetimeTuple( DateTuple("2007", "11", "13", None, None, None), TimeTuple("09", "00", None, None), ), DatetimeTuple( DateTuple(None, None, "15", None, None, None), TimeTuple("17", "00", None, None), ), DatetimeTuple( DateTuple("2007", "11", "15", None, None, None), TimeTuple("17", "00", None, None), ), ), ( DatetimeTuple( DateTuple("2007", "11", "13", None, None, None), TimeTuple("00", "00", None, None), ), DatetimeTuple( DateTuple(None, None, "16", None, None, None), TimeTuple("00", "00", None, None), ), DatetimeTuple( DateTuple("2007", "11", "16", None, None, None), TimeTuple("00", "00", None, None), ), ), ( DatetimeTuple( DateTuple("2007", "11", "13", None, None, None), TimeTuple( "09", "00", None, TimezoneTuple(False, True, None, None, "Z") ), ), DatetimeTuple( DateTuple(None, None, "15", None, None, None), TimeTuple("17", "00", None, None), ), DatetimeTuple( DateTuple("2007", "11", "15", None, None, None), TimeTuple( "17", "00", None, TimezoneTuple(False, True, None, None, "Z") ), ), ), ) for testtuple in testtuples: result = BaseTimeBuilder._combine_concise_interval_tuples( testtuple[0], testtuple[1] ) self.assertEqual(result, testtuple[2]) class TestTupleBuilder(unittest.TestCase): def test_build_date(self): datetuple = TupleBuilder.build_date() self.assertEqual(datetuple, DateTuple(None, None, None, None, None, None)) datetuple = TupleBuilder.build_date( YYYY="1", MM="2", DD="3", Www="4", D="5", DDD="6" ) self.assertEqual(datetuple, DateTuple("1", "2", "3", "4", "5", "6")) def test_build_time(self): testtuples = ( ({}, TimeTuple(None, None, None, None)), ( {"hh": "1", "mm": "2", "ss": "3", "tz": None}, TimeTuple("1", "2", "3", None), ), ( { "hh": "1", "mm": "2", "ss": "3", "tz": TimezoneTuple(False, False, "4", "5", "tz name"), }, TimeTuple( "1", "2", "3", TimezoneTuple(False, False, "4", "5", "tz name") ), ), ) for testtuple in testtuples: self.assertEqual(TupleBuilder.build_time(**testtuple[0]), testtuple[1]) def test_build_datetime(self): testtuples = ( ( { "date": DateTuple("1", "2", "3", "4", "5", "6"), "time": TimeTuple("7", "8", "9", None), }, DatetimeTuple( DateTuple("1", "2", "3", "4", "5", "6"), TimeTuple("7", "8", "9", None), ), ), ( { "date": DateTuple("1", "2", "3", "4", "5", "6"), "time": TimeTuple( "7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name") ), }, DatetimeTuple( DateTuple("1", "2", "3", "4", "5", "6"), TimeTuple( "7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name") ), ), ), ) for testtuple in testtuples: self.assertEqual(TupleBuilder.build_datetime(**testtuple[0]), testtuple[1]) def test_build_duration(self): testtuples = ( ({}, DurationTuple(None, None, None, None, None, None, None)), ( { "PnY": "1", "PnM": "2", "PnW": "3", "PnD": "4", "TnH": "5", "TnM": "6", "TnS": "7", }, DurationTuple("1", "2", "3", "4", "5", "6", "7"), ), ) for testtuple in testtuples: self.assertEqual(TupleBuilder.build_duration(**testtuple[0]), testtuple[1]) def test_build_interval(self): testtuples = ( ({}, IntervalTuple(None, None, None)), ( { "start": DateTuple("1", "2", "3", "4", "5", "6"), "end": DateTuple("7", "8", "9", "10", "11", "12"), }, IntervalTuple( DateTuple("1", "2", "3", "4", "5", "6"), DateTuple("7", "8", "9", "10", "11", "12"), None, ), ), ( { "start": TimeTuple( "1", "2", "3", TimezoneTuple(True, False, "7", "8", "tz name") ), "end": TimeTuple( "4", "5", "6", TimezoneTuple(False, False, "9", "10", "tz name") ), }, IntervalTuple( TimeTuple( "1", "2", "3", TimezoneTuple(True, False, "7", "8", "tz name") ), TimeTuple( "4", "5", "6", TimezoneTuple(False, False, "9", "10", "tz name") ), None, ), ), ( { "start": DatetimeTuple( DateTuple("1", "2", "3", "4", "5", "6"), TimeTuple( "7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name"), ), ), "end": DatetimeTuple( DateTuple("12", "13", "14", "15", "16", "17"), TimeTuple( "18", "19", "20", TimezoneTuple(False, False, "21", "22", "tz name"), ), ), }, IntervalTuple( DatetimeTuple( DateTuple("1", "2", "3", "4", "5", "6"), TimeTuple( "7", "8", "9", TimezoneTuple(True, False, "10", "11", "tz name"), ), ), DatetimeTuple( DateTuple("12", "13", "14", "15", "16", "17"), TimeTuple( "18", "19", "20", TimezoneTuple(False, False, "21", "22", "tz name"), ), ), None, ), ), ( { "start": DateTuple("1", "2", "3", "4", "5", "6"), "end": None, "duration": DurationTuple("7", "8", "9", "10", "11", "12", "13"), }, IntervalTuple( DateTuple("1", "2", "3", "4", "5", "6"), None, DurationTuple("7", "8", "9", "10", "11", "12", "13"), ), ), ( { "start": None, "end": TimeTuple( "1", "2", "3", TimezoneTuple(True, False, "4", "5", "tz name") ), "duration": DurationTuple("6", "7", "8", "9", "10", "11", "12"), }, IntervalTuple( None, TimeTuple( "1", "2", "3", TimezoneTuple(True, False, "4", "5", "tz name") ), DurationTuple("6", "7", "8", "9", "10", "11", "12"), ), ), ) for testtuple in testtuples: self.assertEqual(TupleBuilder.build_interval(**testtuple[0]), testtuple[1]) def test_build_repeating_interval(self): testtuples = ( ({}, RepeatingIntervalTuple(None, None, None)), ( { "R": True, "interval": IntervalTuple( DateTuple("1", "2", "3", "4", "5", "6"), DateTuple("7", "8", "9", "10", "11", "12"), None, ), }, RepeatingIntervalTuple( True, None, IntervalTuple( DateTuple("1", "2", "3", "4", "5", "6"), DateTuple("7", "8", "9", "10", "11", "12"), None, ), ), ), ( { "R": False, "Rnn": "1", "interval": IntervalTuple( DatetimeTuple( DateTuple("2", "3", "4", "5", "6", "7"), TimeTuple("8", "9", "10", None), ), DatetimeTuple( DateTuple("11", "12", "13", "14", "15", "16"), TimeTuple("17", "18", "19", None), ), None, ), }, RepeatingIntervalTuple( False, "1", IntervalTuple( DatetimeTuple( DateTuple("2", "3", "4", "5", "6", "7"), TimeTuple("8", "9", "10", None), ), DatetimeTuple( DateTuple("11", "12", "13", "14", "15", "16"), TimeTuple("17", "18", "19", None), ), None, ), ), ), ) for testtuple in testtuples: result = TupleBuilder.build_repeating_interval(**testtuple[0]) self.assertEqual(result, testtuple[1]) def test_build_timezone(self): testtuples = ( ({}, TimezoneTuple(None, None, None, None, "")), ( {"negative": False, "Z": True, "name": "UTC"}, TimezoneTuple(False, True, None, None, "UTC"), ), ( {"negative": False, "Z": False, "hh": "1", "mm": "2", "name": "+01:02"}, TimezoneTuple(False, False, "1", "2", "+01:02"), ), ( {"negative": True, "Z": False, "hh": "1", "mm": "2", "name": "-01:02"}, TimezoneTuple(True, False, "1", "2", "-01:02"), ), ) for testtuple in testtuples: result = TupleBuilder.build_timezone(**testtuple[0]) self.assertEqual(result, testtuple[1]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1613696531.0 aniso8601-9.0.1/aniso8601/builders/tests/test_python.py0000664000175000017500000017023100000000000022675 0ustar00nielsenbnielsenb# -*- coding: utf-8 -*- # Copyright (c) 2021, Brandon Nielsen # All rights reserved. # # This software may be modified and distributed under the terms # of the BSD license. See the LICENSE file for details. import datetime import unittest from aniso8601 import compat from aniso8601.builders import ( DatetimeTuple, DateTuple, DurationTuple, IntervalTuple, Limit, TimeTuple, TimezoneTuple, ) from aniso8601.builders.python import ( FractionalComponent, PythonTimeBuilder, _cast_to_fractional_component, fractional_range_check, year_range_check, ) from aniso8601.exceptions import ( DayOutOfBoundsError, HoursOutOfBoundsError, ISOFormatError, LeapSecondError, MidnightBoundsError, MinutesOutOfBoundsError, MonthOutOfBoundsError, SecondsOutOfBoundsError, WeekOutOfBoundsError, YearOutOfBoundsError, ) from aniso8601.utcoffset import UTCOffset class TestPythonTimeBuilder_UtiltyFunctions(unittest.TestCase): def test_year_range_check(self): yearlimit = Limit( "Invalid year string.", 0000, 9999, YearOutOfBoundsError, "Year must be between 1..9999.", None, ) self.assertEqual(year_range_check("1", yearlimit), 1000) def test_fractional_range_check(self): limit = Limit( "Invalid string.", -1, 1, ValueError, "Value must be between -1..1.", None ) self.assertEqual(fractional_range_check(10, "1", limit), 1) self.assertEqual(fractional_range_check(10, "-1", limit), -1) self.assertEqual( fractional_range_check(10, "0.1", limit), FractionalComponent(0, 1) ) self.assertEqual( fractional_range_check(10, "-0.1", limit), FractionalComponent(-0, 1) ) with self.assertRaises(ValueError): fractional_range_check(10, "1.1", limit) with self.assertRaises(ValueError): fractional_range_check(10, "-1.1", limit) def test_cast_to_fractional_component(self): self.assertEqual( _cast_to_fractional_component(10, "1.1"), FractionalComponent(1, 1) ) self.assertEqual( _cast_to_fractional_component(10, "-1.1"), FractionalComponent(-1, 1) ) self.assertEqual( _cast_to_fractional_component(100, "1.1"), FractionalComponent(1, 10) ) self.assertEqual( _cast_to_fractional_component(100, "-1.1"), FractionalComponent(-1, 10) ) class TestPythonTimeBuilder(unittest.TestCase): def test_build_date(self): testtuples = ( ( { "YYYY": "2013", "MM": None, "DD": None, "Www": None, "D": None, "DDD": None, }, datetime.date(2013, 1, 1), ), ( { "YYYY": "0001", "MM": None, "DD": None, "Www": None, "D": None, "DDD": None, }, datetime.date(1, 1, 1), ), ( { "YYYY": "1900", "MM": None, "DD": None, "Www": None, "D": None, "DDD": None, }, datetime.date(1900, 1, 1), ), ( { "YYYY": "1981", "MM": "04", "DD": "05", "Www": None, "D": None, "DDD": None, }, datetime.date(1981, 4, 5), ), ( { "YYYY": "1981", "MM": "04", "DD": None, "Www": None, "D": None, "DDD": None, }, datetime.date(1981, 4, 1), ), ( { "YYYY": "1981", "MM": None, "DD": None, "Www": None, "D": None, "DDD": "095", }, datetime.date(1981, 4, 5), ), ( { "YYYY": "1981", "MM": None, "DD": None, "Www": None, "D": None, "DDD": "365", }, datetime.date(1981, 12, 31), ), ( { "YYYY": "1980", "MM": None, "DD": None, "Www": None, "D": None, "DDD": "366", }, datetime.date(1980, 12, 31), ), # Make sure we shift in zeros ( { "YYYY": "1", "MM": None, "DD": None, "Www": None, "D": None, "DDD": None, }, datetime.date(1000, 1, 1), ), ( { "YYYY": "12", "MM": None, "DD": None, "Www": None, "D": None, "DDD": None, }, datetime.date(1200, 1, 1), ), ( { "YYYY": "123", "MM": None, "DD": None, "Www": None, "D": None, "DDD": None, }, datetime.date(1230, 1, 1), ), ) for testtuple in testtuples: result = PythonTimeBuilder.build_date(**testtuple[0]) self.assertEqual(result, testtuple[1]) # Test weekday testtuples = ( ( { "YYYY": "2004", "MM": None, "DD": None, "Www": "53", "D": None, "DDD": None, }, datetime.date(2004, 12, 27), 0, ), ( { "YYYY": "2009", "MM": None, "DD": None, "Www": "01", "D": None, "DDD": None, }, datetime.date(2008, 12, 29), 0, ), ( { "YYYY": "2010", "MM": None, "DD": None, "Www": "01", "D": None, "DDD": None, }, datetime.date(2010, 1, 4), 0, ), ( { "YYYY": "2009", "MM": None, "DD": None, "Www": "53", "D": None, "DDD": None, }, datetime.date(2009, 12, 28), 0, ), ( { "YYYY": "2009", "MM": None, "DD": None, "Www": "01", "D": "1", "DDD": None, }, datetime.date(2008, 12, 29), 0, ), ( { "YYYY": "2009", "MM": None, "DD": None, "Www": "53", "D": "7", "DDD": None, }, datetime.date(2010, 1, 3), 6, ), ( { "YYYY": "2010", "MM": None, "DD": None, "Www": "01", "D": "1", "DDD": None, }, datetime.date(2010, 1, 4), 0, ), ( { "YYYY": "2004", "MM": None, "DD": None, "Www": "53", "D": "6", "DDD": None, }, datetime.date(2005, 1, 1), 5, ), ) for testtuple in testtuples: result = PythonTimeBuilder.build_date(**testtuple[0]) self.assertEqual(result, testtuple[1]) self.assertEqual(result.weekday(), testtuple[2]) def test_build_time(self): testtuples = ( ({}, datetime.time()), ({"hh": "12.5"}, datetime.time(hour=12, minute=30)), ( {"hh": "23.99999999997"}, datetime.time(hour=23, minute=59, second=59, microsecond=999999), ), ({"hh": "1", "mm": "23"}, datetime.time(hour=1, minute=23)), ( {"hh": "1", "mm": "23.4567"}, datetime.time(hour=1, minute=23, second=27, microsecond=402000), ), ( {"hh": "14", "mm": "43.999999997"}, datetime.time(hour=14, minute=43, second=59, microsecond=999999), ), ( {"hh": "1", "mm": "23", "ss": "45"}, datetime.time(hour=1, minute=23, second=45), ), ( {"hh": "23", "mm": "21", "ss": "28.512400"}, datetime.time(hour=23, minute=21, second=28, microsecond=512400), ), ( {"hh": "01", "mm": "03", "ss": "11.858714"}, datetime.time(hour=1, minute=3, second=11, microsecond=858714), ), ( {"hh": "14", "mm": "43", "ss": "59.9999997"}, datetime.time(hour=14, minute=43, second=59, microsecond=999999), ), ({"hh": "24"}, datetime.time(hour=0)), ({"hh": "24", "mm": "00"}, datetime.time(hour=0)), ({"hh": "24", "mm": "00", "ss": "00"}, datetime.time(hour=0)), ( {"tz": TimezoneTuple(False, None, "00", "00", "UTC")}, datetime.time(tzinfo=UTCOffset(name="UTC", minutes=0)), ), ( { "hh": "23", "mm": "21", "ss": "28.512400", "tz": TimezoneTuple(False, None, "00", "00", "+00:00"), }, datetime.time( hour=23, minute=21, second=28, microsecond=512400, tzinfo=UTCOffset(name="+00:00", minutes=0), ), ), ( { "hh": "1", "mm": "23", "tz": TimezoneTuple(False, None, "01", "00", "+1"), }, datetime.time( hour=1, minute=23, tzinfo=UTCOffset(name="+1", minutes=60) ), ), ( { "hh": "1", "mm": "23.4567", "tz": TimezoneTuple(True, None, "01", "00", "-1"), }, datetime.time( hour=1, minute=23, second=27, microsecond=402000, tzinfo=UTCOffset(name="-1", minutes=-60), ), ), ( { "hh": "23", "mm": "21", "ss": "28.512400", "tz": TimezoneTuple(False, None, "01", "30", "+1:30"), }, datetime.time( hour=23, minute=21, second=28, microsecond=512400, tzinfo=UTCOffset(name="+1:30", minutes=90), ), ), ( { "hh": "23", "mm": "21", "ss": "28.512400", "tz": TimezoneTuple(False, None, "11", "15", "+11:15"), }, datetime.time( hour=23, minute=21, second=28, microsecond=512400, tzinfo=UTCOffset(name="+11:15", minutes=675), ), ), ( { "hh": "23", "mm": "21", "ss": "28.512400", "tz": TimezoneTuple(False, None, "12", "34", "+12:34"), }, datetime.time( hour=23, minute=21, second=28, microsecond=512400, tzinfo=UTCOffset(name="+12:34", minutes=754), ), ), ( { "hh": "23", "mm": "21", "ss": "28.512400", "tz": TimezoneTuple(False, None, "00", "00", "UTC"), }, datetime.time( hour=23, minute=21, second=28, microsecond=512400, tzinfo=UTCOffset(name="UTC", minutes=0), ), ), # Make sure we truncate, not round # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is # https://bitbucket.org/nielsenb/aniso8601/issues/21/sub-microsecond-precision-is-lost-when ( {"hh": "14.9999999999999999"}, datetime.time(hour=14, minute=59, second=59, microsecond=999999), ), ({"mm": "0.00000000999"}, datetime.time()), ({"mm": "0.0000000999"}, datetime.time(microsecond=5)), ({"ss": "0.0000001"}, datetime.time()), ({"ss": "2.0000048"}, datetime.time(second=2, microsecond=4)), ) for testtuple in testtuples: result = PythonTimeBuilder.build_time(**testtuple[0]) self.assertEqual(result, testtuple[1]) def test_build_datetime(self): testtuples = ( ( ( DateTuple("2019", "06", "05", None, None, None), TimeTuple("01", "03", "11.858714", None), ), datetime.datetime( 2019, 6, 5, hour=1, minute=3, second=11, microsecond=858714 ), ), ( ( DateTuple("1234", "02", "03", None, None, None), TimeTuple("23", "21", "28.512400", None), ), datetime.datetime( 1234, 2, 3, hour=23, minute=21, second=28, microsecond=512400 ), ), ( ( DateTuple("1981", "04", "05", None, None, None), TimeTuple( "23", "21", "28.512400", TimezoneTuple(False, None, "11", "15", "+11:15"), ), ), datetime.datetime( 1981, 4, 5, hour=23, minute=21, second=28, microsecond=512400, tzinfo=UTCOffset(name="+11:15", minutes=675), ), ), ) for testtuple in testtuples: result = PythonTimeBuilder.build_datetime(*testtuple[0]) self.assertEqual(result, testtuple[1]) def test_build_duration(self): testtuples = ( ( { "PnY": "1", "PnM": "2", "PnD": "3", "TnH": "4", "TnM": "54", "TnS": "6", }, datetime.timedelta(days=428, hours=4, minutes=54, seconds=6), ), ( { "PnY": "1", "PnM": "2", "PnD": "3", "TnH": "4", "TnM": "54", "TnS": "6.5", }, datetime.timedelta(days=428, hours=4, minutes=54, seconds=6.5), ), ({"PnY": "1", "PnM": "2", "PnD": "3"}, datetime.timedelta(days=428)), ({"PnY": "1", "PnM": "2", "PnD": "3.5"}, datetime.timedelta(days=428.5)), ( {"TnH": "4", "TnM": "54", "TnS": "6.5"}, datetime.timedelta(hours=4, minutes=54, seconds=6.5), ), ( {"TnH": "1", "TnM": "3", "TnS": "11.858714"}, datetime.timedelta(hours=1, minutes=3, seconds=11, microseconds=858714), ), ( {"TnH": "4", "TnM": "54", "TnS": "28.512400"}, datetime.timedelta( hours=4, minutes=54, seconds=28, microseconds=512400 ), ), # Make sure we truncate, not round # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is # https://bitbucket.org/nielsenb/aniso8601/issues/21/sub-microsecond-precision-is-lost-when ( {"PnY": "1999.9999999999999999"}, datetime.timedelta(days=729999, seconds=86399, microseconds=999999), ), ( {"PnM": "1.9999999999999999"}, datetime.timedelta( days=59, hours=23, minutes=59, seconds=59, microseconds=999999 ), ), ( {"PnW": "1.9999999999999999"}, datetime.timedelta( days=13, hours=23, minutes=59, seconds=59, microseconds=999999 ), ), ( {"PnD": "1.9999999999999999"}, datetime.timedelta( days=1, hours=23, minutes=59, seconds=59, microseconds=999999 ), ), ( {"TnH": "14.9999999999999999"}, datetime.timedelta( hours=14, minutes=59, seconds=59, microseconds=999999 ), ), ({"TnM": "0.00000000999"}, datetime.timedelta(0)), ({"TnM": "0.0000000999"}, datetime.timedelta(microseconds=5)), ({"TnS": "0.0000001"}, datetime.timedelta(0)), ({"TnS": "2.0000048"}, datetime.timedelta(seconds=2, microseconds=4)), ({"PnY": "1"}, datetime.timedelta(days=365)), ({"PnY": "1.5"}, datetime.timedelta(days=547.5)), ({"PnM": "1"}, datetime.timedelta(days=30)), ({"PnM": "1.5"}, datetime.timedelta(days=45)), ({"PnW": "1"}, datetime.timedelta(days=7)), ({"PnW": "1.5"}, datetime.timedelta(days=10.5)), ({"PnD": "1"}, datetime.timedelta(days=1)), ({"PnD": "1.5"}, datetime.timedelta(days=1.5)), ( { "PnY": "0003", "PnM": "06", "PnD": "04", "TnH": "12", "TnM": "30", "TnS": "05", }, datetime.timedelta(days=1279, hours=12, minutes=30, seconds=5), ), ( { "PnY": "0003", "PnM": "06", "PnD": "04", "TnH": "12", "TnM": "30", "TnS": "05.5", }, datetime.timedelta(days=1279, hours=12, minutes=30, seconds=5.5), ), # Test timedelta limit ( {"PnD": "999999999", "TnH": "23", "TnM": "59", "TnS": "59.999999"}, datetime.timedelta.max, ), # Make sure we truncate, not round # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is ( { "PnY": "0001", "PnM": "02", "PnD": "03", "TnH": "14", "TnM": "43", "TnS": "59.9999997", }, datetime.timedelta( days=428, hours=14, minutes=43, seconds=59, microseconds=999999 ), ), # Verify overflows ({"TnH": "36"}, datetime.timedelta(days=1, hours=12)), ) for testtuple in testtuples: result = PythonTimeBuilder.build_duration(**testtuple[0]) self.assertEqual(result, testtuple[1]) def test_build_interval(self): testtuples = ( ( { "end": DatetimeTuple( DateTuple("1981", "04", "05", None, None, None), TimeTuple("01", "01", "00", None), ), "duration": DurationTuple(None, "1", None, None, None, None, None), }, datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1), datetime.datetime(year=1981, month=3, day=6, hour=1, minute=1), ), ( { "end": DateTuple("1981", "04", "05", None, None, None), "duration": DurationTuple(None, "1", None, None, None, None, None), }, datetime.date(year=1981, month=4, day=5), datetime.date(year=1981, month=3, day=6), ), ( { "end": DateTuple("2018", "03", "06", None, None, None), "duration": DurationTuple( "1.5", None, None, None, None, None, None ), }, datetime.date(year=2018, month=3, day=6), datetime.datetime(year=2016, month=9, day=4, hour=12), ), ( { "end": DateTuple("2014", "11", "12", None, None, None), "duration": DurationTuple(None, None, None, None, "1", None, None), }, datetime.date(year=2014, month=11, day=12), datetime.datetime(year=2014, month=11, day=11, hour=23), ), ( { "end": DateTuple("2014", "11", "12", None, None, None), "duration": DurationTuple(None, None, None, None, "4", "54", "6.5"), }, datetime.date(year=2014, month=11, day=12), datetime.datetime( year=2014, month=11, day=11, hour=19, minute=5, second=53, microsecond=500000, ), ), ( { "end": DatetimeTuple( DateTuple("2050", "03", "01", None, None, None), TimeTuple( "13", "00", "00", TimezoneTuple(False, True, None, None, "Z"), ), ), "duration": DurationTuple(None, None, None, None, "10", None, None), }, datetime.datetime( year=2050, month=3, day=1, hour=13, tzinfo=UTCOffset(name="UTC", minutes=0), ), datetime.datetime( year=2050, month=3, day=1, hour=3, tzinfo=UTCOffset(name="UTC", minutes=0), ), ), # Make sure we truncate, not round # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is # https://bitbucket.org/nielsenb/aniso8601/issues/21/sub-microsecond-precision-is-lost-when ( { "end": DateTuple("2000", "01", "01", None, None, None), "duration": DurationTuple( "1999.9999999999999999", None, None, None, None, None, None ), }, datetime.date(year=2000, month=1, day=1), datetime.datetime( year=1, month=4, day=30, hour=0, minute=0, second=0, microsecond=1 ), ), ( { "end": DateTuple("1989", "03", "01", None, None, None), "duration": DurationTuple( None, "1.9999999999999999", None, None, None, None, None ), }, datetime.date(year=1989, month=3, day=1), datetime.datetime( year=1988, month=12, day=31, hour=0, minute=0, second=0, microsecond=1, ), ), ( { "end": DateTuple("1989", "03", "01", None, None, None), "duration": DurationTuple( None, None, "1.9999999999999999", None, None, None, None ), }, datetime.date(year=1989, month=3, day=1), datetime.datetime( year=1989, month=2, day=15, hour=0, minute=0, second=0, microsecond=1, ), ), ( { "end": DateTuple("1989", "03", "01", None, None, None), "duration": DurationTuple( None, None, None, "1.9999999999999999", None, None, None ), }, datetime.date(year=1989, month=3, day=1), datetime.datetime( year=1989, month=2, day=27, hour=0, minute=0, second=0, microsecond=1, ), ), ( { "end": DateTuple("2001", "01", "01", None, None, None), "duration": DurationTuple( None, None, None, None, "14.9999999999999999", None, None ), }, datetime.date(year=2001, month=1, day=1), datetime.datetime( year=2000, month=12, day=31, hour=9, minute=0, second=0, microsecond=1, ), ), ( { "end": DateTuple("2001", "01", "01", None, None, None), "duration": DurationTuple( None, None, None, None, None, "0.00000000999", None ), }, datetime.date(year=2001, month=1, day=1), datetime.datetime(year=2001, month=1, day=1), ), ( { "end": DateTuple("2001", "01", "01", None, None, None), "duration": DurationTuple( None, None, None, None, None, "0.0000000999", None ), }, datetime.date(year=2001, month=1, day=1), datetime.datetime( year=2000, month=12, day=31, hour=23, minute=59, second=59, microsecond=999995, ), ), ( { "end": DateTuple("2018", "03", "06", None, None, None), "duration": DurationTuple( None, None, None, None, None, None, "0.0000001" ), }, datetime.date(year=2018, month=3, day=6), datetime.datetime(year=2018, month=3, day=6), ), ( { "end": DateTuple("2018", "03", "06", None, None, None), "duration": DurationTuple( None, None, None, None, None, None, "2.0000048" ), }, datetime.date(year=2018, month=3, day=6), datetime.datetime( year=2018, month=3, day=5, hour=23, minute=59, second=57, microsecond=999996, ), ), ( { "start": DatetimeTuple( DateTuple("1981", "04", "05", None, None, None), TimeTuple("01", "01", "00", None), ), "duration": DurationTuple(None, "1", None, "1", None, "1", None), }, datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1), datetime.datetime(year=1981, month=5, day=6, hour=1, minute=2), ), ( { "start": DateTuple("1981", "04", "05", None, None, None), "duration": DurationTuple(None, "1", None, "1", None, None, None), }, datetime.date(year=1981, month=4, day=5), datetime.date(year=1981, month=5, day=6), ), ( { "start": DateTuple("2018", "03", "06", None, None, None), "duration": DurationTuple( None, "2.5", None, None, None, None, None ), }, datetime.date(year=2018, month=3, day=6), datetime.date(year=2018, month=5, day=20), ), ( { "start": DateTuple("2014", "11", "12", None, None, None), "duration": DurationTuple(None, None, None, None, "1", None, None), }, datetime.date(year=2014, month=11, day=12), datetime.datetime(year=2014, month=11, day=12, hour=1, minute=0), ), ( { "start": DateTuple("2014", "11", "12", None, None, None), "duration": DurationTuple(None, None, None, None, "4", "54", "6.5"), }, datetime.date(year=2014, month=11, day=12), datetime.datetime( year=2014, month=11, day=12, hour=4, minute=54, second=6, microsecond=500000, ), ), ( { "start": DatetimeTuple( DateTuple("2050", "03", "01", None, None, None), TimeTuple( "13", "00", "00", TimezoneTuple(False, True, None, None, "Z"), ), ), "duration": DurationTuple(None, None, None, None, "10", None, None), }, datetime.datetime( year=2050, month=3, day=1, hour=13, tzinfo=UTCOffset(name="UTC", minutes=0), ), datetime.datetime( year=2050, month=3, day=1, hour=23, tzinfo=UTCOffset(name="UTC", minutes=0), ), ), # Make sure we truncate, not round # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is ( { "start": DateTuple("0001", "01", "01", None, None, None), "duration": DurationTuple( "1999.9999999999999999", None, None, None, None, None, None ), }, datetime.date(year=1, month=1, day=1), datetime.datetime( year=1999, month=9, day=3, hour=23, minute=59, second=59, microsecond=999999, ), ), ( { "start": DateTuple("1989", "03", "01", None, None, None), "duration": DurationTuple( None, "1.9999999999999999", None, None, None, None, None ), }, datetime.date(year=1989, month=3, day=1), datetime.datetime( year=1989, month=4, day=29, hour=23, minute=59, second=59, microsecond=999999, ), ), ( { "start": DateTuple("1989", "03", "01", None, None, None), "duration": DurationTuple( None, None, "1.9999999999999999", None, None, None, None ), }, datetime.date(year=1989, month=3, day=1), datetime.datetime( year=1989, month=3, day=14, hour=23, minute=59, second=59, microsecond=999999, ), ), ( { "start": DateTuple("1989", "03", "01", None, None, None), "duration": DurationTuple( None, None, None, "1.9999999999999999", None, None, None ), }, datetime.date(year=1989, month=3, day=1), datetime.datetime( year=1989, month=3, day=2, hour=23, minute=59, second=59, microsecond=999999, ), ), ( { "start": DateTuple("2001", "01", "01", None, None, None), "duration": DurationTuple( None, None, None, None, "14.9999999999999999", None, None ), }, datetime.date(year=2001, month=1, day=1), datetime.datetime( year=2001, month=1, day=1, hour=14, minute=59, second=59, microsecond=999999, ), ), ( { "start": DateTuple("2001", "01", "01", None, None, None), "duration": DurationTuple( None, None, None, None, None, "0.00000000999", None ), }, datetime.date(year=2001, month=1, day=1), datetime.datetime(year=2001, month=1, day=1), ), ( { "start": DateTuple("2001", "01", "01", None, None, None), "duration": DurationTuple( None, None, None, None, None, "0.0000000999", None ), }, datetime.date(year=2001, month=1, day=1), datetime.datetime( year=2001, month=1, day=1, hour=0, minute=0, second=0, microsecond=5 ), ), ( { "start": DateTuple("2018", "03", "06", None, None, None), "duration": DurationTuple( None, None, None, None, None, None, "0.0000001" ), }, datetime.date(year=2018, month=3, day=6), datetime.datetime(year=2018, month=3, day=6), ), ( { "start": DateTuple("2018", "03", "06", None, None, None), "duration": DurationTuple( None, None, None, None, None, None, "2.0000048" ), }, datetime.date(year=2018, month=3, day=6), datetime.datetime( year=2018, month=3, day=6, hour=0, minute=0, second=2, microsecond=4 ), ), ( { "start": DatetimeTuple( DateTuple("1980", "03", "05", None, None, None), TimeTuple("01", "01", "00", None), ), "end": DatetimeTuple( DateTuple("1981", "04", "05", None, None, None), TimeTuple("01", "01", "00", None), ), }, datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1), datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1), ), ( { "start": DatetimeTuple( DateTuple("1980", "03", "05", None, None, None), TimeTuple("01", "01", "00", None), ), "end": DateTuple("1981", "04", "05", None, None, None), }, datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1), datetime.date(year=1981, month=4, day=5), ), ( { "start": DateTuple("1980", "03", "05", None, None, None), "end": DatetimeTuple( DateTuple("1981", "04", "05", None, None, None), TimeTuple("01", "01", "00", None), ), }, datetime.date(year=1980, month=3, day=5), datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1), ), ( { "start": DateTuple("1980", "03", "05", None, None, None), "end": DateTuple("1981", "04", "05", None, None, None), }, datetime.date(year=1980, month=3, day=5), datetime.date(year=1981, month=4, day=5), ), ( { "start": DateTuple("1981", "04", "05", None, None, None), "end": DateTuple("1980", "03", "05", None, None, None), }, datetime.date(year=1981, month=4, day=5), datetime.date(year=1980, month=3, day=5), ), ( { "start": DatetimeTuple( DateTuple("2050", "03", "01", None, None, None), TimeTuple( "13", "00", "00", TimezoneTuple(False, True, None, None, "Z"), ), ), "end": DatetimeTuple( DateTuple("2050", "05", "11", None, None, None), TimeTuple( "15", "30", "00", TimezoneTuple(False, True, None, None, "Z"), ), ), }, datetime.datetime( year=2050, month=3, day=1, hour=13, tzinfo=UTCOffset(name="UTC", minutes=0), ), datetime.datetime( year=2050, month=5, day=11, hour=15, minute=30, tzinfo=UTCOffset(name="UTC", minutes=0), ), ), # Test concise representation ( { "start": DateTuple("2020", "01", "01", None, None, None), "end": DateTuple(None, None, "02", None, None, None), }, datetime.date(year=2020, month=1, day=1), datetime.date(year=2020, month=1, day=2), ), ( { "start": DateTuple("2008", "02", "15", None, None, None), "end": DateTuple(None, "03", "14", None, None, None), }, datetime.date(year=2008, month=2, day=15), datetime.date(year=2008, month=3, day=14), ), ( { "start": DatetimeTuple( DateTuple("2007", "12", "14", None, None, None), TimeTuple("13", "30", None, None), ), "end": TimeTuple("15", "30", None, None), }, datetime.datetime(year=2007, month=12, day=14, hour=13, minute=30), datetime.datetime(year=2007, month=12, day=14, hour=15, minute=30), ), ( { "start": DatetimeTuple( DateTuple("2007", "11", "13", None, None, None), TimeTuple("09", "00", None, None), ), "end": DatetimeTuple( DateTuple(None, None, "15", None, None, None), TimeTuple("17", "00", None, None), ), }, datetime.datetime(year=2007, month=11, day=13, hour=9), datetime.datetime(year=2007, month=11, day=15, hour=17), ), ( { "start": DatetimeTuple( DateTuple("2007", "11", "13", None, None, None), TimeTuple("00", "00", None, None), ), "end": DatetimeTuple( DateTuple(None, None, "16", None, None, None), TimeTuple("00", "00", None, None), ), }, datetime.datetime(year=2007, month=11, day=13), datetime.datetime(year=2007, month=11, day=16), ), ( { "start": DatetimeTuple( DateTuple("2007", "11", "13", None, None, None), TimeTuple( "09", "00", None, TimezoneTuple(False, True, None, None, "Z"), ), ), "end": DatetimeTuple( DateTuple(None, None, "15", None, None, None), TimeTuple("17", "00", None, None), ), }, datetime.datetime( year=2007, month=11, day=13, hour=9, tzinfo=UTCOffset(name="UTC", minutes=0), ), datetime.datetime( year=2007, month=11, day=15, hour=17, tzinfo=UTCOffset(name="UTC", minutes=0), ), ), ( { "start": DatetimeTuple( DateTuple("2007", "11", "13", None, None, None), TimeTuple("09", "00", None, None), ), "end": TimeTuple("12", "34.567", None, None), }, datetime.datetime(year=2007, month=11, day=13, hour=9), datetime.datetime( year=2007, month=11, day=13, hour=12, minute=34, second=34, microsecond=20000, ), ), ( { "start": DateTuple("2007", "11", "13", None, None, None), "end": TimeTuple("12", "34", None, None), }, datetime.date(year=2007, month=11, day=13), datetime.datetime(year=2007, month=11, day=13, hour=12, minute=34), ), # Make sure we truncate, not round # https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is ( { "start": DatetimeTuple( DateTuple("1980", "03", "05", None, None, None), TimeTuple("01", "01", "00.0000001", None), ), "end": DatetimeTuple( DateTuple("1981", "04", "05", None, None, None), TimeTuple("14", "43", "59.9999997", None), ), }, datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1), datetime.datetime( year=1981, month=4, day=5, hour=14, minute=43, second=59, microsecond=999999, ), ), ) for testtuple in testtuples: result = PythonTimeBuilder.build_interval(**testtuple[0]) self.assertEqual(result[0], testtuple[1]) self.assertEqual(result[1], testtuple[2]) def test_build_repeating_interval(self): args = { "Rnn": "3", "interval": IntervalTuple( DateTuple("1981", "04", "05", None, None, None), None, DurationTuple(None, None, None, "1", None, None, None), ), } results = list(PythonTimeBuilder.build_repeating_interval(**args)) self.assertEqual(results[0], datetime.date(year=1981, month=4, day=5)) self.assertEqual(results[1], datetime.date(year=1981, month=4, day=6)) self.assertEqual(results[2], datetime.date(year=1981, month=4, day=7)) args = { "Rnn": "11", "interval": IntervalTuple( None, DatetimeTuple( DateTuple("1980", "03", "05", None, None, None), TimeTuple("01", "01", "00", None), ), DurationTuple(None, None, None, None, "1", "2", None), ), } results = list(PythonTimeBuilder.build_repeating_interval(**args)) for dateindex in compat.range(0, 11): self.assertEqual( results[dateindex], datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1) - dateindex * datetime.timedelta(hours=1, minutes=2), ) args = { "Rnn": "2", "interval": IntervalTuple( DatetimeTuple( DateTuple("1980", "03", "05", None, None, None), TimeTuple("01", "01", "00", None), ), DatetimeTuple( DateTuple("1981", "04", "05", None, None, None), TimeTuple("01", "01", "00", None), ), None, ), } results = list(PythonTimeBuilder.build_repeating_interval(**args)) self.assertEqual( results[0], datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1) ) self.assertEqual( results[1], datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1) ) args = { "Rnn": "2", "interval": IntervalTuple( DatetimeTuple( DateTuple("1980", "03", "05", None, None, None), TimeTuple("01", "01", "00", None), ), DatetimeTuple( DateTuple("1981", "04", "05", None, None, None), TimeTuple("01", "01", "00", None), ), None, ), } results = list(PythonTimeBuilder.build_repeating_interval(**args)) self.assertEqual( results[0], datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1) ) self.assertEqual( results[1], datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1) ) args = { "R": True, "interval": IntervalTuple( None, DatetimeTuple( DateTuple("1980", "03", "05", None, None, None), TimeTuple("01", "01", "00", None), ), DurationTuple(None, None, None, None, "1", "2", None), ), } resultgenerator = PythonTimeBuilder.build_repeating_interval(**args) # Test the first 11 generated for dateindex in compat.range(0, 11): self.assertEqual( next(resultgenerator), datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1) - dateindex * datetime.timedelta(hours=1, minutes=2), ) args = { "R": True, "interval": IntervalTuple( DateTuple("1981", "04", "05", None, None, None), None, DurationTuple(None, None, None, "1", None, None, None), ), } resultgenerator = PythonTimeBuilder.build_repeating_interval(**args) # Test the first 11 generated for dateindex in compat.range(0, 11): self.assertEqual( next(resultgenerator), ( datetime.datetime(year=1981, month=4, day=5, hour=0, minute=0) + dateindex * datetime.timedelta(days=1) ).date(), ) def test_build_timezone(self): testtuples = ( ({"Z": True, "name": "Z"}, datetime.timedelta(hours=0), "UTC"), ( {"negative": False, "hh": "00", "mm": "00", "name": "+00:00"}, datetime.timedelta(hours=0), "+00:00", ), ( {"negative": False, "hh": "01", "mm": "00", "name": "+01:00"}, datetime.timedelta(hours=1), "+01:00", ), ( {"negative": True, "hh": "01", "mm": "00", "name": "-01:00"}, -datetime.timedelta(hours=1), "-01:00", ), ( {"negative": False, "hh": "00", "mm": "12", "name": "+00:12"}, datetime.timedelta(minutes=12), "+00:12", ), ( {"negative": False, "hh": "01", "mm": "23", "name": "+01:23"}, datetime.timedelta(hours=1, minutes=23), "+01:23", ), ( {"negative": True, "hh": "01", "mm": "23", "name": "-01:23"}, -datetime.timedelta(hours=1, minutes=23), "-01:23", ), ( {"negative": False, "hh": "00", "name": "+00"}, datetime.timedelta(hours=0), "+00", ), ( {"negative": False, "hh": "01", "name": "+01"}, datetime.timedelta(hours=1), "+01", ), ( {"negative": True, "hh": "01", "name": "-01"}, -datetime.timedelta(hours=1), "-01", ), ( {"negative": False, "hh": "12", "name": "+12"}, datetime.timedelta(hours=12), "+12", ), ( {"negative": True, "hh": "12", "name": "-12"}, -datetime.timedelta(hours=12), "-12", ), ) for testtuple in testtuples: result = PythonTimeBuilder.build_timezone(**testtuple[0]) self.assertEqual(result.utcoffset(None), testtuple[1]) self.assertEqual(result.tzname(None), testtuple[2]) def test_range_check_date(self): # 0 isn't a valid year for a Python builder with self.assertRaises(YearOutOfBoundsError): PythonTimeBuilder.build_date(YYYY="0000") # Leap year # https://bitbucket.org/nielsenb/aniso8601/issues/14/parsing-ordinal-dates-should-only-allow with self.assertRaises(DayOutOfBoundsError): PythonTimeBuilder.build_date(YYYY="1981", DDD="366") def test_range_check_time(self): # Hour 24 can only represent midnight with self.assertRaises(MidnightBoundsError): PythonTimeBuilder.build_time(hh="24", mm="00", ss="01") with self.assertRaises(MidnightBoundsError): PythonTimeBuilder.build_time(hh="24", mm="00.1") with self.assertRaises(MidnightBoundsError): PythonTimeBuilder.build_time(hh="24", mm="01") with self.assertRaises(MidnightBoundsError): PythonTimeBuilder.build_time(hh="24.1") def test_range_check_duration(self): with self.assertRaises(YearOutOfBoundsError): PythonTimeBuilder.build_duration( PnY=str((datetime.timedelta.max.days // 365) + 1) ) with self.assertRaises(MonthOutOfBoundsError): PythonTimeBuilder.build_duration( PnM=str((datetime.timedelta.max.days // 30) + 1) ) with self.assertRaises(DayOutOfBoundsError): PythonTimeBuilder.build_duration(PnD=str(datetime.timedelta.max.days + 1)) with self.assertRaises(WeekOutOfBoundsError): PythonTimeBuilder.build_duration( PnW=str((datetime.timedelta.max.days // 7) + 1) ) with self.assertRaises(HoursOutOfBoundsError): PythonTimeBuilder.build_duration( TnH=str((datetime.timedelta.max.days * 24) + 24) ) with self.assertRaises(MinutesOutOfBoundsError): PythonTimeBuilder.build_duration( TnM=str((datetime.timedelta.max.days * 24 * 60) + 24 * 60) ) with self.assertRaises(SecondsOutOfBoundsError): PythonTimeBuilder.build_duration( TnS=str((datetime.timedelta.max.days * 24 * 60 * 60) + 24 * 60 * 60) ) # Split max range across all parts maxpart = datetime.timedelta.max.days // 7 with self.assertRaises(DayOutOfBoundsError): PythonTimeBuilder.build_duration( PnY=str((maxpart // 365) + 1), PnM=str((maxpart // 30) + 1), PnD=str((maxpart + 1)), PnW=str((maxpart // 7) + 1), TnH=str((maxpart * 24) + 1), TnM=str((maxpart * 24 * 60) + 1), TnS=str((maxpart * 24 * 60 * 60) + 1), ) def test_range_check_interval(self): with self.assertRaises(YearOutOfBoundsError): PythonTimeBuilder.build_interval( start=DateTuple("0007", None, None, None, None, None), duration=DurationTuple( None, None, None, str(datetime.timedelta.max.days), None, None, None ), ) with self.assertRaises(YearOutOfBoundsError): PythonTimeBuilder.build_interval( start=DatetimeTuple( DateTuple("0007", None, None, None, None, None), TimeTuple("1", None, None, None), ), duration=DurationTuple( str(datetime.timedelta.max.days // 365), None, None, None, None, None, None, ), ) with self.assertRaises(YearOutOfBoundsError): PythonTimeBuilder.build_interval( end=DateTuple("0001", None, None, None, None, None), duration=DurationTuple("3", None, None, None, None, None, None), ) with self.assertRaises(YearOutOfBoundsError): PythonTimeBuilder.build_interval( end=DatetimeTuple( DateTuple("0001", None, None, None, None, None), TimeTuple("1", None, None, None), ), duration=DurationTuple("2", None, None, None, None, None, None), ) def test_build_week_date(self): weekdate = PythonTimeBuilder._build_week_date(2009, 1) self.assertEqual(weekdate, datetime.date(year=2008, month=12, day=29)) weekdate = PythonTimeBuilder._build_week_date(2009, 53, isoday=7) self.assertEqual(weekdate, datetime.date(year=2010, month=1, day=3)) def test_build_ordinal_date(self): ordinaldate = PythonTimeBuilder._build_ordinal_date(1981, 95) self.assertEqual(ordinaldate, datetime.date(year=1981, month=4, day=5)) def test_iso_year_start(self): yearstart = PythonTimeBuilder._iso_year_start(2004) self.assertEqual(yearstart, datetime.date(year=2003, month=12, day=29)) yearstart = PythonTimeBuilder._iso_year_start(2010) self.assertEqual(yearstart, datetime.date(year=2010, month=1, day=4)) yearstart = PythonTimeBuilder._iso_year_start(2009) self.assertEqual(yearstart, datetime.date(year=2008, month=12, day=29)) def test_date_generator(self): startdate = datetime.date(year=2018, month=8, day=29) timedelta = datetime.timedelta(days=1) iterations = 10 generator = PythonTimeBuilder._date_generator(startdate, timedelta, iterations) results = list(generator) for dateindex in compat.range(0, 10): self.assertEqual( results[dateindex], datetime.date(year=2018, month=8, day=29) + dateindex * datetime.timedelta(days=1), ) def test_date_generator_unbounded(self): startdate = datetime.date(year=2018, month=8, day=29) timedelta = datetime.timedelta(days=5) generator = PythonTimeBuilder._date_generator_unbounded(startdate, timedelta) # Check the first 10 results for dateindex in compat.range(0, 10): self.assertEqual( next(generator), datetime.date(year=2018, month=8, day=29) + dateindex * datetime.timedelta(days=5), ) def test_distribute_microseconds(self): self.assertEqual(PythonTimeBuilder._distribute_microseconds(1, (), ()), (1,)) self.assertEqual( PythonTimeBuilder._distribute_microseconds(11, (0,), (10,)), (1, 1) ) self.assertEqual( PythonTimeBuilder._distribute_microseconds(211, (0, 0), (100, 10)), (2, 1, 1), ) self.assertEqual(PythonTimeBuilder._distribute_microseconds(1, (), ()), (1,)) self.assertEqual( PythonTimeBuilder._distribute_microseconds(11, (5,), (10,)), (6, 1) ) self.assertEqual( PythonTimeBuilder._distribute_microseconds(211, (10, 5), (100, 10)), (12, 6, 1), ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1613696531.0 aniso8601-9.0.1/aniso8601/compat.py0000664000175000017500000000107300000000000016622 0ustar00nielsenbnielsenb# -*- coding: utf-8 -*- # Copyright (c) 2021, Brandon Nielsen # All rights reserved. # # This software may be modified and distributed under the terms # of the BSD license. See the LICENSE file for details. import sys PY2 = sys.version_info[0] == 2 if PY2: # pragma: no cover range = xrange # pylint: disable=undefined-variable else: range = range def is_string(tocheck): # pylint: disable=undefined-variable if PY2: # pragma: no cover return isinstance(tocheck, str) or isinstance(tocheck, unicode) return isinstance(tocheck, str) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1613696531.0 aniso8601-9.0.1/aniso8601/date.py0000664000175000017500000001057300000000000016261 0ustar00nielsenbnielsenb# -*- coding: utf-8 -*- # Copyright (c) 2021, Brandon Nielsen # All rights reserved. # # This software may be modified and distributed under the terms # of the BSD license. See the LICENSE file for details. from aniso8601.builders import TupleBuilder from aniso8601.builders.python import PythonTimeBuilder from aniso8601.compat import is_string from aniso8601.exceptions import ISOFormatError from aniso8601.resolution import DateResolution def get_date_resolution(isodatestr): # Valid string formats are: # # Y[YYY] # YYYY-MM-DD # YYYYMMDD # YYYY-MM # YYYY-Www # YYYYWww # YYYY-Www-D # YYYYWwwD # YYYY-DDD # YYYYDDD isodatetuple = parse_date(isodatestr, builder=TupleBuilder) if isodatetuple.DDD is not None: # YYYY-DDD # YYYYDDD return DateResolution.Ordinal if isodatetuple.D is not None: # YYYY-Www-D # YYYYWwwD return DateResolution.Weekday if isodatetuple.Www is not None: # YYYY-Www # YYYYWww return DateResolution.Week if isodatetuple.DD is not None: # YYYY-MM-DD # YYYYMMDD return DateResolution.Day if isodatetuple.MM is not None: # YYYY-MM return DateResolution.Month # Y[YYY] return DateResolution.Year def parse_date(isodatestr, builder=PythonTimeBuilder): # Given a string in any ISO 8601 date format, return a datetime.date # object that corresponds to the given date. Valid string formats are: # # Y[YYY] # YYYY-MM-DD # YYYYMMDD # YYYY-MM # YYYY-Www # YYYYWww # YYYY-Www-D # YYYYWwwD # YYYY-DDD # YYYYDDD if is_string(isodatestr) is False: raise ValueError("Date must be string.") if isodatestr.startswith("+") or isodatestr.startswith("-"): raise NotImplementedError( "ISO 8601 extended year representation " "not supported." ) if len(isodatestr) == 0 or isodatestr.count("-") > 2: raise ISOFormatError('"{0}" is not a valid ISO 8601 date.'.format(isodatestr)) yearstr = None monthstr = None daystr = None weekstr = None weekdaystr = None ordinaldaystr = None if len(isodatestr) <= 4: # Y[YYY] yearstr = isodatestr elif "W" in isodatestr: if len(isodatestr) == 10: # YYYY-Www-D yearstr = isodatestr[0:4] weekstr = isodatestr[6:8] weekdaystr = isodatestr[9] elif len(isodatestr) == 8: if "-" in isodatestr: # YYYY-Www yearstr = isodatestr[0:4] weekstr = isodatestr[6:] else: # YYYYWwwD yearstr = isodatestr[0:4] weekstr = isodatestr[5:7] weekdaystr = isodatestr[7] elif len(isodatestr) == 7: # YYYYWww yearstr = isodatestr[0:4] weekstr = isodatestr[5:] elif len(isodatestr) == 7: if "-" in isodatestr: # YYYY-MM yearstr = isodatestr[0:4] monthstr = isodatestr[5:] else: # YYYYDDD yearstr = isodatestr[0:4] ordinaldaystr = isodatestr[4:] elif len(isodatestr) == 8: if "-" in isodatestr: # YYYY-DDD yearstr = isodatestr[0:4] ordinaldaystr = isodatestr[5:] else: # YYYYMMDD yearstr = isodatestr[0:4] monthstr = isodatestr[4:6] daystr = isodatestr[6:] elif len(isodatestr) == 10: # YYYY-MM-DD yearstr = isodatestr[0:4] monthstr = isodatestr[5:7] daystr = isodatestr[8:] else: raise ISOFormatError('"{0}" is not a valid ISO 8601 date.'.format(isodatestr)) hascomponent = False for componentstr in [yearstr, monthstr, daystr, weekstr, weekdaystr, ordinaldaystr]: if componentstr is not None: hascomponent = True if componentstr.isdigit() is False: raise ISOFormatError( '"{0}" is not a valid ISO 8601 date.'.format(isodatestr) ) if hascomponent is False: raise ISOFormatError('"{0}" is not a valid ISO 8601 date.'.format(isodatestr)) return builder.build_date( YYYY=yearstr, MM=monthstr, DD=daystr, Www=weekstr, D=weekdaystr, DDD=ordinaldaystr, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1613696531.0 aniso8601-9.0.1/aniso8601/decimalfraction.py0000664000175000017500000000051500000000000020463 0ustar00nielsenbnielsenb# -*- coding: utf-8 -*- # Copyright (c) 2021, Brandon Nielsen # All rights reserved. # # This software may be modified and distributed under the terms # of the BSD license. See the LICENSE file for details. def normalize(value): """Returns the string with decimal separators normalized.""" return value.replace(",", ".") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1614645234.0 aniso8601-9.0.1/aniso8601/duration.py0000664000175000017500000002255700000000000017176 0ustar00nielsenbnielsenb# -*- coding: utf-8 -*- # Copyright (c) 2021, Brandon Nielsen # All rights reserved. # # This software may be modified and distributed under the terms # of the BSD license. See the LICENSE file for details. from aniso8601 import compat from aniso8601.builders import TupleBuilder from aniso8601.builders.python import PythonTimeBuilder from aniso8601.date import parse_date from aniso8601.decimalfraction import normalize from aniso8601.exceptions import ISOFormatError from aniso8601.resolution import DurationResolution from aniso8601.time import parse_time def get_duration_resolution(isodurationstr): # Valid string formats are: # # PnYnMnDTnHnMnS (or any reduced precision equivalent) # PnW # PT