megaparsec-time-0.3.0.0: src/Text/Megaparsec/Time/Common.hs
{-# LANGUAGE FlexibleContexts #-}
-- |
-- Module : Text.Megaparsec.Time.Common
-- Description : Stream-agnostic time parser builders.
-- Copyright : (c) drlkf, 2024
-- License : GPL-3
-- Maintainer : drlkf@drlkf.net
-- Stability : experimental
--
-- Builders shared by 'Text.Megaparsec.Time.Char' and 'Text.Megaparsec.Time.Byte'.
module Text.Megaparsec.Time.Common (
-- * Types
DayResult,
-- * Shared parser builders
mkDateParser,
mkDayParser,
mkDurationParser,
mkGregorianDayParser,
mkTimeParser,
) where
import Control.Applicative (many, optional, (<|>))
import Control.Monad (replicateM)
import Control.Monad.Combinators (choice, some)
import Data.Foldable (traverse_)
import Data.Functor (($>))
import Data.Maybe (fromMaybe)
import Data.Time (
Day,
DayOfWeek (..),
NominalDiffTime,
TimeOfDay (..),
defaultTimeLocale,
makeTimeOfDayValid,
parseTimeM,
)
import Text.Megaparsec (MonadParsec, Token, anySingle, try)
import Text.Printf (printf)
-- | Representation of a parser result with either a number of days relative to
-- the current day, or a 'DayOfWeek'.
--
-- @since 0.3.0.0
type DayResult = Either Int DayOfWeek
-- | Build a parser for a tuple containing a day or not, and a 'TimeOfDay',
-- separated with a space.
--
-- @since 0.3.0.0
mkDateParser
:: MonadParsec e s m
=> m ()
-- ^ Non-empty space parser.
-> m DayResult
-- ^ Day parser.
-> m TimeOfDay
-- ^ Time parser.
-> m (Maybe DayResult, TimeOfDay)
mkDateParser space1 day time = (,) <$> optional (try (day <* space1)) <*> time
-- | Build a case-insensitive day parser, see 'Text.Megaparsec.Time.Char.dayParser'.
--
-- @since 0.3.0.0
mkDayParser
:: MonadParsec e s m
=> (Token s -> m (Token s))
-- ^ Case-insensitive single-token parser.
-> (Char -> Token s)
-- ^ Convert a literal 'Char' to a token.
-> m (Token s)
-- ^ Digit parser.
-> (Token s -> Char)
-- ^ Convert a token to a 'Char'.
-> m DayResult
mkDayParser char' fromChar digitChar toChar =
choice
[ Right <$> longDay
, Right <$> shortDay
, Left <$> (str "yesterday" $> -1)
, Left <$> (str "tomorrow" $> 1)
, Right <$> absoluteDay
, Left <$> relativeDay
]
where
str = try . traverse_ (char' . fromChar)
shortDay = choice $ map (dayString (take 3 . show)) weekDays
longDay = choice $ map (dayString show) weekDays
dayString f d = str (f d) $> d
weekDays = [Monday .. Friday]
sign = (str "-" $> negate) <|> (str "+" $> id)
digits = map toChar <$> some digitChar
absoluteDay = toEnum . read <$> digits
relativeDay = ($) <$> sign <*> (read <$> digits)
-- | Build a 'NominalDiffTime' parser from hours, minutes and seconds parsers,
-- with all components being optional as long as one is present.
--
-- @since 0.3.0.0
mkDurationParser
:: MonadParsec e s m
=> m ()
-- ^ Space parser.
-> m NominalDiffTime
-- ^ Hours parser.
-> m NominalDiffTime
-- ^ Minutes parser.
-> m NominalDiffTime
-- ^ Seconds parser.
-> m NominalDiffTime
mkDurationParser space hoursParser minutesParser secondsParser =
try hours <|> try minutes <|> secondsParser
where
hours = do
h <- hoursParser <* space
m <- fromMaybe 0 <$> optional (try minutes)
s <- fromMaybe 0 <$> optional secondsParser
return (h + m + s)
minutes = do
m <- minutesParser <* space
s <- fromMaybe 0 <$> optional secondsParser
return (m + s)
-- | Build a Gregorian 'Day' parser from a @%F@ or @%d\/%m\/%Y@ format.
--
-- @since 0.3.0.0
mkGregorianDayParser
:: MonadFail m
=> MonadParsec e s m
=> (Token s -> Char)
-- ^ Convert a token to a 'Char'.
-> m Day
mkGregorianDayParser toChar = do
s <- map toChar <$> many anySingle
let parseTime = flip (parseTimeM False defaultTimeLocale) s
parseTime "%F" <|> parseTime "%d/%m/%Y"
-- | Build a 'TimeOfDay' parser from a string like @01:23@.
--
-- @since 0.3.0.0
mkTimeParser
:: MonadFail m
=> m t
-- ^ Digit parser.
-> m t
-- ^ Colon parser.
-> (t -> Char)
-- ^ Convert a token to a 'Char'.
-> m TimeOfDay
mkTimeParser digitChar colon toChar = do
h <- read . map toChar <$> replicateM 2 digitChar <* colon
m <- read . map toChar <$> replicateM 2 digitChar
maybe
(fail (printf "invalid hours or minutes %02d:%02d" h m))
pure
(makeTimeOfDayValid h m 0)