Skip to content

Add ISO 8601 Duration support#1251

Open
veeceey wants to merge 3 commits intoarrow-py:masterfrom
veeceey:feat/issue-757-iso8601-duration
Open

Add ISO 8601 Duration support#1251
veeceey wants to merge 3 commits intoarrow-py:masterfrom
veeceey:feat/issue-757-iso8601-duration

Conversation

@veeceey
Copy link
Contributor

@veeceey veeceey commented Feb 23, 2026

Adds a Duration class that handles ISO 8601 duration strings (the P1Y2M3DT4H5M6S format). Came up in #757.

Supports parsing, formatting, and doing math with Arrow objects:

from arrow import Duration

# Parse
d = Duration.parse("P1Y2M3DT4H5M6S")

# Format
str(d)  # "P1Y2M3DT4H5M6S"

# Use with Arrow
import arrow
arw = arrow.get("2020-01-01")
arw + Duration(years=1, months=2)  # 2021-03-01

# Convert
d.to_timedelta()
d.to_relativedelta()
Duration.from_timedelta(timedelta(days=5, hours=3))

Handles weeks (P2W), fractional values (PT1.5S), and the full date+time combination. Also supports duration arithmetic (add/subtract/negate) and equality/hashing.

39 new tests covering parsing, formatting, roundtrips, arithmetic, conversions, edge cases, and error handling.

Closes #757

Implements parsing and formatting of ISO 8601 duration strings
(P1Y2M3DT4H5M6S format), with arithmetic support for Arrow objects.

The Duration class supports:
- Parsing: Duration.parse("P1Y2M3DT4H5M6S")
- Formatting: str(duration) / duration.isoformat()
- Arrow integration: arrow.get("2020-01-01") + Duration(years=1)
- Conversion to/from timedelta and relativedelta
- Duration arithmetic (add, subtract, negate)
- Fractional values (PT1.5S)
- Weeks (P2W)

Closes arrow-py#757
@veeceey
Copy link
Contributor Author

veeceey commented Feb 23, 2026

Test Results

All 39 new tests pass, and all 1940 existing tests continue to pass:

$ python3 -m pytest tests/ -x -o "addopts=" --timeout=60
======================= 1940 passed, 1 skipped in 2.96s ========================

$ python3 -m pytest tests/test_duration.py -xvs -o "addopts="
tests/test_duration.py::TestDurationParse::test_full_duration PASSED
tests/test_duration.py::TestDurationParse::test_date_only PASSED
tests/test_duration.py::TestDurationParse::test_time_only PASSED
tests/test_duration.py::TestDurationParse::test_weeks PASSED
tests/test_duration.py::TestDurationParse::test_days_only PASSED
tests/test_duration.py::TestDurationParse::test_hours_only PASSED
tests/test_duration.py::TestDurationParse::test_minutes_only PASSED
tests/test_duration.py::TestDurationParse::test_seconds_only PASSED
tests/test_duration.py::TestDurationParse::test_fractional_seconds PASSED
tests/test_duration.py::TestDurationParse::test_fractional_hours PASSED
tests/test_duration.py::TestDurationParse::test_years_months PASSED
tests/test_duration.py::TestDurationParse::test_invalid_empty PASSED
tests/test_duration.py::TestDurationParse::test_invalid_no_p PASSED
tests/test_duration.py::TestDurationParse::test_invalid_only_p PASSED
tests/test_duration.py::TestDurationParse::test_invalid_only_pt PASSED
tests/test_duration.py::TestDurationParse::test_invalid_garbage PASSED
tests/test_duration.py::TestDurationFormat::test_full_format PASSED
tests/test_duration.py::TestDurationFormat::test_date_only_format PASSED
tests/test_duration.py::TestDurationFormat::test_time_only_format PASSED
tests/test_duration.py::TestDurationFormat::test_weeks_format PASSED
tests/test_duration.py::TestDurationFormat::test_zero_duration PASSED
tests/test_duration.py::TestDurationFormat::test_roundtrip PASSED
tests/test_duration.py::TestDurationFormat::test_isoformat_method PASSED
tests/test_duration.py::TestDurationArithmetic::test_add_durations PASSED
tests/test_duration.py::TestDurationArithmetic::test_sub_durations PASSED
tests/test_duration.py::TestDurationArithmetic::test_negate PASSED
tests/test_duration.py::TestDurationArithmetic::test_add_to_arrow PASSED
tests/test_duration.py::TestDurationArithmetic::test_add_days_to_arrow PASSED
tests/test_duration.py::TestDurationArithmetic::test_add_time_to_arrow PASSED
tests/test_duration.py::TestDurationEquality::test_equal PASSED
tests/test_duration.py::TestDurationEquality::test_not_equal PASSED
tests/test_duration.py::TestDurationEquality::test_hash PASSED
tests/test_duration.py::TestDurationConversions::test_to_timedelta PASSED
tests/test_duration.py::TestDurationConversions::test_to_relativedelta PASSED
tests/test_duration.py::TestDurationConversions::test_from_timedelta PASSED
tests/test_duration.py::TestDurationRepr::test_repr_full PASSED
tests/test_duration.py::TestDurationRepr::test_repr_empty PASSED
tests/test_duration.py::TestDurationRepr::test_repr_partial PASSED
tests/test_duration.py::TestDurationImport::test_importable_from_arrow PASSED
============================== 39 passed in 0.18s ==============================

Manual test:

>>> from arrow import Duration
>>> d = Duration.parse("P1Y2M3DT4H5M6S")
>>> d
Duration(years=1, months=2, days=3, hours=4, minutes=5, seconds=6)
>>> str(d)
'P1Y2M3DT4H5M6S'
>>> import arrow
>>> arrow.get("2020-01-01") + Duration(years=1, months=2)
<Arrow [2021-03-01T00:00:00+00:00]>

@codecov
Copy link

codecov bot commented Feb 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (b423717) to head (44379cf).

Additional details and impacted files
@@            Coverage Diff             @@
##            master     #1251    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           10        11     +1     
  Lines         2315      2420   +105     
  Branches       358       379    +21     
==========================================
+ Hits          2315      2420   +105     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

- Fix import ordering in test_duration.py (stdlib before third-party)
- Apply black formatting to both duration.py and test_duration.py
- Fix mypy type error in from_timedelta by using typed local variable
@veeceey
Copy link
Contributor Author

veeceey commented Mar 12, 2026

any chance this could get reviewed? happy to make changes

Cover NotImplemented returns, microsecond handling in
from_timedelta, years/months in to_timedelta, and
fractional number formatting.
@veeceey
Copy link
Contributor Author

veeceey commented Mar 18, 2026

Pushed additional tests to cover the missing lines flagged by codecov - NotImplemented returns, microsecond handling, years/months in to_timedelta, and fractional formatting. Should bring patch coverage up to 100%.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate support for ISO 8601 Durations/Time Intervals

1 participant