aniso8601-8.0.0/0000775000175000017500000000000013536314113014571 5ustar nielsenbnielsenb00000000000000aniso8601-8.0.0/LICENSE0000664000175000017500000000273513415212725015607 0ustar nielsenbnielsenb00000000000000Copyright (c) 2019, 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. aniso8601-8.0.0/MANIFEST.in0000664000175000017500000000004313415212725016326 0ustar nielsenbnielsenb00000000000000include LICENSE include README.rst aniso8601-8.0.0/PKG-INFO0000664000175000017500000005060513536314113015674 0ustar nielsenbnielsenb00000000000000Metadata-Version: 1.1 Name: aniso8601 Version: 8.0.0 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 * Python 3 support * 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 formats * 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 ----------------- 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 "aniso8601/time.py", line 131, in parse_datetime return builder.build_datetime(datepart, timepart) File "aniso8601/builder.py", line 300, in build_datetime cls._build_object(time)) File "aniso8601/builder.py", line 71, in _build_object ss=parsetuple[2], tz=parsetuple[3]) File "aniso8601/builder.py", line 253, in build_time raise LeapSecondError('Leap seconds are not supported.') aniso8601.exceptions.LeapSecondError: Leap seconds are not supported. Parsing dates ------------- 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) Parsing times ------------- 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 "aniso8601/time.py", line 116, in parse_time return _RESOLUTION_MAP[get_time_resolution(timestr)](timestr, tz, builder) File "aniso8601/time.py", line 165, in _parse_second_time return builder.build_time(hh=hourstr, mm=minutestr, ss=secondstr, tz=tz) File "aniso8601/builder.py", line 253, in build_time raise LeapSecondError('Leap seconds are not supported.') aniso8601.exceptions.LeapSecondError: Leap seconds are not supported. 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:5') datetime.timedelta(397, 5405) 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)) 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 "aniso8601/builders/python.py", line 463, in _date_generator_unbounded currentdate += timedelta OverflowError: date value out of range Date and time resolution ------------------------ In some situations, it may be useful to figure out the resolution provided by an ISO 8601 date or time string. Two functions are provided for this purpose. To get the resolution of a 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 Similarly, for 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 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 tuple of strings and is located in the :code:`aniso8601.builders` module. 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 tuples of strings. It is located in the :code:`aniso8601.builders` module. Datetimes ^^^^^^^^^ Parsing a datetime returns a tuple containing a date tuple as a collection of strings, a time tuple as a collection of strings, and the 'datetime' string. The date tuple contains the following parse components: :code:`(YYYY, MM, DD, Www, D, DDD, 'date')`. The time tuple contains the following parse components :code:`(hh, mm, ss, tz, 'time')`, where :code:`tz` is a tuple with the following components :code:`(negative, Z, hh, mm, name, 'timezone')` 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) (('1977', '06', '10', None, None, None, 'date'), ('12', '00', '00', None, 'time'), 'datetime') >>> aniso8601.parse_datetime('1979-06-05T08:00:00-08:00', builder=TupleBuilder) (('1979', '06', '05', None, None, None, 'date'), ('08', '00', '00', (True, None, '08', '00', '-08:00', 'timezone'), 'time'), 'datetime') Dates ^^^^^ Parsing a date returns a tuple containing the following parse components: :code:`(YYYY, MM, DD, Www, D, DDD, 'date')`:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_date('1984-04-23', builder=TupleBuilder) ('1984', '04', '23', None, None, None, 'date') >>> aniso8601.parse_date('1986-W38-1', builder=TupleBuilder) ('1986', None, None, '38', '1', None, 'date') >>> aniso8601.parse_date('1988-132', builder=TupleBuilder) ('1988', None, None, None, None, '132', 'date') Times ^^^^^ Parsing a time returns a tuple containing following parse components: :code:`(hh, mm, ss, tz, 'time')`, where :code:`tz` is a tuple with the following components :code:`(negative, Z, hh, mm, name, 'timezone')` with :code:`negative` and :code:`Z` being booleans:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_time('11:31:14', builder=TupleBuilder) ('11', '31', '14', None, 'time') >>> aniso8601.parse_time('171819Z', builder=TupleBuilder) ('17', '18', '19', (False, True, None, None, 'Z', 'timezone'), 'time') >>> aniso8601.parse_time('17:18:19-02:30', builder=TupleBuilder) ('17', '18', '19', (True, None, '02', '30', '-02:30', 'timezone'), 'time') Durations ^^^^^^^^^ Parsing a duration returns a tuple containing the following parse components: :code:`(PnY, PnM, PnW, PnD, TnH, TnM, TnS, 'duration')`:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_duration('P1Y2M3DT4H54M6S', builder=TupleBuilder) ('1', '2', None, '3', '4', '54', '6', 'duration') >>> aniso8601.parse_duration('P7W', builder=TupleBuilder) (None, None, '7', None, None, None, None, 'duration') Intervals ^^^^^^^^^ Parsing an interval returns a tuple containing the following parse components: :code:`(start, end, duration, 'interval')`, :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) ((('2007', '03', '01', None, None, None, 'date'), ('13', '00', '00', None, 'time'), 'datetime'), (('2008', '05', '11', None, None, None, 'date'), ('15', '30', '00', None, 'time'), 'datetime'), None, 'interval') >>> aniso8601.parse_interval('2007-03-01T13:00:00Z/P1Y2M10DT2H30M', builder=TupleBuilder) ((('2007', '03', '01', None, None, None, 'date'), ('13', '00', '00', (False, True, None, None, 'Z', 'timezone'), 'time'), 'datetime'), None, ('1', '2', None, '10', '2', '30', None, 'duration'), 'interval') >>> aniso8601.parse_interval('P1M/1981-04-05', builder=TupleBuilder) (None, ('1981', '04', '05', None, None, None, 'date'), (None, '1', None, None, None, None, None, 'duration'), 'interval') A repeating interval returns a tuple containing the following parse components: :code:`(R, Rnn, interval, 'repeatinginterval')` 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) (False, '3', (('1981', '04', '05', None, None, None, 'date'), None, (None, None, None, '1', None, None, None, 'duration'), 'interval'), 'repeatinginterval') >>> aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00', builder=TupleBuilder) (True, None, (None, (('1980', '03', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime'), (None, None, None, None, '1', '2', None, 'duration'), 'interval'), 'repeatinginterval') Development =========== Setup ----- It is recommended to develop using a `virtualenv `_. Tests ----- Tests can be run using `setuptools `:: $ python setup.py test 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: Topic :: Software Development :: Libraries :: Python Modules aniso8601-8.0.0/README.rst0000664000175000017500000004025013536313376016273 0ustar nielsenbnielsenb00000000000000aniso8601 ========= Another ISO 8601 parser for Python ---------------------------------- Features ======== * Pure Python implementation * Python 3 support * 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 formats * 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 ----------------- 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 "aniso8601/time.py", line 131, in parse_datetime return builder.build_datetime(datepart, timepart) File "aniso8601/builder.py", line 300, in build_datetime cls._build_object(time)) File "aniso8601/builder.py", line 71, in _build_object ss=parsetuple[2], tz=parsetuple[3]) File "aniso8601/builder.py", line 253, in build_time raise LeapSecondError('Leap seconds are not supported.') aniso8601.exceptions.LeapSecondError: Leap seconds are not supported. Parsing dates ------------- 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) Parsing times ------------- 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 "aniso8601/time.py", line 116, in parse_time return _RESOLUTION_MAP[get_time_resolution(timestr)](timestr, tz, builder) File "aniso8601/time.py", line 165, in _parse_second_time return builder.build_time(hh=hourstr, mm=minutestr, ss=secondstr, tz=tz) File "aniso8601/builder.py", line 253, in build_time raise LeapSecondError('Leap seconds are not supported.') aniso8601.exceptions.LeapSecondError: Leap seconds are not supported. 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:5') datetime.timedelta(397, 5405) 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)) 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 "aniso8601/builders/python.py", line 463, in _date_generator_unbounded currentdate += timedelta OverflowError: date value out of range Date and time resolution ------------------------ In some situations, it may be useful to figure out the resolution provided by an ISO 8601 date or time string. Two functions are provided for this purpose. To get the resolution of a 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 Similarly, for 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 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 tuple of strings and is located in the :code:`aniso8601.builders` module. 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 tuples of strings. It is located in the :code:`aniso8601.builders` module. Datetimes ^^^^^^^^^ Parsing a datetime returns a tuple containing a date tuple as a collection of strings, a time tuple as a collection of strings, and the 'datetime' string. The date tuple contains the following parse components: :code:`(YYYY, MM, DD, Www, D, DDD, 'date')`. The time tuple contains the following parse components :code:`(hh, mm, ss, tz, 'time')`, where :code:`tz` is a tuple with the following components :code:`(negative, Z, hh, mm, name, 'timezone')` 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) (('1977', '06', '10', None, None, None, 'date'), ('12', '00', '00', None, 'time'), 'datetime') >>> aniso8601.parse_datetime('1979-06-05T08:00:00-08:00', builder=TupleBuilder) (('1979', '06', '05', None, None, None, 'date'), ('08', '00', '00', (True, None, '08', '00', '-08:00', 'timezone'), 'time'), 'datetime') Dates ^^^^^ Parsing a date returns a tuple containing the following parse components: :code:`(YYYY, MM, DD, Www, D, DDD, 'date')`:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_date('1984-04-23', builder=TupleBuilder) ('1984', '04', '23', None, None, None, 'date') >>> aniso8601.parse_date('1986-W38-1', builder=TupleBuilder) ('1986', None, None, '38', '1', None, 'date') >>> aniso8601.parse_date('1988-132', builder=TupleBuilder) ('1988', None, None, None, None, '132', 'date') Times ^^^^^ Parsing a time returns a tuple containing following parse components: :code:`(hh, mm, ss, tz, 'time')`, where :code:`tz` is a tuple with the following components :code:`(negative, Z, hh, mm, name, 'timezone')` with :code:`negative` and :code:`Z` being booleans:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_time('11:31:14', builder=TupleBuilder) ('11', '31', '14', None, 'time') >>> aniso8601.parse_time('171819Z', builder=TupleBuilder) ('17', '18', '19', (False, True, None, None, 'Z', 'timezone'), 'time') >>> aniso8601.parse_time('17:18:19-02:30', builder=TupleBuilder) ('17', '18', '19', (True, None, '02', '30', '-02:30', 'timezone'), 'time') Durations ^^^^^^^^^ Parsing a duration returns a tuple containing the following parse components: :code:`(PnY, PnM, PnW, PnD, TnH, TnM, TnS, 'duration')`:: >>> import aniso8601 >>> from aniso8601.builders import TupleBuilder >>> aniso8601.parse_duration('P1Y2M3DT4H54M6S', builder=TupleBuilder) ('1', '2', None, '3', '4', '54', '6', 'duration') >>> aniso8601.parse_duration('P7W', builder=TupleBuilder) (None, None, '7', None, None, None, None, 'duration') Intervals ^^^^^^^^^ Parsing an interval returns a tuple containing the following parse components: :code:`(start, end, duration, 'interval')`, :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) ((('2007', '03', '01', None, None, None, 'date'), ('13', '00', '00', None, 'time'), 'datetime'), (('2008', '05', '11', None, None, None, 'date'), ('15', '30', '00', None, 'time'), 'datetime'), None, 'interval') >>> aniso8601.parse_interval('2007-03-01T13:00:00Z/P1Y2M10DT2H30M', builder=TupleBuilder) ((('2007', '03', '01', None, None, None, 'date'), ('13', '00', '00', (False, True, None, None, 'Z', 'timezone'), 'time'), 'datetime'), None, ('1', '2', None, '10', '2', '30', None, 'duration'), 'interval') >>> aniso8601.parse_interval('P1M/1981-04-05', builder=TupleBuilder) (None, ('1981', '04', '05', None, None, None, 'date'), (None, '1', None, None, None, None, None, 'duration'), 'interval') A repeating interval returns a tuple containing the following parse components: :code:`(R, Rnn, interval, 'repeatinginterval')` 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) (False, '3', (('1981', '04', '05', None, None, None, 'date'), None, (None, None, None, '1', None, None, None, 'duration'), 'interval'), 'repeatinginterval') >>> aniso8601.parse_repeating_interval('R/PT1H2M/1980-03-05T01:01:00', builder=TupleBuilder) (True, None, (None, (('1980', '03', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime'), (None, None, None, None, '1', '2', None, 'duration'), 'interval'), 'repeatinginterval') Development =========== Setup ----- It is recommended to develop using a `virtualenv `_. Tests ----- Tests can be run using `setuptools `:: $ python setup.py test 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 `_ aniso8601-8.0.0/aniso8601/0000775000175000017500000000000013536314113016221 5ustar nielsenbnielsenb00000000000000aniso8601-8.0.0/aniso8601/__init__.py0000664000175000017500000000101713415212725020333 0ustar nielsenbnielsenb00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2019, 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 the main parsing functions so they are readily available from aniso8601.time import parse_datetime, parse_time, get_time_resolution from aniso8601.date import parse_date, get_date_resolution from aniso8601.duration import parse_duration from aniso8601.interval import parse_interval, parse_repeating_interval aniso8601-8.0.0/aniso8601/builders/0000775000175000017500000000000013536314113020032 5ustar nielsenbnielsenb00000000000000aniso8601-8.0.0/aniso8601/builders/__init__.py0000664000175000017500000001040313436316001022136 0ustar nielsenbnielsenb00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2019, 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.exceptions import ISOFormatError class BaseTimeBuilder(object): @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 @staticmethod def cast(value, castfunction, caughtexceptions=(ValueError,), thrownexception=ISOFormatError, thrownmessage=None): try: result = castfunction(value) except caughtexceptions: raise thrownexception(thrownmessage) return result @classmethod def _build_object(cls, parsetuple): #Given a TupleBuilder tuple, build the correct object if parsetuple[-1] == 'date': return cls.build_date(YYYY=parsetuple[0], MM=parsetuple[1], DD=parsetuple[2], Www=parsetuple[3], D=parsetuple[4], DDD=parsetuple[5]) elif parsetuple[-1] == 'time': return cls.build_time(hh=parsetuple[0], mm=parsetuple[1], ss=parsetuple[2], tz=parsetuple[3]) elif parsetuple[-1] == 'datetime': return cls.build_datetime(parsetuple[0], parsetuple[1]) elif parsetuple[-1] == 'duration': return cls.build_duration(PnY=parsetuple[0], PnM=parsetuple[1], PnW=parsetuple[2], PnD=parsetuple[3], TnH=parsetuple[4], TnM=parsetuple[5], TnS=parsetuple[6]) elif parsetuple[-1] == 'interval': return cls.build_interval(start=parsetuple[0], end=parsetuple[1], duration=parsetuple[2]) elif parsetuple[-1] == 'repeatinginterval': return cls.build_repeating_interval(R=parsetuple[0], Rnn=parsetuple[1], interval=parsetuple[2]) return cls.build_timezone(negative=parsetuple[0], Z=parsetuple[1], hh=parsetuple[2], mm=parsetuple[3], name=parsetuple[4]) 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 (YYYY, MM, DD, Www, D, DDD, 'date') @classmethod def build_time(cls, hh=None, mm=None, ss=None, tz=None): return (hh, mm, ss, tz, 'time') @classmethod def build_datetime(cls, date, time): return (date, time, 'datetime') @classmethod def build_duration(cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None): return (PnY, PnM, PnW, PnD, TnH, TnM, TnS, 'duration') @classmethod def build_interval(cls, start=None, end=None, duration=None): return (start, end, duration, 'interval') @classmethod def build_repeating_interval(cls, R=None, Rnn=None, interval=None): return (R, Rnn, interval, 'repeatinginterval') @classmethod def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=''): return (negative, Z, hh, mm, name, 'timezone') aniso8601-8.0.0/aniso8601/builders/python.py0000664000175000017500000004272213477776432021760 0ustar nielsenbnielsenb00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2019, 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 aniso8601.builders import BaseTimeBuilder, TupleBuilder from aniso8601.exceptions import (DayOutOfBoundsError, HoursOutOfBoundsError, LeapSecondError, MidnightBoundsError, MinutesOutOfBoundsError, SecondsOutOfBoundsError, WeekOutOfBoundsError, YearOutOfBoundsError) from aniso8601.utcoffset import UTCOffset 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 = 30 * MICROSECONDS_PER_DAY MICROSECONDS_PER_YEAR = 365 * MICROSECONDS_PER_DAY class PythonTimeBuilder(BaseTimeBuilder): @classmethod def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None): if YYYY is not None: #Truncated dates, like '19', refer to 1900-1999 inclusive, #we simply parse to 1900 if len(YYYY) < 4: #Shift 0s in from the left to form complete year YYYY = YYYY.ljust(4, '0') year = cls.cast(YYYY, int, thrownmessage='Invalid year string.') if MM is not None: month = cls.cast(MM, int, thrownmessage='Invalid month string.') else: month = 1 if DD is not None: day = cls.cast(DD, int, thrownmessage='Invalid day string.') else: day = 1 if Www is not None: weeknumber = cls.cast(Www, int, thrownmessage='Invalid week string.') if weeknumber == 0 or weeknumber > 53: raise WeekOutOfBoundsError('Week number must be between ' '1..53.') else: weeknumber = None if DDD is not None: dayofyear = cls.cast(DDD, int, thrownmessage='Invalid day string.') else: dayofyear = None if D is not None: dayofweek = cls.cast(D, int, thrownmessage='Invalid day string.') if dayofweek == 0 or dayofweek > 7: raise DayOutOfBoundsError('Weekday number must be between ' '1..7.') else: dayofweek = None #0000 (1 BC) is not representable as a Python date so a ValueError is #raised if year == 0: raise YearOutOfBoundsError('Year must be between 1..9999.') if dayofyear is not None: return PythonTimeBuilder._build_ordinal_date(year, dayofyear) if weeknumber is not None: return PythonTimeBuilder._build_week_date(year, weeknumber, isoday=dayofweek) return datetime.date(year, month, day) @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 if hh is not None: if '.' in hh: hours, remainingmicroseconds = cls._split_to_microseconds(hh, MICROSECONDS_PER_HOUR, 'Invalid hour string.') microseconds += remainingmicroseconds else: hours = cls.cast(hh, int, thrownmessage='Invalid hour string.') if mm is not None: if '.' in mm: minutes, remainingmicroseconds = cls._split_to_microseconds(mm, MICROSECONDS_PER_MINUTE, 'Invalid minute string.') microseconds += remainingmicroseconds else: minutes = cls.cast(mm, int, thrownmessage='Invalid minute string.') if ss is not None: if '.' in ss: seconds, remainingmicroseconds = cls._split_to_microseconds(ss, MICROSECONDS_PER_SECOND, 'Invalid second string.') microseconds += remainingmicroseconds else: seconds = cls.cast(ss, int, thrownmessage='Invalid second string.') hours, minutes, seconds, microseconds = PythonTimeBuilder._distribute_microseconds(microseconds, (hours, minutes, seconds), (MICROSECONDS_PER_HOUR, MICROSECONDS_PER_MINUTE, MICROSECONDS_PER_SECOND)) #Range checks if hours == 23 and minutes == 59 and seconds == 60: #https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is raise LeapSecondError('Leap seconds are not supported.') if (hours == 24 and (minutes != 0 or seconds != 0)): raise MidnightBoundsError('Hour 24 may only represent midnight.') if hours > 24: raise HoursOutOfBoundsError('Hour must be between 0..24 with ' '24 representing midnight.') if minutes >= 60: raise MinutesOutOfBoundsError('Minutes must be less than 60.') if seconds >= 60: raise SecondsOutOfBoundsError('Seconds must be less than 60.') #Fix ranges that have passed range checks if hours == 24: hours = 0 minutes = 0 seconds = 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): years = 0 months = 0 days = 0 weeks = 0 hours = 0 minutes = 0 seconds = 0 microseconds = 0 if PnY is not None: if '.' in PnY: years, remainingmicroseconds = cls._split_to_microseconds(PnY, MICROSECONDS_PER_YEAR, 'Invalid year string.') microseconds += remainingmicroseconds else: years = cls.cast(PnY, int, thrownmessage='Invalid year string.') if PnM is not None: if '.' in PnM: months, remainingmicroseconds = cls._split_to_microseconds(PnM, MICROSECONDS_PER_MONTH, 'Invalid month string.') microseconds += remainingmicroseconds else: months = cls.cast(PnM, int, thrownmessage='Invalid month string.') if PnW is not None: if '.' in PnW: weeks, remainingmicroseconds = cls._split_to_microseconds(PnW, MICROSECONDS_PER_WEEK, 'Invalid week string.') microseconds += remainingmicroseconds else: weeks = cls.cast(PnW, int, thrownmessage='Invalid week string.') if PnD is not None: if '.' in PnD: days, remainingmicroseconds = cls._split_to_microseconds(PnD, MICROSECONDS_PER_DAY, 'Invalid day string.') microseconds += remainingmicroseconds else: days = cls.cast(PnD, int, thrownmessage='Invalid day string.') if TnH is not None: if '.' in TnH: hours, remainingmicroseconds = cls._split_to_microseconds(TnH, MICROSECONDS_PER_HOUR, 'Invalid hour string.') microseconds += remainingmicroseconds else: hours = cls.cast(TnH, int, thrownmessage='Invalid hour string.') if TnM is not None: if '.' in TnM: minutes, remainingmicroseconds = cls._split_to_microseconds(TnM, MICROSECONDS_PER_MINUTE, 'Invalid minute string.') microseconds += remainingmicroseconds else: minutes = cls.cast(TnM, int, thrownmessage='Invalid minute string.') if TnS is not None: if '.' in TnS: seconds, remainingmicroseconds = cls._split_to_microseconds(TnS, MICROSECONDS_PER_SECOND, 'Invalid second string.') microseconds += remainingmicroseconds else: seconds = cls.cast(TnS, int, thrownmessage='Invalid second string.') 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 * 365 + months * 30 + days return datetime.timedelta(days=totaldays, seconds=seconds, microseconds=microseconds, minutes=minutes, hours=hours, weeks=weeks) @classmethod def build_interval(cls, start=None, end=None, duration=None): 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[4] is not None or duration[5] is not None or duration[6] is not None or durationobject.seconds != 0 or durationobject.microseconds != 0) if end is not None: #/ endobject = cls._build_object(end) if end[-1] == 'date' 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) if start[-1] == 'date' 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 if interval[0] is not None: startobject = cls._build_object(interval[0]) if interval[1] is not None: endobject = cls._build_object(interval[1]) if interval[2] is not None: durationobject = cls._build_object(interval[2]) 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 = cls.cast(Rnn, int, thrownmessage='Invalid iterations.') 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=''): if Z is True: #Z -> UTC return UTCOffset(name='UTC', minutes=0) if hh is not None: tzhour = cls.cast(hh, int, thrownmessage='Invalid hour string.') else: tzhour = 0 if mm is not None: tzminute = cls.cast(mm, int, thrownmessage='Invalid minute string.') else: tzminute = 0 if negative is True: return UTCOffset(name=name, minutes=-(tzhour * 60 + tzminute)) return UTCOffset(name=name, minutes=tzhour * 60 + tzminute) @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)) #Enforce ordinal day limitation #https://bitbucket.org/nielsenb/aniso8601/issues/14/parsing-ordinal-dates-should-only-allow if isoday == 0 or builtdate.year != isoyear: raise DayOutOfBoundsError('Day of year must be from 1..365, ' '1..366 for leap year.') 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 @classmethod def _split_to_microseconds(cls, floatstr, conversion, thrownmessage): #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 = cls.cast(intpart, int, thrownmessage=thrownmessage) preconvertedvalue = cls.cast(floatpart, int, thrownmessage=thrownmessage) convertedvalue = ((preconvertedvalue * conversion) // (10 ** len(floatpart))) return (intvalue, convertedvalue) @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) aniso8601-8.0.0/aniso8601/builders/tests/0000775000175000017500000000000013536314113021174 5ustar nielsenbnielsenb00000000000000aniso8601-8.0.0/aniso8601/builders/tests/__init__.py0000664000175000017500000000032113436316001023276 0ustar nielsenbnielsenb00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2019, 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. aniso8601-8.0.0/aniso8601/builders/tests/test_init.py0000664000175000017500000004654313436316001023561 0ustar nielsenbnielsenb00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2019, 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, TupleBuilder from aniso8601.exceptions import ISOFormatError from aniso8601.tests.compat import mock 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_cast(self): self.assertEqual(BaseTimeBuilder.cast('1', int), 1) self.assertEqual(BaseTimeBuilder.cast('-2', int), -2) self.assertEqual(BaseTimeBuilder.cast('3', float), float(3)) self.assertEqual(BaseTimeBuilder.cast('-4', float), float(-4)) self.assertEqual(BaseTimeBuilder.cast('5.6', float), 5.6) self.assertEqual(BaseTimeBuilder.cast('-7.8', float), -7.8) def test_cast_exception(self): with self.assertRaises(ISOFormatError): BaseTimeBuilder.cast('asdf', int) with self.assertRaises(ISOFormatError): BaseTimeBuilder.cast('asdf', float) def test_cast_caughtexception(self): def tester(value): raise RuntimeError with self.assertRaises(ISOFormatError): BaseTimeBuilder.cast('asdf', tester, caughtexceptions=(RuntimeError,)) def test_cast_thrownexception(self): with self.assertRaises(RuntimeError): BaseTimeBuilder.cast('asdf', int, thrownexception=RuntimeError) def test_build_object(self): datetest = (('1', '2', '3', '4', '5', '6', 'date'), {'YYYY': '1', 'MM': '2', 'DD': '3', 'Www': '4', 'D': '5', 'DDD': '6'}) timetest = (('1', '2', '3', (False, False, '4', '5', 'tz name', 'timezone'), 'time'), {'hh': '1', 'mm': '2', 'ss': '3', 'tz': (False, False, '4', '5', 'tz name', 'timezone')}) datetimetest = ((('1', '2', '3', '4', '5', '6', 'date'), ('7', '8', '9', (True, False, '10', '11', 'tz name', 'timezone'), 'time'), 'datetime'), (('1', '2', '3', '4', '5', '6', 'date'), ('7', '8', '9', (True, False, '10', '11', 'tz name', 'timezone'), 'time'))) durationtest = (('1', '2', '3', '4', '5', '6', '7', 'duration'), {'PnY': '1', 'PnM': '2', 'PnW': '3', 'PnD': '4', 'TnH': '5', 'TnM': '6', 'TnS': '7'}) intervaltests = (((('1', '2', '3', '4', '5', '6', 'date'), ('7', '8', '9', '10', '11', '12', 'date'), None, 'interval'), {'start': ('1', '2', '3', '4', '5', '6', 'date'), 'end': ('7', '8', '9', '10', '11', '12', 'date'), 'duration': None}), ((('1', '2', '3', '4', '5', '6', 'date'), None, ('7', '8', '9', '10', '11', '12', '13', 'duration'), 'interval'), {'start': ('1', '2', '3', '4', '5', '6', 'date'), 'end': None, 'duration': ('7', '8', '9', '10', '11', '12', '13', 'duration')}), ((None, ('1', '2', '3', (True, False, '4', '5', 'tz name', 'timezone'), 'time'), ('6', '7', '8', '9', '10', '11', '12', 'duration'), 'interval'), {'start': None, 'end': ('1', '2', '3', (True, False, '4', '5', 'tz name', 'timezone'), 'time'), 'duration': ('6', '7', '8', '9', '10', '11', '12', 'duration')})) repeatingintervaltests = (((True, None, (('1', '2', '3', '4', '5', '6', 'date'), ('7', '8', '9', '10', '11', '12', 'date'), None, 'interval'), 'repeatinginterval'), {'R': True, 'Rnn': None, 'interval': (('1', '2', '3', '4', '5', '6', 'date'), ('7', '8', '9', '10', '11', '12', 'date'), None, 'interval')}), ((False, '1', ((('2', '3', '4', '5', '6', '7', 'date'), ('8', '9', '10', None, 'time'), 'datetime'), (('11', '12', '13', '14', '15', '16', 'date'), ('17', '18', '19', None, 'time'), 'datetime'), None, 'interval'), 'repeatinginterval'), {'R':False, 'Rnn': '1', 'interval': ((('2', '3', '4', '5', '6', '7', 'date'), ('8', '9', '10', None, 'time'), 'datetime'), (('11', '12', '13', '14', '15', '16', 'date'), ('17', '18', '19', None, 'time'), 'datetime'), None, 'interval')})) timezonetest = ((False, False, '1', '2', '+01:02', 'timezone'), {'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]) class TestTupleBuilder(unittest.TestCase): def test_build_date(self): datetuple = TupleBuilder.build_date() self.assertEqual(datetuple, (None, None, None, None, None, None, 'date')) datetuple = TupleBuilder.build_date(YYYY='1', MM='2', DD='3', Www='4', D='5', DDD='6') self.assertEqual(datetuple, ('1', '2', '3', '4', '5', '6', 'date')) def test_build_time(self): testtuples = (({}, (None, None, None, None, 'time')), ({'hh': '1', 'mm': '2', 'ss': '3', 'tz': None}, ('1', '2', '3', None, 'time')), ({'hh': '1', 'mm': '2', 'ss': '3', 'tz': (False, False, '4', '5', 'tz name', 'timezone')}, ('1', '2', '3', (False, False, '4', '5', 'tz name', 'timezone'), 'time'))) for testtuple in testtuples: self.assertEqual(TupleBuilder.build_time(**testtuple[0]), testtuple[1]) def test_build_datetime(self): testtuples = (({'date': ('1', '2', '3', '4', '5', '6', 'date'), 'time': ('7', '8', '9', None, 'time')}, (('1', '2', '3', '4', '5', '6', 'date'), ('7', '8', '9', None, 'time'), 'datetime')), ({'date': ('1', '2', '3', '4', '5', '6', 'date'), 'time': ('7', '8', '9', (True, False, '10', '11', 'tz name', 'timezone'), 'time')}, (('1', '2', '3', '4', '5', '6', 'date'), ('7', '8', '9', (True, False, '10', '11', 'tz name', 'timezone'), 'time'), 'datetime'))) for testtuple in testtuples: self.assertEqual(TupleBuilder.build_datetime(**testtuple[0]), testtuple[1]) def test_build_duration(self): testtuples = (({}, (None, None, None, None, None, None, None, 'duration')), ({'PnY': '1', 'PnM': '2', 'PnW': '3', 'PnD': '4', 'TnH': '5', 'TnM': '6', 'TnS': '7'}, ('1', '2', '3', '4', '5', '6', '7', 'duration'))) for testtuple in testtuples: self.assertEqual(TupleBuilder.build_duration(**testtuple[0]), testtuple[1]) def test_build_interval(self): testtuples = (({}, (None, None, None, 'interval')), ({'start': ('1', '2', '3', '4', '5', '6', 'date'), 'end': ('7', '8', '9', '10', '11', '12', 'date')}, (('1', '2', '3', '4', '5', '6', 'date'), ('7', '8', '9', '10', '11', '12', 'date'), None, 'interval')), ({'start': ('1', '2', '3', (True, False, '7', '8', 'tz name', 'timezone'), 'time'), 'end': ('4', '5', '6', (False, False, '9', '10', 'tz name', 'timezone'), 'time')}, (('1', '2', '3', (True, False, '7', '8', 'tz name', 'timezone'), 'time'), ('4', '5', '6', (False, False, '9', '10', 'tz name', 'timezone'), 'time'), None, 'interval')), ({'start': (('1', '2', '3', '4', '5', '6', 'date'), ('7', '8', '9', (True, False, '10', '11', 'tz name', 'timezone'), 'time'), 'datetime'), 'end': (('12', '13', '14', '15', '16', '17', 'date'), ('18', '19', '20', (False, False, '21', '22', 'tz name', 'timezone'), 'time'), 'datetime')}, ((('1', '2', '3', '4', '5', '6', 'date'), ('7', '8', '9', (True, False, '10', '11', 'tz name', 'timezone'), 'time'), 'datetime'), (('12', '13', '14', '15', '16', '17', 'date'), ('18', '19', '20', (False, False, '21', '22', 'tz name', 'timezone'), 'time'), 'datetime'), None, 'interval')), ({'start': ('1', '2', '3', '4', '5', '6', 'date'), 'end': None, 'duration': ('7', '8', '9', '10', '11', '12', '13', 'duration')}, (('1', '2', '3', '4', '5', '6', 'date'), None, ('7', '8', '9', '10', '11', '12', '13', 'duration'), 'interval')), ({'start': None, 'end': ('1', '2', '3', (True, False, '4', '5', 'tz name', 'timezone'), 'time'), 'duration': ('6', '7', '8', '9', '10', '11', '12', 'duration')}, (None, ('1', '2', '3', (True, False, '4', '5', 'tz name', 'timezone'), 'time'), ('6', '7', '8', '9', '10', '11', '12', 'duration'), 'interval'))) for testtuple in testtuples: self.assertEqual(TupleBuilder.build_interval(**testtuple[0]), testtuple[1]) def test_build_repeating_interval(self): testtuples = (({}, (None, None, None, 'repeatinginterval')), ({'R': True, 'interval':(('1', '2', '3', '4', '5', '6', 'date'), ('7', '8', '9', '10', '11', '12', 'date'), None, 'interval')}, (True, None, (('1', '2', '3', '4', '5', '6', 'date'), ('7', '8', '9', '10', '11', '12', 'date'), None, 'interval'), 'repeatinginterval')), ({'R':False, 'Rnn': '1', 'interval': ((('2', '3', '4', '5', '6', '7', 'date'), ('8', '9', '10', None, 'time'), 'datetime'), (('11', '12', '13', '14', '15', '16', 'date'), ('17', '18', '19', None, 'time'), 'datetime'), None, 'interval')}, (False, '1', ((('2', '3', '4', '5', '6', '7', 'date'), ('8', '9', '10', None, 'time'), 'datetime'), (('11', '12', '13', '14', '15', '16', 'date'), ('17', '18', '19', None, 'time'), 'datetime'), None, 'interval'), 'repeatinginterval'))) for testtuple in testtuples: result = TupleBuilder.build_repeating_interval(**testtuple[0]) self.assertEqual(result, testtuple[1]) def test_build_timezone(self): testtuples = (({}, (None, None, None, None, '', 'timezone')), ({'negative': False, 'Z': True, 'name': 'UTC'}, (False, True, None, None, 'UTC', 'timezone')), ({'negative': False, 'Z': False, 'hh': '1', 'mm': '2', 'name': '+01:02'}, (False, False, '1', '2', '+01:02', 'timezone')), ({'negative': True, 'Z': False, 'hh': '1', 'mm': '2', 'name': '-01:02'}, (True, False, '1', '2', '-01:02', 'timezone'))) for testtuple in testtuples: result = TupleBuilder.build_timezone(**testtuple[0]) self.assertEqual(result, testtuple[1]) aniso8601-8.0.0/aniso8601/builders/tests/test_python.py0000664000175000017500000015634213477776432024165 0ustar nielsenbnielsenb00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2019, 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.exceptions import (DayOutOfBoundsError, HoursOutOfBoundsError, ISOFormatError, LeapSecondError, MidnightBoundsError, MinutesOutOfBoundsError, SecondsOutOfBoundsError, WeekOutOfBoundsError, YearOutOfBoundsError) from aniso8601.builders.python import PythonTimeBuilder from aniso8601.utcoffset import UTCOffset 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_date_bounds_checking(self): #0 isn't a valid week number with self.assertRaises(WeekOutOfBoundsError): PythonTimeBuilder.build_date(YYYY='2003', Www='00') #Week must not be larger than 53 with self.assertRaises(WeekOutOfBoundsError): PythonTimeBuilder.build_date(YYYY='2004', Www='54') #0 isn't a valid day number with self.assertRaises(DayOutOfBoundsError): PythonTimeBuilder.build_date(YYYY='2001', Www='02', D='0') #Day must not be larger than 7 with self.assertRaises(DayOutOfBoundsError): PythonTimeBuilder.build_date(YYYY='2001', Www='02', D='8') #0 isn't a valid year for a Python builder with self.assertRaises(YearOutOfBoundsError): PythonTimeBuilder.build_date(YYYY='0000') with self.assertRaises(DayOutOfBoundsError): PythonTimeBuilder.build_date(YYYY='1981', DDD='000') #Day 366 is only valid on a leap year with self.assertRaises(DayOutOfBoundsError): PythonTimeBuilder.build_date(YYYY='1981', DDD='366') #Day must me 365, or 366, not larger with self.assertRaises(DayOutOfBoundsError): PythonTimeBuilder.build_date(YYYY='1981', DDD='367') 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': (False, None, '00', '00', 'UTC', 'timezone')}, datetime.time(tzinfo=UTCOffset(name='UTC', minutes=0))), ({'hh': '23', 'mm': '21', 'ss': '28.512400', 'tz': (False, None, '00', '00', '+00:00', 'timezone')}, datetime.time(hour=23, minute=21, second=28, microsecond=512400, tzinfo=UTCOffset(name='+00:00', minutes=0))), ({'hh': '1', 'mm': '23', 'tz': (False, None, '01', '00', '+1', 'timezone')}, datetime.time(hour=1, minute=23, tzinfo=UTCOffset(name='+1', minutes=60))), ({'hh': '1', 'mm': '23.4567', 'tz': (True, None, '01', '00', '-1', 'timezone')}, datetime.time(hour=1, minute=23, second=27, microsecond=402000, tzinfo=UTCOffset(name='-1', minutes=-60))), ({'hh': '23', 'mm': '21', 'ss': '28.512400', 'tz': (False, None, '01', '30', '+1:30', 'timezone')}, 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': (False, None, '11', '15', '+11:15', 'timezone')}, 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': (False, None, '12', '34', '+12:34', 'timezone')}, 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': (False, None, '00', '00', 'UTC', 'timezone')}, 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_time_bounds_checking(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): PythonTimeBuilder.build_time(hh='23', mm='59', ss='60') with self.assertRaises(LeapSecondError): PythonTimeBuilder.build_time(hh='23', mm='59', ss='60', tz=UTCOffset(name='UTC', minutes=0)) with self.assertRaises(SecondsOutOfBoundsError): PythonTimeBuilder.build_time(hh='00', mm='00', ss='60') with self.assertRaises(SecondsOutOfBoundsError): PythonTimeBuilder.build_time(hh='00', mm='00', ss='60', tz=UTCOffset(name='UTC', minutes=0)) with self.assertRaises(SecondsOutOfBoundsError): PythonTimeBuilder.build_time(hh='00', mm='00', ss='61') with self.assertRaises(SecondsOutOfBoundsError): PythonTimeBuilder.build_time(hh='00', mm='00', ss='61', tz=UTCOffset(name='UTC', minutes=0)) with self.assertRaises(MinutesOutOfBoundsError): PythonTimeBuilder.build_time(hh='00', mm='61') with self.assertRaises(MinutesOutOfBoundsError): PythonTimeBuilder.build_time(hh='00', mm='61', tz=UTCOffset(name='UTC', minutes=0)) with self.assertRaises(MinutesOutOfBoundsError): PythonTimeBuilder.build_time(hh='00', mm='60') with self.assertRaises(MinutesOutOfBoundsError): PythonTimeBuilder.build_time(hh='00', mm='60.1') with self.assertRaises(HoursOutOfBoundsError): PythonTimeBuilder.build_time(hh='25') #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_build_datetime(self): testtuples = (((('2019', '06', '05', None, None, None, 'date'), ('01', '03', '11.858714', None, 'time')), datetime.datetime(2019, 6, 5, hour=1, minute=3, second=11, microsecond=858714)), ((('1234', '02', '03', None, None, None, 'date'), ('23', '21', '28.512400', None, 'time')), datetime.datetime(1234, 2, 3, hour=23, minute=21, second=28, microsecond=512400)), ((('1981', '04', '05', None, None, None, 'date'), ('23', '21', '28.512400', (False, None, '11', '15', '+11:15', 'timezone'), 'time')), 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_datetime_bounds_checking(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): PythonTimeBuilder.build_datetime(('2016', '12', '31', None, None, None, 'date'), ('23', '59', '60', None, 'time')) with self.assertRaises(LeapSecondError): PythonTimeBuilder.build_datetime(('2016', '12', '31', None, None, None, 'date'), ('23', '59', '60', (False, None, '00', '00', '+00:00', 'timezone'), 'time')) with self.assertRaises(SecondsOutOfBoundsError): PythonTimeBuilder.build_datetime(('1981', '04', '05', None, None, None, 'date'), ('00', '00', '60', None, 'time')) with self.assertRaises(SecondsOutOfBoundsError): PythonTimeBuilder.build_datetime(('1981', '04', '05', None, None, None, 'date'), ('00', '00', '60', (False, None, '00', '00', '+00:00', 'timezone'), 'time')) with self.assertRaises(SecondsOutOfBoundsError): PythonTimeBuilder.build_datetime(('1981', '04', '05', None, None, None, 'date'), ('00', '00', '61', None, 'time')) with self.assertRaises(SecondsOutOfBoundsError): PythonTimeBuilder.build_datetime(('1981', '04', '05', None, None, None, 'date'), ('00', '00', '61', (False, None, '00', '00', '+00:00', 'timezone'), 'time')) with self.assertRaises(SecondsOutOfBoundsError): PythonTimeBuilder.build_datetime(('1981', '04', '05', None, None, None, 'date'), ('00', '59', '61', None, 'time')) with self.assertRaises(SecondsOutOfBoundsError): PythonTimeBuilder.build_datetime(('1981', '04', '05', None, None, None, 'date'), ('00', '59', '61', (False, None, '00', '00', '+00:00', 'timezone'), 'time')) with self.assertRaises(MinutesOutOfBoundsError): PythonTimeBuilder.build_datetime(('1981', '04', '05', None, None, None, 'date'), ('00', '61', None, None, 'time')) with self.assertRaises(MinutesOutOfBoundsError): PythonTimeBuilder.build_datetime(('1981', '04', '05', None, None, None, 'date'), ('00', '61', None, (False, None, '00', '00', '+00:00', 'timezone'), 'time')) 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)), #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': (('1981', '04', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime'), 'duration': (None, '1', None, None, None, None, None, 'duration')}, 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': ('1981', '04', '05', None, None, None, 'date'), 'duration': (None, '1', None, None, None, None, None, 'duration')}, datetime.date(year=1981, month=4, day=5), datetime.date(year=1981, month=3, day=6)), ({'end': ('2018', '03', '06', None, None, None, 'date'), 'duration': ('1.5', None, None, None, None, None, None, 'duration')}, datetime.date(year=2018, month=3, day=6), datetime.datetime(year=2016, month=9, day=4, hour=12)), ({'end': ('2014', '11', '12', None, None, None, 'date'), 'duration': (None, None, None, None, '1', None, None, 'duration')}, datetime.date(year=2014, month=11, day=12), datetime.datetime(year=2014, month=11, day=11, hour=23)), ({'end': ('2014', '11', '12', None, None, None, 'date'), 'duration': (None, None, None, None, '4', '54', '6.5', 'duration')}, 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': (('2050', '03', '01', None, None, None, 'date'), ('13', '00', '00', (False, True, None, None, 'Z', 'timezone'), 'time'), 'datetime'), 'duration': (None, None, None, None, '10', None, None, 'duration')}, 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': ('2000', '01', '01', None, None, None, 'date'), 'duration': ('1999.9999999999999999', None, None, None, None, None, None, 'duration')}, 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': ('1989', '03', '01', None, None, None, 'date'), 'duration': (None, '1.9999999999999999', None, None, None, None, None, 'duration')}, 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': ('1989', '03', '01', None, None, None, 'date'), 'duration': (None, None, '1.9999999999999999', None, None, None, None, 'duration')}, 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': ('1989', '03', '01', None, None, None, 'date'), 'duration': (None, None, None, '1.9999999999999999', None, None, None, 'duration')}, 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': ('2001', '01', '01', None, None, None, 'date'), 'duration': (None, None, None, None, '14.9999999999999999', None, None, 'duration')}, 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': ('2001', '01', '01', None, None, None, 'date'), 'duration': (None, None, None, None, None, '0.00000000999', None, 'duration')}, datetime.date(year=2001, month=1, day=1), datetime.datetime(year=2001, month=1, day=1)), ({'end': ('2001', '01', '01', None, None, None, 'date'), 'duration': (None, None, None, None, None, '0.0000000999', None, 'duration')}, 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': ('2018', '03', '06', None, None, None, 'date'), 'duration': (None, None, None, None, None, None, '0.0000001', 'duration')}, datetime.date(year=2018, month=3, day=6), datetime.datetime(year=2018, month=3, day=6)), ({'end': ('2018', '03', '06', None, None, None, 'date'), 'duration': (None, None, None, None, None, None, '2.0000048', 'duration')}, 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': (('1981', '04', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime'), 'duration': (None, '1', None, '1', None, '1', None, 'duration')}, 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': ('1981', '04', '05', None, None, None, 'date'), 'duration': (None, '1', None, '1', None, None, None, 'duration')}, datetime.date(year=1981, month=4, day=5), datetime.date(year=1981, month=5, day=6)), ({'start': ('2018', '03', '06', None, None, None, 'date'), 'duration': (None, '2.5', None, None, None, None, None, 'duration')}, datetime.date(year=2018, month=3, day=6), datetime.date(year=2018, month=5, day=20)), ({'start': ('2014', '11', '12', None, None, None, 'date'), 'duration': (None, None, None, None, '1', None, None, 'duration')}, datetime.date(year=2014, month=11, day=12), datetime.datetime(year=2014, month=11, day=12, hour=1, minute=0)), ({'start': ('2014', '11', '12', None, None, None, 'date'), 'duration': (None, None, None, None, '4', '54', '6.5', 'duration')}, 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': (('2050', '03', '01', None, None, None, 'date'), ('13', '00', '00', (False, True, None, None, 'Z', 'timezone'), 'time'), 'datetime'), 'duration': (None, None, None, None, '10', None, None, 'duration')}, 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': ('0001', '01', '01', None, None, None, 'date'), 'duration': ('1999.9999999999999999', None, None, None, None, None, None, 'duration')}, 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': ('1989', '03', '01', None, None, None, 'date'), 'duration': (None, '1.9999999999999999', None, None, None, None, None, 'duration')}, 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': ('1989', '03', '01', None, None, None, 'date'), 'duration': (None, None, '1.9999999999999999', None, None, None, None, 'duration')}, 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': ('1989', '03', '01', None, None, None, 'date'), 'duration': (None, None, None, '1.9999999999999999', None, None, None, 'duration')}, 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': ('2001', '01', '01', None, None, None, 'date'), 'duration': (None, None, None, None, '14.9999999999999999', None, None, 'duration')}, 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': ('2001', '01', '01', None, None, None, 'date'), 'duration': (None, None, None, None, None, '0.00000000999', None, 'duration')}, datetime.date(year=2001, month=1, day=1), datetime.datetime(year=2001, month=1, day=1)), ({'start': ('2001', '01', '01', None, None, None, 'date'), 'duration': (None, None, None, None, None, '0.0000000999', None, 'duration')}, 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': ('2018', '03', '06', None, None, None, 'date'), 'duration': (None, None, None, None, None, None, '0.0000001', 'duration')}, datetime.date(year=2018, month=3, day=6), datetime.datetime(year=2018, month=3, day=6)), ({'start': ('2018', '03', '06', None, None, None, 'date'), 'duration': (None, None, None, None, None, None, '2.0000048', 'duration')}, 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': (('1980', '03', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime'), 'end': (('1981', '04', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime')}, 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': (('1980', '03', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime'), 'end': ('1981', '04', '05', None, None, None, 'date')}, datetime.datetime(year=1980, month=3, day=5, hour=1, minute=1), datetime.date(year=1981, month=4, day=5)), ({'start': ('1980', '03', '05', None, None, None, 'date'), 'end': (('1981', '04', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime')}, datetime.date(year=1980, month=3, day=5), datetime.datetime(year=1981, month=4, day=5, hour=1, minute=1)), ({'start': ('1980', '03', '05', None, None, None, 'date'), 'end': ('1981', '04', '05', None, None, None, 'date')}, datetime.date(year=1980, month=3, day=5), datetime.date(year=1981, month=4, day=5)), ({'start': ('1981', '04', '05', None, None, None, 'date'), 'end': ('1980', '03', '05', None, None, None, 'date')}, datetime.date(year=1981, month=4, day=5), datetime.date(year=1980, month=3, day=5)), ({'start': (('2050', '03', '01', None, None, None, 'date'), ('13', '00', '00', (False, True, None, None, 'Z', 'timezone'), 'time'), 'datetime'), 'end': (('2050', '05', '11', None, None, None, 'date'), ('15', '30', '00', (False, True, None, None, 'Z', 'timezone'), 'time'), 'datetime')}, 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))), #Make sure we truncate, not round #https://bitbucket.org/nielsenb/aniso8601/issues/10/sub-microsecond-precision-in-durations-is ({'start': (('1980', '03', '05', None, None, None, 'date'), ('01', '01', '00.0000001', None, 'time'), 'datetime'), 'end': (('1981', '04', '05', None, None, None, 'date'), ('14', '43', '59.9999997', None, 'time'), 'datetime')}, 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': (('1981', '04', '05', None, None, None, 'date'), None, (None, None, None, '1', None, None, None, 'duration'), 'interval')} 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': (None, (('1980', '03', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime'), (None, None, None, None, '1', '2', None, 'duration'), 'interval')} 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': ((('1980', '03', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime'), (('1981', '04', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime'), None, 'interval')} 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': ((('1980', '03', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime'), (('1981', '04', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime'), None, 'interval')} 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': (None, (('1980', '03', '05', None, None, None, 'date'), ('01', '01', '00', None, 'time'), 'datetime'), (None, None, None, None, '1', '2', None, 'duration'), 'interval')} 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)) 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_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_build_ordinal_date_bounds_checking(self): with self.assertRaises(DayOutOfBoundsError): PythonTimeBuilder._build_ordinal_date(1234, 0) with self.assertRaises(DayOutOfBoundsError): PythonTimeBuilder._build_ordinal_date(1234, 367) 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_split_to_microseconds(self): result = PythonTimeBuilder._split_to_microseconds('1.1', int(1e6), 'dummy') self.assertEqual(result, (1, 100000)) self.assertIsInstance(result[0], int) self.assertIsInstance(result[1], int) result = PythonTimeBuilder._split_to_microseconds('1.000001', int(1e6), 'dummy') self.assertEqual(result, (1, 1)) self.assertIsInstance(result[0], int) self.assertIsInstance(result[1], int) result = PythonTimeBuilder._split_to_microseconds('1.0000001', int(1e6), 'dummy') self.assertEqual(result, (1, 0)) self.assertIsInstance(result[0], int) self.assertIsInstance(result[1], int) def test_split_to_microseconds_exception(self): with self.assertRaises(ISOFormatError) as e: PythonTimeBuilder._split_to_microseconds('b.1', int(1e6), 'exception text') self.assertEqual(str(e.exception), 'exception text') with self.assertRaises(ISOFormatError) as e: PythonTimeBuilder._split_to_microseconds('1.ad', int(1e6), 'exception text') self.assertEqual(str(e.exception), 'exception text') 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)) aniso8601-8.0.0/aniso8601/compat.py0000664000175000017500000000046113415212725020061 0ustar nielsenbnielsenb00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2019, 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: range = xrange else: range = range aniso8601-8.0.0/aniso8601/date.py0000664000175000017500000001521713436316001017513 0ustar nielsenbnielsenb00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2019, 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.exceptions import ISOFormatError from aniso8601.builders.python import PythonTimeBuilder 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 if isodatestr.startswith('+') or isodatestr.startswith('-'): raise NotImplementedError('ISO 8601 extended year representation ' 'not supported.') if isodatestr[0].isdigit() is False or isodatestr[-1].isdigit() is False: raise ISOFormatError('"{0}" is not a valid ISO 8601 date.' .format(isodatestr)) if isodatestr.find('W') != -1: #Handle ISO 8601 week date format hyphens_present = 1 if isodatestr.find('-') != -1 else 0 week_date_len = 7 + hyphens_present weekday_date_len = 8 + 2 * hyphens_present if len(isodatestr) == week_date_len: #YYYY-Www #YYYYWww return DateResolution.Week elif len(isodatestr) == weekday_date_len: #YYYY-Www-D #YYYYWwwD return DateResolution.Weekday else: raise ISOFormatError('"{0}" is not a valid ISO 8601 week date.' .format(isodatestr)) #If the size of the string of 4 or less, #assume its a truncated year representation if len(isodatestr) <= 4: return DateResolution.Year #An ISO string may be a calendar represntation if: # 1) When split on a hyphen, the sizes of the parts are 4, 2, 2 or 4, 2 # 2) There are no hyphens, and the length is 8 datestrsplit = isodatestr.split('-') #Check case 1 if len(datestrsplit) == 2: if len(datestrsplit[0]) == 4 and len(datestrsplit[1]) == 2: return DateResolution.Month if len(datestrsplit) == 3: if (len(datestrsplit[0]) == 4 and len(datestrsplit[1]) == 2 and len(datestrsplit[2]) == 2): return DateResolution.Day #Check case 2 if len(isodatestr) == 8 and isodatestr.find('-') == -1: return DateResolution.Day #An ISO string may be a ordinal date representation if: # 1) When split on a hyphen, the sizes of the parts are 4, 3 # 2) There are no hyphens, and the length is 7 #Check case 1 if len(datestrsplit) == 2: if len(datestrsplit[0]) == 4 and len(datestrsplit[1]) == 3: return DateResolution.Ordinal #Check case 2 if len(isodatestr) == 7 and isodatestr.find('-') == -1: return DateResolution.Ordinal #None of the date representations match raise ISOFormatError('"{0}" is not an ISO 8601 date, perhaps it ' 'represents a time or datetime.'.format(isodatestr)) 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 # #Note that the ISO 8601 date format of ±YYYYY is expressly not supported return _RESOLUTION_MAP[get_date_resolution(isodatestr)](isodatestr, builder) def _parse_year(yearstr, builder): #yearstr is of the format Y[YYY] return builder.build_date(YYYY=yearstr) def _parse_calendar_day(datestr, builder): #datestr is of the format YYYY-MM-DD or YYYYMMDD if len(datestr) == 10: #YYYY-MM-DD yearstr = datestr[0:4] monthstr = datestr[5:7] daystr = datestr[8:] elif len(datestr) == 8: #YYYYMMDD yearstr = datestr[0:4] monthstr = datestr[4:6] daystr = datestr[6:] else: raise ISOFormatError('"{0}" is not a valid ISO 8601 calendar day.' .format(datestr)) return builder.build_date(YYYY=yearstr, MM=monthstr, DD=daystr) def _parse_calendar_month(datestr, builder): #datestr is of the format YYYY-MM if len(datestr) != 7: raise ISOFormatError('"{0}" is not a valid ISO 8601 calendar month.' .format(datestr)) yearstr = datestr[0:4] monthstr = datestr[5:] return builder.build_date(YYYY=yearstr, MM=monthstr) def _parse_week_day(datestr, builder): #datestr is of the format YYYY-Www-D, YYYYWwwD # #W is the week number prefix, ww is the week number, between 1 and 53 #0 is not a valid week number, which differs from the Python implementation # #D is the weekday number, between 1 and 7, which differs from the Python #implementation which is between 0 and 6 yearstr = datestr[0:4] #Week number will be the two characters after the W windex = datestr.find('W') weekstr = datestr[windex + 1:windex + 3] if datestr.find('-') != -1 and len(datestr) == 10: #YYYY-Www-D daystr = datestr[9:10] elif len(datestr) == 8: #YYYYWwwD daystr = datestr[7:8] else: raise ISOFormatError('"{0}" is not a valid ISO 8601 week date.' .format(datestr)) return builder.build_date(YYYY=yearstr, Www=weekstr, D=daystr) def _parse_week(datestr, builder): #datestr is of the format YYYY-Www, YYYYWww # #W is the week number prefix, ww is the week number, between 1 and 53 #0 is not a valid week number, which differs from the Python implementation yearstr = datestr[0:4] #Week number will be the two characters after the W windex = datestr.find('W') weekstr = datestr[windex + 1:windex + 3] return builder.build_date(YYYY=yearstr, Www=weekstr) def _parse_ordinal_date(datestr, builder): #datestr is of the format YYYY-DDD or YYYYDDD #DDD can be from 1 - 36[5,6], this matches Python's definition yearstr = datestr[0:4] if datestr.find('-') != -1: #YYYY-DDD daystr = datestr[(datestr.find('-') + 1):] else: #YYYYDDD daystr = datestr[4:] return builder.build_date(YYYY=yearstr, DDD=daystr) _RESOLUTION_MAP = { DateResolution.Day: _parse_calendar_day, DateResolution.Ordinal: _parse_ordinal_date, DateResolution.Month: _parse_calendar_month, DateResolution.Week: _parse_week, DateResolution.Weekday: _parse_week_day, DateResolution.Year: _parse_year } aniso8601-8.0.0/aniso8601/decimalfraction.py0000664000175000017500000000072613536313376021736 0ustar nielsenbnielsenb00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2019, 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 find_separator(value): """Returns the decimal separator index if found else -1.""" return normalize(value).find('.') def normalize(value): """Returns the string that the decimal separators are normalized.""" return value.replace(',', '.') aniso8601-8.0.0/aniso8601/duration.py0000664000175000017500000002377713536313376020452 0ustar nielsenbnielsenb00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2019, 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 find_separator, normalize from aniso8601.exceptions import ISOFormatError, NegativeDurationError from aniso8601.time import parse_time def parse_duration(isodurationstr, builder=PythonTimeBuilder): #Given a string representing an ISO 8601 duration, return a #a duration built by the given builder. Valid formats are: # #PnYnMnDTnHnMnS (or any reduced precision equivalent) #PT