packages feed

postgresql-simple-interval 0.2025.7.9 → 0.2025.7.10

raw patch · 6 files changed

+382/−370 lines, 6 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Database.PostgreSQL.Interval: MkInterval :: !Int32 -> !Int32 -> !Int64 -> Interval
- Database.PostgreSQL.Interval: [days] :: Interval -> !Int32
- Database.PostgreSQL.Interval: [microseconds] :: Interval -> !Int64
- Database.PostgreSQL.Interval: [months] :: Interval -> !Int32
- Database.PostgreSQL.Interval: data Interval
- Database.PostgreSQL.Interval: fromDays :: Int32 -> Interval
- Database.PostgreSQL.Interval: fromHours :: Int64 -> Maybe Interval
- Database.PostgreSQL.Interval: fromMicroseconds :: Int64 -> Interval
- Database.PostgreSQL.Interval: fromMilliseconds :: Int64 -> Maybe Interval
- Database.PostgreSQL.Interval: fromMinutes :: Int64 -> Maybe Interval
- Database.PostgreSQL.Interval: fromMonths :: Int32 -> Interval
- Database.PostgreSQL.Interval: fromSeconds :: Int64 -> Maybe Interval
- Database.PostgreSQL.Interval: fromWeeks :: Int32 -> Maybe Interval
- Database.PostgreSQL.Interval: fromYears :: Int32 -> Maybe Interval
- Database.PostgreSQL.Interval: zero :: Interval
- Database.PostgreSQL.Interval.Unstable: Days :: !Integer -> Component
- Database.PostgreSQL.Interval.Unstable: Hours :: !Integer -> Component
- Database.PostgreSQL.Interval.Unstable: Microseconds :: !Scientific -> Component
- Database.PostgreSQL.Interval.Unstable: Minutes :: !Integer -> Component
- Database.PostgreSQL.Interval.Unstable: MkInterval :: !Int32 -> !Int32 -> !Int64 -> Interval
- Database.PostgreSQL.Interval.Unstable: Months :: !Integer -> Component
- Database.PostgreSQL.Interval.Unstable: Years :: !Integer -> Component
- Database.PostgreSQL.Interval.Unstable: [days] :: Interval -> !Int32
- Database.PostgreSQL.Interval.Unstable: [microseconds] :: Interval -> !Int64
- Database.PostgreSQL.Interval.Unstable: [months] :: Interval -> !Int32
- Database.PostgreSQL.Interval.Unstable: add :: Interval -> Interval -> Maybe Interval
- Database.PostgreSQL.Interval.Unstable: data Component
- Database.PostgreSQL.Interval.Unstable: data Interval
- Database.PostgreSQL.Interval.Unstable: fromComponent :: Component -> Maybe Interval
- Database.PostgreSQL.Interval.Unstable: fromComponents :: (Alternative f, Traversable t) => t Component -> f Interval
- Database.PostgreSQL.Interval.Unstable: fromDays :: Int32 -> Interval
- Database.PostgreSQL.Interval.Unstable: fromHours :: Int64 -> Maybe Interval
- Database.PostgreSQL.Interval.Unstable: fromMicroseconds :: Int64 -> Interval
- Database.PostgreSQL.Interval.Unstable: fromMilliseconds :: Int64 -> Maybe Interval
- Database.PostgreSQL.Interval.Unstable: fromMinutes :: Int64 -> Maybe Interval
- Database.PostgreSQL.Interval.Unstable: fromMonths :: Int32 -> Interval
- Database.PostgreSQL.Interval.Unstable: fromSeconds :: Int64 -> Maybe Interval
- Database.PostgreSQL.Interval.Unstable: fromWeeks :: Int32 -> Maybe Interval
- Database.PostgreSQL.Interval.Unstable: fromYears :: Int32 -> Maybe Interval
- Database.PostgreSQL.Interval.Unstable: instance Database.PostgreSQL.Simple.FromField.FromField Database.PostgreSQL.Interval.Unstable.Interval
- Database.PostgreSQL.Interval.Unstable: instance Database.PostgreSQL.Simple.ToField.ToField Database.PostgreSQL.Interval.Unstable.Interval
- Database.PostgreSQL.Interval.Unstable: instance GHC.Classes.Eq Database.PostgreSQL.Interval.Unstable.Component
- Database.PostgreSQL.Interval.Unstable: instance GHC.Classes.Eq Database.PostgreSQL.Interval.Unstable.Interval
- Database.PostgreSQL.Interval.Unstable: instance GHC.Internal.Show.Show Database.PostgreSQL.Interval.Unstable.Component
- Database.PostgreSQL.Interval.Unstable: instance GHC.Internal.Show.Show Database.PostgreSQL.Interval.Unstable.Interval
- Database.PostgreSQL.Interval.Unstable: maybePlural :: ByteString -> Parser ByteString
- Database.PostgreSQL.Interval.Unstable: negateComponent :: Component -> Component
- Database.PostgreSQL.Interval.Unstable: negateComponentsWhen :: Functor f => Bool -> f Component -> f Component
- Database.PostgreSQL.Interval.Unstable: parse :: Parser Interval
- Database.PostgreSQL.Interval.Unstable: parseInfinities :: Parser Interval
- Database.PostgreSQL.Interval.Unstable: parseIso8601 :: Parser [Component]
- Database.PostgreSQL.Interval.Unstable: parsePostgres :: Parser [Component]
- Database.PostgreSQL.Interval.Unstable: parsePostgresVerbose :: Parser [Component]
- Database.PostgreSQL.Interval.Unstable: parseSign :: Parser ByteString
- Database.PostgreSQL.Interval.Unstable: parseSqlStandard :: Parser [Component]
- Database.PostgreSQL.Interval.Unstable: parseTime :: Parser [Component]
- Database.PostgreSQL.Interval.Unstable: render :: Interval -> Builder
- Database.PostgreSQL.Interval.Unstable: zero :: Interval
+ Database.PostgreSQL.Simple.Interval: MkInterval :: !Int32 -> !Int32 -> !Int64 -> Interval
+ Database.PostgreSQL.Simple.Interval: [days] :: Interval -> !Int32
+ Database.PostgreSQL.Simple.Interval: [microseconds] :: Interval -> !Int64
+ Database.PostgreSQL.Simple.Interval: [months] :: Interval -> !Int32
+ Database.PostgreSQL.Simple.Interval: data Interval
+ Database.PostgreSQL.Simple.Interval: fromDays :: Int32 -> Interval
+ Database.PostgreSQL.Simple.Interval: fromHours :: Int64 -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval: fromMicroseconds :: Int64 -> Interval
+ Database.PostgreSQL.Simple.Interval: fromMilliseconds :: Int64 -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval: fromMinutes :: Int64 -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval: fromMonths :: Int32 -> Interval
+ Database.PostgreSQL.Simple.Interval: fromSeconds :: Int64 -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval: fromWeeks :: Int32 -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval: fromYears :: Int32 -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval: zero :: Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: Days :: !Integer -> Component
+ Database.PostgreSQL.Simple.Interval.Unstable: Hours :: !Integer -> Component
+ Database.PostgreSQL.Simple.Interval.Unstable: Microseconds :: !Integer -> Component
+ Database.PostgreSQL.Simple.Interval.Unstable: Minutes :: !Integer -> Component
+ Database.PostgreSQL.Simple.Interval.Unstable: MkInterval :: !Int32 -> !Int32 -> !Int64 -> Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: Months :: !Integer -> Component
+ Database.PostgreSQL.Simple.Interval.Unstable: Seconds :: !Scientific -> Component
+ Database.PostgreSQL.Simple.Interval.Unstable: Years :: !Integer -> Component
+ Database.PostgreSQL.Simple.Interval.Unstable: [days] :: Interval -> !Int32
+ Database.PostgreSQL.Simple.Interval.Unstable: [microseconds] :: Interval -> !Int64
+ Database.PostgreSQL.Simple.Interval.Unstable: [months] :: Interval -> !Int32
+ Database.PostgreSQL.Simple.Interval.Unstable: add :: Interval -> Interval -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: data Component
+ Database.PostgreSQL.Simple.Interval.Unstable: data Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: fromComponent :: Component -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: fromComponents :: (Alternative f, Traversable t) => t Component -> f Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: fromDays :: Int32 -> Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: fromHours :: Int64 -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: fromMicroseconds :: Int64 -> Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: fromMilliseconds :: Int64 -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: fromMinutes :: Int64 -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: fromMonths :: Int32 -> Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: fromSeconds :: Int64 -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: fromWeeks :: Int32 -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: fromYears :: Int32 -> Maybe Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: instance Database.PostgreSQL.Simple.FromField.FromField Database.PostgreSQL.Simple.Interval.Unstable.Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: instance Database.PostgreSQL.Simple.ToField.ToField Database.PostgreSQL.Simple.Interval.Unstable.Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: instance GHC.Classes.Eq Database.PostgreSQL.Simple.Interval.Unstable.Component
+ Database.PostgreSQL.Simple.Interval.Unstable: instance GHC.Classes.Eq Database.PostgreSQL.Simple.Interval.Unstable.Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: instance GHC.Internal.Show.Show Database.PostgreSQL.Simple.Interval.Unstable.Component
+ Database.PostgreSQL.Simple.Interval.Unstable: instance GHC.Internal.Show.Show Database.PostgreSQL.Simple.Interval.Unstable.Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: maybePlural :: ByteString -> Parser ByteString
+ Database.PostgreSQL.Simple.Interval.Unstable: negateComponent :: Component -> Component
+ Database.PostgreSQL.Simple.Interval.Unstable: negateComponentsWhen :: Functor f => Bool -> f Component -> f Component
+ Database.PostgreSQL.Simple.Interval.Unstable: parse :: Parser Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: parseInfinities :: Parser Interval
+ Database.PostgreSQL.Simple.Interval.Unstable: parseIso8601 :: Parser [Component]
+ Database.PostgreSQL.Simple.Interval.Unstable: parsePostgres :: Parser [Component]
+ Database.PostgreSQL.Simple.Interval.Unstable: parsePostgresVerbose :: Parser [Component]
+ Database.PostgreSQL.Simple.Interval.Unstable: parseSign :: Parser ByteString
+ Database.PostgreSQL.Simple.Interval.Unstable: parseSqlStandard :: Parser [Component]
+ Database.PostgreSQL.Simple.Interval.Unstable: parseTime :: Parser [Component]
+ Database.PostgreSQL.Simple.Interval.Unstable: render :: Interval -> Builder
+ Database.PostgreSQL.Simple.Interval.Unstable: zero :: Interval

