def duration_to_iso(d: Duration, permit_years_months: bool = True, minus_sign_at_front: bool = True) -> str: """ Converts a :class:`pendulum.Duration` into an ISO-8601 formatted string. Args: d: the duration permit_years_months: - if ``False``, durations with non-zero year or month components will raise a :exc:`ValueError`; otherwise, the ISO format will always be ``PT<seconds>S``. - if ``True``, year/month components will be accepted, and the ISO format will be ``P<years>Y<months>MT<seconds>S``. minus_sign_at_front: Applies to negative durations, which probably aren't part of the ISO standard. - if ``True``, the format ``-P<positive_duration>`` is used, i.e. with a minus sign at the front and individual components positive. - if ``False``, the format ``PT-<positive_seconds>S`` (etc.) is used, i.e. with a minus sign for each component. This format is not re-parsed successfully by ``isodate`` and will therefore fail :func:`duration_from_iso`. Raises: :exc:`ValueError` for bad input The maximum length of the resulting string (see test code below) is: - 21 if years/months are not permitted; - ill-defined if years/months are permitted, but 29 for much more than is realistic (negative, 1000 years, 11 months, and the maximum length for seconds/microseconds). .. code-block:: python from pendulum import DateTime, Duration from cardinal_pythonlib.datetimefunc import duration_from_iso, duration_to_iso from cardinal_pythonlib.logs import main_only_quicksetup_rootlogger main_only_quicksetup_rootlogger() d1 = duration_from_iso("P5W") d2 = duration_from_iso("P3Y1DT3H1M2S") d3 = duration_from_iso("P7000D") d4 = duration_from_iso("P1Y7000D") d5 = duration_from_iso("PT10053.22S") print(duration_to_iso(d1)) print(duration_to_iso(d2)) print(duration_to_iso(d3)) print(duration_to_iso(d4)) print(duration_to_iso(d5)) assert d1 == duration_from_iso(duration_to_iso(d1)) assert d2 == duration_from_iso(duration_to_iso(d2)) assert d3 == duration_from_iso(duration_to_iso(d3)) assert d4 == duration_from_iso(duration_to_iso(d4)) assert d5 == duration_from_iso(duration_to_iso(d5)) strmin = duration_to_iso(Duration.min) # '-P0Y0MT86399999913600.0S' strmax = duration_to_iso(Duration.max) # 'P0Y0MT86400000000000.0S' duration_from_iso(strmin) # raises ISO8601Error from isodate package (bug?) duration_from_iso(strmax) # raises OverflowError from isodate package print(strmin) # P0Y0MT-86399999913600.0S print(strmax) # P0Y0MT86400000000000.0S d6 = duration_from_iso("P100Y999MT86400000000000.0S") # OverflowError d7 = duration_from_iso("P0Y1MT86400000000000.0S") # OverflowError d8 = duration_from_iso("P0Y1111111111111111MT76400000000000.0S") # accepted! # ... length e.g. 38; see len(duration_to_iso(d8)) # So the maximum string length may be ill-defined if years/months are # permitted (since Python 3 integers are unbounded; try 99 ** 10000). # But otherwise: d9longest = duration_from_iso("-P0Y0MT10000000000000.000009S") d10toolong = duration_from_iso("-P0Y0MT100000000000000.000009S") # fails, too many days assert d9longest == duration_from_iso(duration_to_iso(d9longest)) d11longest_with_us = duration_from_iso("-P0Y0MT1000000000.000009S") # microseconds correct d12toolong_rounds_us = duration_from_iso("-P0Y0MT10000000000.000009S") # error in microseconds d13toolong_drops_us = duration_from_iso("-P0Y0MT10000000000000.000009S") # drops microseconds (within datetime.timedelta) d14toolong_parse_fails = duration_from_iso("-P0Y0MT100000000000000.000009S") # fails, too many days assert d11longest_with_us == duration_from_iso(duration_to_iso(d11longest_with_us)) assert d12toolong_rounds_us == duration_from_iso(duration_to_iso(d12toolong_rounds_us)) assert d13toolong_drops_us == duration_from_iso(duration_to_iso(d13toolong_drops_us)) longest_without_ym = duration_to_iso(d11longest_with_us, permit_years_months=False) print(longest_without_ym) # -PT1000000000.000009S print(len(longest_without_ym)) # 21 d15longest_realistic_with_ym_us = duration_from_iso("-P1000Y11MT1000000000.000009S") # microseconds correct longest_realistic_with_ym = duration_to_iso(d15longest_realistic_with_ym_us) print(longest_realistic_with_ym) # -P1000Y11MT1000000000.000009S print(len(longest_realistic_with_ym)) # 29 # Now, double-check how the Pendulum classes handle year/month # calculations: basedate1 = DateTime(year=2000, month=1, day=1) # 2000-01-01 print(basedate1 + Duration(years=1)) # 2001-01-01; OK print(basedate1 + Duration(months=1)) # 2000-02-01; OK basedate2 = DateTime(year=2004, month=2, day=1) # 2004-02-01; leap year print(basedate2 + Duration(years=1)) # 2005-01-01; OK print(basedate2 + Duration(months=1)) # 2000-03-01; OK print(basedate2 + Duration(months=1, days=1)) # 2000-03-02; OK """ # noqa prefix = "" negative = d < Duration() if negative and minus_sign_at_front: prefix = "-" d = -d if permit_years_months: return prefix + "P{years}Y{months}MT{seconds}S".format( years=d.years, months=d.months, seconds=d.total_seconds(), # float ) else: if d.years != 0: raise ValueError(f"Duration has non-zero years: {d.years!r}") if d.months != 0: raise ValueError(f"Duration has non-zero months: {d.months!r}") return prefix + f"PT{d.total_seconds()}S"
def _assert_duration_equal(a: Duration, b: Duration) -> None: assert a.total_seconds() == b.total_seconds(), f"{a!r} != {b!r}"