Exemple #1
0
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"
Exemple #2
0
 def _assert_duration_equal(a: Duration, b: Duration) -> None:
     assert a.total_seconds() == b.total_seconds(), f"{a!r} != {b!r}"