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 +4/−0
- LICENSE.txt +14/−0
- README.md +3/−0
- postgresql-simple-interval.cabal +72/−0
- source/library/Database/PostgreSQL/Interval.hs +16/−0
- source/library/Database/PostgreSQL/Interval/Unstable.hs +344/−0
- source/test-suite/Main.hs +222/−0
+ 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"+ ]