Files

postgresql-simple-interval.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: postgresql-simple-interval-version: 0.2025.7.9+version: 0.2025.7.10 synopsis: A simple interval type for PostgreSQL. description:   This library provides a simple interval type for PostgreSQL. It supports@@ -59,8 +59,8 @@    -- cabal-gild: discover source/library   exposed-modules:-    Database.PostgreSQL.Interval-    Database.PostgreSQL.Interval.Unstable+    Database.PostgreSQL.Simple.Interval+    Database.PostgreSQL.Simple.Interval.Unstable    hs-source-dirs: source/library 
− source/library/Database/PostgreSQL/Interval.hs
@@ -1,16 +0,0 @@-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
@@ -1,344 +0,0 @@-{-# 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/library/Database/PostgreSQL/Simple/Interval.hs view
@@ -0,0 +1,16 @@+module Database.PostgreSQL.Simple.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.Simple.Interval.Unstable as Unstable
+ source/library/Database/PostgreSQL/Simple/Interval/Unstable.hs view
@@ -0,0 +1,348 @@+{-# LANGUAGE LexicalNegation #-}+{-# LANGUAGE NumDecimals #-}+{-# LANGUAGE OverloadedStrings #-}++module Database.PostgreSQL.Simple.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 mon Y day Z us"@, 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 mon -1 day +2 us"+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)+        <> " mon "+        <> signed Builder.int32Dec (days x)+        <> " day "+        <> signed Builder.int64Dec (microseconds x)+        <> " us"++-- | 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",+          Seconds <$> 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.decimal <* " us",+          Seconds <$> 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 <* ":"+  seconds <- Seconds <$> A.scientific+  pure $ negateComponentsWhen (sign == "-") [hours, minutes, seconds]++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+  | Seconds !Scientific.Scientific+  | Microseconds !Integer+  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+  Seconds u -> fromMicroseconds <$> Scientific.toBoundedInteger (u * 1e6)+  Microseconds u -> fromMicroseconds <$> Bits.toIntegralSized u++-- | 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+  Seconds u -> Seconds -u+  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
@@ -6,16 +6,16 @@ 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.ByteString.Lazy as LazyByteString import qualified Data.Int as Int-import qualified Database.PostgreSQL.Interval.Unstable as I+import qualified Database.PostgreSQL.Simple.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+spec = H.describe "Database.PostgreSQL.Simple.Interval" $ do   H.describe "zero" $ do     H.it "works" $ do       I.zero `H.shouldBe` I.MkInterval 0 0 0@@ -106,15 +106,15 @@   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"+      actual `H.shouldBe` "@ 0 mon 0 day 0 us"      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"+      actual `H.shouldBe` "@ +1 mon +2 day +3 us"      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"+      actual `H.shouldBe` "@ -3 mon -2 day -1 us"    H.describe "parse" $ do     H.it "fails with invalid input" $ do@@ -132,9 +132,17 @@     Monad.forM_ intervalStyles $ \(_, field) ->       Monad.forM_ examples $ \example -> do         let input = field example-        H.it ("succeeds with " <> Ascii.unpack input) $ do+        H.it ("succeeds with " <> show input) $ do           let actual = Attoparsec.parseOnly I.parse input           actual `H.shouldBe` Right (exampleInterval example)++  H.describe "round trip" $ do+    Monad.forM_ examples $ \example -> do+      let interval = exampleInterval example+      let input = LazyByteString.toStrict . Builder.toLazyByteString $ I.render interval+      H.it ("succeeds with " <> show input) $ do+        let actual = Attoparsec.parseOnly I.parse input+        actual `H.shouldBe` Right interval  data IntervalStyle   = Iso8601