packages feed

postgresql-simple-interval (empty) → 0.2025.7.9

raw patch · 7 files changed

+675/−0 lines, 7 filesdep +attoparsecdep +basedep +bytestring

Dependencies added: attoparsec, base, bytestring, hspec, postgresql-simple, postgresql-simple-interval, scientific

Files

+ CHANGELOG.md view
@@ -0,0 +1,4 @@+# Change log++postgresql-simple-interval follows the [Package Versioning Policy](https://pvp.haskell.org). You can+find release notes [on GitHub](https://github.com/MercuryTechnologies/postgresql-simple-interval/releases).
+ LICENSE.txt view
@@ -0,0 +1,14 @@+BSD Zero Clause License++Copyright (c) 2025 Mercury Technologies, Inc.++Permission to use, copy, modify, and/or distribute this software for any+purpose with or without fee is hereby granted.++THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR+PERFORMANCE OF THIS SOFTWARE.
+ README.md view
@@ -0,0 +1,3 @@+# postgresql-simple-interval++A simple interval type for PostgreSQL.
+ postgresql-simple-interval.cabal view
@@ -0,0 +1,72 @@+cabal-version: 3.0+name: postgresql-simple-interval+version: 0.2025.7.9+synopsis: A simple interval type for PostgreSQL.+description:+  This library provides a simple interval type for PostgreSQL. It supports+  round-tripping through Postgres without losing information.++category: Database+license: 0BSD+license-file: LICENSE.txt+maintainer: Mercury Technologies, Inc.+extra-doc-files:+  CHANGELOG.md+  README.md++source-repository head+  location: https://github.com/MercuryTechnologies/postgresql-simple-interval+  type: git++flag pedantic+  default: False+  manual: True++common library+  build-depends:+    attoparsec ^>=0.14.4,+    base ^>={4.19, 4.20, 4.21},+    bytestring ^>=0.12.1,++  default-language: Haskell2010+  ghc-options:+    -Weverything+    -Wno-all-missed-specialisations+    -Wno-implicit-prelude+    -Wno-missing-deriving-strategies+    -Wno-missing-export-lists+    -Wno-missing-kind-signatures+    -Wno-missing-safe-haskell-mode+    -Wno-prepositive-qualified-module+    -Wno-safe+    -Wno-unsafe++  if flag(pedantic)+    ghc-options: -Werror++common executable+  import: library+  build-depends: postgresql-simple-interval+  ghc-options:+    -rtsopts+    -threaded++library+  import: library+  build-depends:+    postgresql-simple ^>=0.7,+    scientific ^>=0.3.8,++  -- cabal-gild: discover source/library+  exposed-modules:+    Database.PostgreSQL.Interval+    Database.PostgreSQL.Interval.Unstable++  hs-source-dirs: source/library++test-suite postgresql-simple-interval-test-suite+  import: executable+  build-depends: hspec ^>=2.11.12+  hs-source-dirs: source/test-suite+  main-is: Main.hs+  type: exitcode-stdio-1.0
+ source/library/Database/PostgreSQL/Interval.hs view
@@ -0,0 +1,16 @@+module Database.PostgreSQL.Interval+  ( Unstable.Interval (..),+    Unstable.zero,+    Unstable.fromMicroseconds,+    Unstable.fromMilliseconds,+    Unstable.fromSeconds,+    Unstable.fromMinutes,+    Unstable.fromHours,+    Unstable.fromDays,+    Unstable.fromWeeks,+    Unstable.fromMonths,+    Unstable.fromYears,+  )+where++import qualified Database.PostgreSQL.Interval.Unstable as Unstable
+ source/library/Database/PostgreSQL/Interval/Unstable.hs view
@@ -0,0 +1,344 @@+{-# LANGUAGE LexicalNegation #-}+{-# LANGUAGE NumDecimals #-}+{-# LANGUAGE OverloadedStrings #-}++module Database.PostgreSQL.Interval.Unstable where++import qualified Control.Applicative as Applicative+import qualified Control.Monad as Monad+import qualified Data.Attoparsec.ByteString.Char8 as A+import qualified Data.Bits as Bits+import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Builder as Builder+import qualified Data.Function as Function+import qualified Data.Int as Int+import qualified Data.Scientific as Scientific+import qualified Database.PostgreSQL.Simple.FromField as Postgres+import qualified Database.PostgreSQL.Simple.ToField as Postgres+import qualified Database.PostgreSQL.Simple.TypeInfo.Static as Postgres++-- | This type represents a PostgreSQL interval. Intervals can have month, day,+-- and microsecond components. Each component is bounded, so they are not+-- arbitrary precision. For more information about intervals, consult the+-- PostgreSQL documentation:+-- <https://www.postgresql.org/docs/17/datatype-datetime.html#DATATYPE-INTERVAL-INPUT>.+--+-- Note that the @time@ library provides several duration types that are not+-- appropriate to use as PostgreSQL intervals:+--+-- - @NominalDiffTime@: Does not handle days or months. Allows up to picosecond+--   precision. Is not bounded.+-- - @CalendarDiffTime@: Does not handle days. Embeds a @NominalDiffTime@. Is+--   not bounded.+-- - @CalendarDiffDays@: Does not handle seconds. Is not bounded.+data Interval = MkInterval+  { months :: !Int.Int32,+    days :: !Int.Int32,+    microseconds :: !Int.Int64+  }+  deriving (Eq, Show)++-- | Uses 'parse'. Ensures that the OID is 'Postgres.intervalOid'.+instance Postgres.FromField Interval where+  fromField = Postgres.attoFieldParser (== Postgres.intervalOid) parse++-- | Uses 'render'. Always includes an @interval@ prefix, like+-- @interval '2 months 3 days 4 microseconds'@.+instance Postgres.ToField Interval where+  toField = Postgres.Plain . ("interval " <>) . Postgres.inQuotes . render++-- | The empty interval, representing no time at all.+--+-- >>> zero+-- MkInterval {months = 0, days = 0, microseconds = 0}+zero :: Interval+zero = MkInterval 0 0 0++-- | Creates an interval from a number of microseconds.+--+-- >>> fromMicroseconds 1+-- MkInterval {months = 0, days = 0, microseconds = 1}+fromMicroseconds :: Int.Int64 -> Interval+fromMicroseconds x = zero {microseconds = x}++-- | Creates an interval from a number of milliseconds. Returns 'Nothing' if+-- the interval would overflow.+--+-- >>> fromMilliseconds 1+-- Just (MkInterval {months = 0, days = 0, microseconds = 1000})+-- >>> fromMilliseconds 9223372036854776+-- Nothing+fromMilliseconds :: Int.Int64 -> Maybe Interval+fromMilliseconds =+  fmap fromMicroseconds+    . Bits.toIntegralSized+    . (* 1e3)+    . toInteger++-- | Creates an interval from a number of seconds. Returns 'Nothing' if the+-- interval would overflow.+--+-- >>> fromSeconds 1+-- Just (MkInterval {months = 0, days = 0, microseconds = 1000000})+-- >>> fromSeconds 9223372036855+-- Nothing+fromSeconds :: Int.Int64 -> Maybe Interval+fromSeconds =+  fmap fromMicroseconds+    . Bits.toIntegralSized+    . (* 1e6)+    . toInteger++-- | Creates an interval from a number of minutes. Returns 'Nothing' if the+-- interval would overflow.+--+-- >>> fromMinutes 1+-- Just (MkInterval {months = 0, days = 0, microseconds = 60000000})+-- >>> fromMinutes 153722867281+-- Nothing+fromMinutes :: Int.Int64 -> Maybe Interval+fromMinutes =+  fmap fromMicroseconds+    . Bits.toIntegralSized+    . (* 60e6)+    . toInteger++-- | Creates an interval from a number of hours. Returns 'Nothing' if the+-- interval would overflow.+--+-- >>> fromHours 1+-- Just (MkInterval {months = 0, days = 0, microseconds = 3600000000})+-- >>> fromHours 2562047789+-- Nothing+fromHours :: Int.Int64 -> Maybe Interval+fromHours =+  fmap fromMicroseconds+    . Bits.toIntegralSized+    . (* 3600e6)+    . toInteger++-- | Creates an interval from a number of days.+--+-- >>> fromDays 1+-- MkInterval {months = 0, days = 1, microseconds = 0}+fromDays :: Int.Int32 -> Interval+fromDays x = zero {days = x}++-- | Creates an interval from a number of weeks. Returns 'Nothing' if the+-- interval would overflow.+--+-- >>> fromWeeks 1+-- Just (MkInterval {months = 0, days = 7, microseconds = 0})+-- >>> fromWeeks 306783379+-- Nothing+fromWeeks :: Int.Int32 -> Maybe Interval+fromWeeks =+  fmap fromDays+    . Bits.toIntegralSized+    . (* 7)+    . toInteger++-- | Creates an interval from a number of months.+--+-- >>> fromMonths 1+-- MkInterval {months = 1, days = 0, microseconds = 0}+fromMonths :: Int.Int32 -> Interval+fromMonths x = zero {months = x}++-- | Creates an interval from a number of years. Returns 'Nothing' if the+-- interval would overflow.+--+-- >>> fromYears 1+-- Just (MkInterval {months = 12, days = 0, microseconds = 0})+-- >>> fromYears 178956971+-- Nothing+fromYears :: Int.Int32 -> Maybe Interval+fromYears =+  fmap fromMonths+    . Bits.toIntegralSized+    . (* 12)+    . toInteger++-- | Adds two intervals. Returns 'Nothing' if the result would overflow.+--+-- >>> add (fromMonths 1) (fromDays 2)+-- Just (MkInterval {months = 1, days = 2, microseconds = 0})+-- >>> add (fromDays 2147483647) (fromDays 1)+-- Nothing+add :: Interval -> Interval -> Maybe Interval+add x y =+  let safeAdd :: (Bits.Bits a, Integral a) => a -> a -> Maybe a+      safeAdd n = Bits.toIntegralSized . Function.on (+) toInteger n+   in MkInterval+        <$> Function.on safeAdd months x y+        <*> Function.on safeAdd days x y+        <*> Function.on safeAdd microseconds x y++-- | Renders an interval to a 'Builder'. This always has the same format:+-- @"X months Y days Z microseconds"@, where @X@, @Y@, and @Z@ are signed+-- integers.+--+-- This is not the most compact format, but it is very easy to interpret and+-- does not require dealing with decimals (which could introduce precision+-- problems).+--+-- >>> render MkInterval { months = 0, days = -1, microseconds = 2 }+-- "0 months -1 days +2 microseconds"+render :: Interval -> Builder.Builder+render x =+  let signed :: (Num a, Ord a) => (a -> Builder.Builder) -> a -> Builder.Builder+      signed f n = (if n > 0 then "+" else "") <> f n+   in signed Builder.int32Dec (months x)+        <> " months "+        <> signed Builder.int32Dec (days x)+        <> " days "+        <> signed Builder.int64Dec (microseconds x)+        <> " microseconds"++-- | Parses an interval. This is not a general purpose parser. It only supports+-- the formats that PostgreSQL generates. For example, it will fail to parse an+-- interval like @"1 week"@ because PostgreSQL never uses weeks when rendering+-- intervals.+parse :: A.Parser Interval+parse =+  -- Start with parsers that have non-empty prefixes, in order to avoid+  -- ambiguity. Neither of the `postgres` nor `sql_standard` interval styles+  -- have a prefix (or suffix), so whichever one is attempted first needs to+  -- make sure it has consumed all of the input.+  A.choice $+    parseInfinities+      : fmap+        (fromComponents =<<)+        [ parseIso8601,+          parsePostgresVerbose,+          parsePostgres <* A.endOfInput,+          parseSqlStandard <* A.endOfInput+        ]++parseInfinities :: A.Parser Interval+parseInfinities =+  -- Both `-infinity` and `infinity` are new as of PostgreSQL 17.0.+  -- https://www.postgresql.org/message-id/E1r2rB1-005PHm-UL%40gemulon.postgresql.org+  A.choice+    [ MkInterval minBound minBound minBound <$ "-infinity",+      MkInterval maxBound maxBound maxBound <$ "infinity"+    ]++parseIso8601 :: A.Parser [Component]+parseIso8601 = do+  Monad.void "P"+  dates <-+    A.many' $+      A.choice+        [ Years <$> A.signed A.decimal <* "Y",+          Months <$> A.signed A.decimal <* "M",+          Days <$> A.signed A.decimal <* "D"+        ]+  times <- A.option [] $ do+    Monad.void "T"+    A.many' $+      A.choice+        [ Hours <$> A.signed A.decimal <* "H",+          Minutes <$> A.signed A.decimal <* "M",+          Microseconds <$> A.signed A.scientific <* "S"+        ]+  pure $ dates <> times++parsePostgresVerbose :: A.Parser [Component]+parsePostgresVerbose = do+  Monad.void "@ "+  components <-+    flip A.sepBy " " $+      A.choice+        [ Years <$> A.signed A.decimal <* maybePlural " year",+          Months <$> A.signed A.decimal <* maybePlural " mon",+          Days <$> A.signed A.decimal <* maybePlural " day",+          Hours <$> A.signed A.decimal <* maybePlural " hour",+          Minutes <$> A.signed A.decimal <* maybePlural " min",+          Microseconds <$> A.signed A.scientific <* A.option "" (maybePlural " sec")+        ]+  ago <- A.option "" " ago"+  pure $ negateComponentsWhen (not $ ByteString.null ago) components++parsePostgres :: A.Parser [Component]+parsePostgres = do+  dates <-+    flip A.sepBy " " $+      A.choice+        [ Years <$> A.signed A.decimal <* maybePlural " year",+          Months <$> A.signed A.decimal <* maybePlural " mon",+          Days <$> A.signed A.decimal <* maybePlural " day"+        ]+  time <- A.option [] $ A.skipSpace *> parseTime+  pure $ dates <> time++parseSqlStandard :: A.Parser [Component]+parseSqlStandard = do+  let parseYearsAndMonths = do+        sign <- parseSign+        years <- Years <$> A.decimal <* "-"+        months_ <- Months <$> A.decimal+        pure $ negateComponentsWhen (sign == "-") [years, months_]+  let parseDays = (: []) . Days <$> A.signed A.decimal+  let parsers = [parseYearsAndMonths, parseTime, parseDays]+  mconcat <$> A.sepBy1 (A.choice parsers) " "++parseTime :: A.Parser [Component]+parseTime = do+  sign <- parseSign+  hours <- Hours <$> A.decimal <* ":"+  minutes <- Minutes <$> A.decimal <* ":"+  micros <- Microseconds <$> A.scientific+  pure $ negateComponentsWhen (sign == "-") [hours, minutes, micros]++parseSign :: A.Parser ByteString.ByteString+parseSign = A.choice ["-", "+", ""]++maybePlural :: ByteString.ByteString -> A.Parser ByteString.ByteString+maybePlural word = (<>) <$> A.string word <*> A.option "" "s"++-- | One component of an interval. This is used to retain arbitrary precision+-- for as long as possible before converting. It also shows which components+-- are accepted, like years and months.+data Component+  = Years !Integer+  | Months !Integer+  | Days !Integer+  | Hours !Integer+  | Minutes !Integer+  | Microseconds !Scientific.Scientific+  deriving (Eq, Show)++-- | Converts a 'Component' to an 'Interval'. Returns 'Nothing' if the+-- component would overflow.+fromComponent :: Component -> Maybe Interval+fromComponent c = case c of+  Years y -> fromYears =<< Bits.toIntegralSized y+  Months m -> fromMonths <$> Bits.toIntegralSized m+  Days d -> fromDays <$> Bits.toIntegralSized d+  Hours h -> fromHours =<< Bits.toIntegralSized h+  Minutes m -> fromMinutes =<< Bits.toIntegralSized m+  Microseconds u -> fromMicroseconds <$> Scientific.toBoundedInteger (u * 1e6)++-- | Converts a list of 'Component's to an 'Interval'. Returns 'Nothing' if any+-- of the components would overflow, or if adding any of them together would+-- overflow.+fromComponents ::+  (Applicative.Alternative f, Traversable t) =>+  t Component ->+  f Interval+fromComponents =+  maybe Applicative.empty pure+    . (Monad.foldM add zero Monad.<=< traverse fromComponent)++negateComponent :: Component -> Component+negateComponent c = case c of+  Years y -> Years -y+  Months m -> Months -m+  Days d -> Days -d+  Hours h -> Hours -h+  Minutes m -> Minutes -m+  Microseconds u -> Microseconds -u++negateComponentsWhen :: (Functor f) => Bool -> f Component -> f Component+negateComponentsWhen p = if p then fmap negateComponent else id
+ source/test-suite/Main.hs view
@@ -0,0 +1,222 @@+{-# LANGUAGE LexicalNegation #-}+{-# LANGUAGE NumDecimals #-}+{-# LANGUAGE OverloadedStrings #-}++import qualified Control.Monad as Monad+import qualified Data.Attoparsec.ByteString.Char8 as Attoparsec+import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Builder as Builder+import qualified Data.ByteString.Char8 as Ascii+import qualified Data.Int as Int+import qualified Database.PostgreSQL.Interval.Unstable as I+import qualified Test.Hspec as H++main :: IO ()+main = H.hspec spec++spec :: H.Spec+spec = H.describe "Database.PostgreSQL.Interval" $ do+  H.describe "zero" $ do+    H.it "works" $ do+      I.zero `H.shouldBe` I.MkInterval 0 0 0++  H.describe "add" $ do+    H.it "succeeds with no overflow" $ do+      let actual = I.add (I.MkInterval 1 2 3) (I.MkInterval 4 5 6)+      actual `H.shouldBe` Just (I.MkInterval 5 7 9)++    H.it "fails with positive month overflow" $ do+      let actual = I.add (I.fromMonths maxBound) (I.fromMonths 1)+      actual `H.shouldBe` Nothing++    H.it "fails with negative month overflow" $ do+      let actual = I.add (I.fromMonths minBound) (I.fromMonths -1)+      actual `H.shouldBe` Nothing++    H.it "fails with positive day overflow" $ do+      let actual = I.add (I.fromDays maxBound) (I.fromDays 1)+      actual `H.shouldBe` Nothing++    H.it "fails with negative day overflow" $ do+      let actual = I.add (I.fromDays minBound) (I.fromDays -1)+      actual `H.shouldBe` Nothing++    H.it "fails with positive microsecond overflow" $ do+      let actual = I.add (I.fromMicroseconds maxBound) (I.fromMicroseconds 1)+      actual `H.shouldBe` Nothing++    H.it "fails with negative microsecond overflow" $ do+      let actual = I.add (I.fromMicroseconds minBound) (I.fromMicroseconds -1)+      actual `H.shouldBe` Nothing++  H.describe "fromMicroseconds" $ do+    H.it "works" $ do+      I.fromMicroseconds 1 `H.shouldBe` I.MkInterval 0 0 1++  H.describe "fromMilliseconds" $ do+    H.it "succeeds with no overflow" $ do+      I.fromMilliseconds 1 `H.shouldBe` Just (I.MkInterval 0 0 1e3)++    H.it "fails with overflow" $ do+      I.fromMilliseconds maxBound `H.shouldBe` Nothing++  H.describe "fromSeconds" $ do+    H.it "succeeds with no overflow" $ do+      I.fromSeconds 1 `H.shouldBe` Just (I.MkInterval 0 0 1e6)++    H.it "fails with overflow" $ do+      I.fromSeconds maxBound `H.shouldBe` Nothing++  H.describe "fromMinutes" $ do+    H.it "succeeds with no overflow" $ do+      I.fromMinutes 1 `H.shouldBe` Just (I.MkInterval 0 0 60e6)++    H.it "fails with overflow" $ do+      I.fromMinutes maxBound `H.shouldBe` Nothing++  H.describe "fromHours" $ do+    H.it "succeeds with no overflow" $ do+      I.fromHours 1 `H.shouldBe` Just (I.MkInterval 0 0 3600e6)++    H.it "fails with overflow" $ do+      I.fromHours maxBound `H.shouldBe` Nothing++  H.describe "fromDays" $ do+    H.it "works" $ do+      I.fromDays 1 `H.shouldBe` I.MkInterval 0 1 0++  H.describe "fromWeeks" $ do+    H.it "succeeds with no overflow" $ do+      I.fromWeeks 1 `H.shouldBe` Just (I.MkInterval 0 7 0)++    H.it "fails with overflow" $ do+      I.fromWeeks maxBound `H.shouldBe` Nothing++  H.describe "fromMonths" $ do+    H.it "works" $ do+      I.fromMonths 1 `H.shouldBe` I.MkInterval 1 0 0++  H.describe "fromYears" $ do+    H.it "succeeds with no overflow" $ do+      I.fromYears 1 `H.shouldBe` Just (I.MkInterval 12 0 0)++    H.it "fails with overflow" $ do+      I.fromYears maxBound `H.shouldBe` Nothing++  H.describe "render" $ do+    H.it "works with zero" $ do+      let actual = Builder.toLazyByteString $ I.render I.zero+      actual `H.shouldBe` "0 months 0 days 0 microseconds"++    H.it "works with positive components" $ do+      let actual = Builder.toLazyByteString . I.render $ I.MkInterval 1 2 3+      actual `H.shouldBe` "+1 months +2 days +3 microseconds"++    H.it "works with negative components" $ do+      let actual = Builder.toLazyByteString . I.render $ I.MkInterval -3 -2 -1+      actual `H.shouldBe` "-3 months -2 days -1 microseconds"++  H.describe "parse" $ do+    H.it "fails with invalid input" $ do+      let actual = Attoparsec.parseOnly I.parse "invalid"+      actual `H.shouldBe` Left "Failed reading: empty"++    H.it "succeeds with positive infinity" $ do+      let actual = Attoparsec.parseOnly I.parse "infinity"+      actual `H.shouldBe` Right (I.MkInterval maxBound maxBound maxBound)++    H.it "succeeds with negative infinity" $ do+      let actual = Attoparsec.parseOnly I.parse "-infinity"+      actual `H.shouldBe` Right (I.MkInterval minBound minBound minBound)++    Monad.forM_ intervalStyles $ \(_, field) ->+      Monad.forM_ examples $ \example -> do+        let input = field example+        H.it ("succeeds with " <> Ascii.unpack input) $ do+          let actual = Attoparsec.parseOnly I.parse input+          actual `H.shouldBe` Right (exampleInterval example)++data IntervalStyle+  = Iso8601+  | Postgres+  | PostgresVerbose+  | SqlStandard+  deriving (Eq, Show)++data Example = MkExample+  { exampleInterval :: I.Interval,+    exampleIso8601 :: ByteString.ByteString,+    examplePostgres :: ByteString.ByteString,+    examplePostgresVerbose :: ByteString.ByteString,+    exampleSqlStandard :: ByteString.ByteString+  }+  deriving (Eq, Show)++intervalStyles :: [(IntervalStyle, Example -> ByteString.ByteString)]+intervalStyles =+  [ (Iso8601, exampleIso8601),+    (Postgres, examplePostgres),+    (PostgresVerbose, examplePostgresVerbose),+    (SqlStandard, exampleSqlStandard)+  ]++mkExample ::+  Int.Int32 ->+  Int.Int32 ->+  Int.Int64 ->+  ByteString.ByteString ->+  ByteString.ByteString ->+  ByteString.ByteString ->+  ByteString.ByteString ->+  Example+mkExample m d s iso8601 postgres postgresVerbose sqlStandard =+  MkExample+    { exampleInterval = I.MkInterval {I.months = m, I.days = d, I.microseconds = s},+      exampleIso8601 = iso8601,+      examplePostgres = postgres,+      examplePostgresVerbose = postgresVerbose,+      exampleSqlStandard = sqlStandard+    }++examples :: [Example]+examples =+  [ mkExample 0 0 0 "PT0S" "00:00:00" "@ 0" "0",+    mkExample 1 0 0 "P1M" "1 mon" "@ 1 mon" "0-1",+    mkExample -1 0 0 "P-1M" "-1 mons" "@ 1 mon ago" "-0-1",+    mkExample 3 0 0 "P3M" "3 mons" "@ 3 mons" "0-3",+    mkExample 6 0 0 "P6M" "6 mons" "@ 6 mons" "0-6",+    mkExample 12 0 0 "P1Y" "1 year" "@ 1 year" "1-0",+    mkExample -12 0 0 "P-1Y" "-1 years" "@ 1 year ago" "-1-0",+    mkExample 13 0 0 "P1Y1M" "1 year 1 mon" "@ 1 year 1 mon" "1-1",+    mkExample -13 0 0 "P-1Y-1M" "-1 years -1 mons" "@ 1 year 1 mon ago" "-1-1",+    mkExample 24 0 0 "P2Y" "2 years" "@ 2 years" "2-0",+    mkExample 0 1 0 "P1D" "1 day" "@ 1 day" "1 0:00:00",+    mkExample 0 -1 0 "P-1D" "-1 days" "@ 1 day ago" "-1 0:00:00",+    mkExample 0 2 0 "P2D" "2 days" "@ 2 days" "2 0:00:00",+    mkExample 0 7 0 "P7D" "7 days" "@ 7 days" "7 0:00:00",+    mkExample 0 0 1 "PT0.000001S" "00:00:00.000001" "@ 0.000001 secs" "0:00:00.000001",+    mkExample 0 0 -1 "PT-0.000001S" "-00:00:00.000001" "@ 0.000001 secs ago" "-0:00:00.000001",+    mkExample 0 0 1e3 "PT0.001S" "00:00:00.001" "@ 0.001 secs" "0:00:00.001",+    mkExample 0 0 1e6 "PT1S" "00:00:01" "@ 1 sec" "0:00:01",+    mkExample 0 0 -1e6 "PT-1S" "-00:00:01" "@ 1 sec ago" "-0:00:01",+    mkExample 0 0 2e6 "PT2S" "00:00:02" "@ 2 secs" "0:00:02",+    mkExample 0 0 60e6 "PT1M" "00:01:00" "@ 1 min" "0:01:00",+    mkExample 0 0 -60e6 "PT-1M" "-00:01:00" "@ 1 min ago" "-0:01:00",+    mkExample 0 0 120e6 "PT2M" "00:02:00" "@ 2 mins" "0:02:00",+    mkExample 0 0 3600e6 "PT1H" "01:00:00" "@ 1 hour" "01:00:00",+    mkExample 0 0 -3600e6 "PT-1H" "-01:00:00" "@ 1 hour ago" "-01:00:00",+    mkExample 0 0 7200e6 "PT2H" "02:00:00" "@ 2 hours" "02:00:00",+    mkExample 0 0 86400e6 "PT24H" "24:00:00" "@ 24 hours" "24:00:00",+    mkExample 1 1 1e6 "P1M1DT1S" "1 mon 1 day 00:00:01" "@ 1 mon 1 day 1 sec" "+0-1 +1 +0:00:01",+    mkExample -1 -1 -1e6 "P-1M-1DT-1S" "-1 mons -1 days -00:00:01" "@ 1 mon 1 day 1 sec ago" "-0-1 -1 -0:00:01",+    mkExample -1 1 1e6 "P-1M1DT1S" "-1 mons +1 day 00:00:01" "@ 1 mon -1 days -1 sec ago" "-0-1 +1 +0:00:01",+    mkExample 1 -1 1e6 "P1M-1DT1S" "1 mon -1 days +00:00:01" "@ 1 mon -1 days 1 sec" "+0-1 -1 +0:00:01",+    mkExample 1 1 -1e6 "P1M1DT-1S" "1 mon 1 day -00:00:01" "@ 1 mon 1 day -1 sec" "+0-1 +1 -0:00:01",+    mkExample 14 3 14706000007 "P1Y2M3DT4H5M6.000007S" "1 year 2 mons 3 days 04:05:06.000007" "@ 1 year 2 mons 3 days 4 hours 5 mins 6.000007 secs" "+1-2 +3 +4:05:06.000007",+    mkExample maxBound 0 0 "P178956970Y7M" "178956970 years 7 mons" "@ 178956970 years 7 mons" "178956970-7",+    mkExample minBound 0 0 "P-178956970Y-8M" "-178956970 years -8 mons" "@ 178956970 years 8 mons ago" "-178956970-8",+    mkExample 0 maxBound 0 "P2147483647D" "2147483647 days" "@ 2147483647 days" "2147483647 0:00:00",+    mkExample 0 minBound 0 "P-2147483648D" "-2147483648 days" "@ 2147483648 days ago" "-2147483648 0:00:00",+    mkExample 0 0 maxBound "PT2562047788H54.775807S" "2562047788:00:54.775807" "@ 2562047788 hours 54.775807 secs" "2562047788:00:54.775807",+    mkExample 0 0 minBound "PT-2562047788H-54.775808S" "-2562047788:00:54.775808" "@ 2562047788 hours 54.775808 secs ago" "-2562047788:00:54.775808"+  ]