Skip to content

CalendarDiffTime loses ISO 8601 / XSD duration structure on round-trip #292

@mgajda

Description

@mgajda

Summary

Data.Time.Format.ISO8601 parses ISO 8601 durations into
CalendarDiffTime, which stores only (months :: Integer, time :: NominalDiffTime). The structured Y / M / W / D / H / M / S grammar of
the input is collapsed at parse time, so iso8601Show . iso8601ParseM
is not the identity on perfectly valid ISO 8601 strings.

This is strictly required by XML Schema xs:duration (which
mandates lexical-space round-trip), but it is desirable in general.

Why this matters beyond XSD

  1. Round-trip is the standard expectation for Show/Read. Today
    read . show on a CalendarDiffTime is fine, but iso8601ParseM
    on user/wire input followed by iso8601Show silently rewrites the
    string — surprising for any data-interchange use.

  2. Canonical forms matter outside XSD too. JSON Schema
    (format: "duration", RFC 3339 Appendix A), OData, ICS/RFC 5545
    (DURATION), and most REST APIs that quote ISO 8601 expect the
    producer's chosen form to be preserved. An SLA expressed as PT1H
    in a contract should not become PT3600S after a parse/serialize.

  3. Weeks are silently dropped. P1W parses, but serializes back
    as P7D. Weeks are a first-class designator in ISO 8601:2004 sec.
    4.4.3.2 and are mutually exclusive with the other designators.

  4. No way to express "valid ISO 8601" at the type level. ISO 8601
    permits at most one leading sign for the whole duration.
    CalendarDiffTime allows independently-signed ctMonths and
    ctTime, so values exist that cannot be losslessly rendered as
    ISO 8601 / XSD.

Concrete failures

> iso8601Show <$> (iso8601ParseM "P1Y"  :: Maybe CalendarDiffTime)
Just "P12M"
> iso8601Show <$> (iso8601ParseM "P1W"  :: Maybe CalendarDiffTime)
Just "P7D"
> iso8601Show <$> (iso8601ParseM "PT1H" :: Maybe CalendarDiffTime)
Just "PT3600S"
> iso8601Show <$> (iso8601ParseM "PT60M":: Maybe CalendarDiffTime)
Just "PT3600S"

All four inputs are valid; none round-trip.

Proposal

Add a structure-preserving duration type alongside CalendarDiffTime,
e.g.

data Duration = Duration
  { durSign    :: Sign           -- one leading sign, per spec
  , durYears   :: Integer
  , durMonths  :: Integer
  , durWeeks   :: Maybe Integer  -- Just _ excludes Y/M/D/T per 4.4.3.2
  , durDays    :: Integer
  , durHours   :: Integer
  , durMinutes :: Integer
  , durSeconds :: Pico
  } deriving (Eq, Show, Generic, Data)

with:

  • iso8601Format :: Format Duration that round-trips bit-exact for
    every valid ISO 8601 lexical form;
  • total conversions
    toCalendarDiffTime :: Duration -> CalendarDiffTime
    and a partial
    fromCalendarDiffTime :: CalendarDiffTime -> Maybe Duration
    (partial because CalendarDiffTime admits values outside ISO 8601's
    grammar, e.g. mixed-sign);
  • arithmetic helpers (addDuration, diffDuration) implemented in
    terms of existing addUTCDurationClip / diffUTCDurationClip.

The existing CalendarDiffTime instance stays as-is for backward
compatibility; Duration is the new recommended type for parsing
external ISO 8601 / XSD input.

Prior art / context

  • First noticed in 2014: Add parser for ISO8601 durations #40.
  • W3C XML Schema Part 2 §3.2.6 (duration) — requires lexical-space
    preservation.
  • RFC 3339 Appendix A — ISO 8601 duration grammar.
  • The iso8601-duration package on Hackage exists precisely because
    time does not provide this; expanding time to subsume it is great.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions