{-# 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: -- . -- -- 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