fuzzy-time-0.3.0.0: src/Data/FuzzyTime/Parser.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
module Data.FuzzyTime.Parser
( fuzzyLocalTimeP,
fuzzyTimeOfDayP,
atHourP,
atMinuteP,
atExactP,
hourSegmentP,
minuteSegmentP,
twoDigitsSegmentP,
fuzzyDayP,
fuzzyDayOfTheWeekP,
Parser,
)
where
import Control.Monad (guard, msum, void)
import Data.Char as Char (toLower)
import Data.Fixed (Pico)
import Data.FuzzyTime.Types (FuzzyDay (..), FuzzyLocalTime (..), FuzzyTimeOfDay (AtExact, AtHour, AtMinute, Evening, HoursDiff, Midnight, MinutesDiff, Morning, Noon, SecondsDiff))
import Data.List (find)
import Data.Maybe (fromMaybe, maybeToList)
import Data.Text (Text)
import Data.Time (DayOfWeek (..), TimeOfDay (TimeOfDay), defaultTimeLocale, parseTimeM)
import Data.Tree (Forest, Tree (Node), rootLabel, subForest)
import Data.Validity (isValid)
import Data.Void (Void)
import Data.Word (Word8)
import Text.Megaparsec (Parsec, empty, eof, label, oneOf, optional, some, try, (<|>))
import Text.Megaparsec.Char as Char (char, digitChar, letterChar, space1, string)
import Text.Megaparsec.Char.Lexer as Lexer (decimal)
import Text.Read (readMaybe)
type Parser = Parsec Void Text
fuzzyLocalTimeP :: Parser FuzzyLocalTime
fuzzyLocalTimeP =
label "FuzzyLocalTime" $
choice''
[ do
a <- fuzzyDayP
space1
b <- fuzzyTimeOfDayP
pure $ FuzzyLocalTimeBoth a b,
FuzzyLocalTimeDay <$> fuzzyDayP,
FuzzyLocalTimeTimeOfDay <$> fuzzyTimeOfDayP
]
fuzzyTimeOfDayP :: Parser FuzzyTimeOfDay
fuzzyTimeOfDayP =
label "FuzzyTimeOfDay" $
choice'
[ recTreeParser
[ ("midnight", Midnight),
("midday", Noon),
("noon", Noon),
("morning", Morning),
("evening", Evening)
],
atExactP,
atMinuteP,
atHourP,
diffP
]
atHourP :: Parser FuzzyTimeOfDay
atHourP =
label "AtHour" $ do
h <- hourSegmentP
pure $ AtHour h
atMinuteP :: Parser FuzzyTimeOfDay
atMinuteP =
label "AtMinute" $ do
h <- hourSegmentP
m <- minuteSegmentP
pure $ AtMinute h m
atExactP :: Parser FuzzyTimeOfDay
atExactP =
label "AtExact" $ do
h <- hourSegmentP
m <- minuteSegmentP
void $ char ':'
s <- readSimplePico
pure $ AtExact $ TimeOfDay h m s
readSimplePico :: Parser Pico
readSimplePico = do
let d = oneOf ['0' .. '9']
beforeDot <- some d :: Parser String
afterDot <-
optional $ do
dot <- char '.'
r <- some d
pure $ dot : r
pure $ read $ beforeDot <> fromMaybe "" afterDot
diffP :: Parser FuzzyTimeOfDay
diffP =
label "Diff" $ do
n <- signed' decimal
mc <- optional $ choice' [char 'h', char 'm', char 's']
f <-
case mc of
Nothing -> pure HoursDiff
Just 'h' -> pure HoursDiff
Just 'm' -> pure MinutesDiff
Just 's' -> pure (\i -> SecondsDiff $ fromIntegral i)
_ -> fail "should not happen."
pure $ f n
hourSegmentP :: Parser Int
hourSegmentP =
label "hour segment" $ do
h <- twoDigitsSegmentP
guard $ h >= 0 && h < 24
void $ optional $ char ':'
pure h
minuteSegmentP :: Parser Int
minuteSegmentP =
label "minute segment" $ do
m <- twoDigitsSegmentP
guard $ m >= 0 && m < 60
pure m
twoDigitsSegmentP :: (Num a, Read a) => Parser a
twoDigitsSegmentP =
label "two digit segment" $ do
d1 <- digit
md2 <- optional digit
pure $
case md2 of
Nothing -> d1
Just d2 -> 10 * d1 + d2
digit :: (Read a) => Parser a
digit =
label "digit" $ do
let l = ['0' .. '9']
c <- oneOf l
case readMaybe [c] of
Nothing -> fail "Shouldn't happen."
Just d -> pure d
-- | Can handle:
--
-- - yesterday
-- - now
-- - today
-- - tomorrow
-- - "%Y-%m-%d"
--
-- and all non-ambiguous prefixes
fuzzyDayP :: Parser FuzzyDay
fuzzyDayP =
label "FuzzyDay" $
choice'
[ recTreeParser
[("yesterday", Yesterday), ("now", Now), ("today", Today), ("tomorrow", Tomorrow)],
fmap ExactDay (some (digitChar <|> char '-') >>= parseTimeM True defaultTimeLocale "%Y-%m-%d"),
dayInMonthP,
dayOfTheMonthP,
fuzzyDayOfTheWeekP,
diffDayP
]
dayOfTheMonthP :: Parser FuzzyDay
dayOfTheMonthP = do
dayNo <- twoDigitsSegmentP
let v = OnlyDay dayNo
guard $ isValid v
pure v
dayInMonthP :: Parser FuzzyDay
dayInMonthP = do
m <-
choice'
[ do
m <- twoDigitsSegmentP
guard (m >= 1)
guard (m <= 12)
pure m,
namedMonthP
]
void $ string "-"
d <- twoDigitsSegmentP
let v = DayInMonth m d
guard $ isValid v
pure v
namedMonthP :: Parser Word8
namedMonthP =
recTreeParser
[ ("january", 1),
("february", 2),
("march", 3),
("april", 4),
("may", 5),
("june", 6),
("july", 7),
("august", 8),
("september", 9),
("october", 10),
("november", 11),
("december", 12)
]
diffDayP :: Parser FuzzyDay
diffDayP = do
d <- signed' decimal
mc <- optional $ oneOf ['d', 'w', 'm']
let f =
case mc of
Nothing -> DiffDays
Just 'd' -> DiffDays
Just 'w' -> DiffWeeks
Just 'm' -> DiffMonths
_ -> DiffDays -- Should not happen.
pure $ f d
fuzzyDayOfTheWeekP :: Parser FuzzyDay
fuzzyDayOfTheWeekP = do
dow <- dayOfTheWeekP
mExtraDiff <- optional $ signed' decimal
pure $ DayOfTheWeek dow (fromMaybe 0 mExtraDiff)
-- | Can handle:
--
-- - monday
-- - tuesday
-- - wednesday
-- - thursday
-- - friday
-- - saturday
-- - sunday
--
-- and all non-ambiguous prefixes
dayOfTheWeekP :: Parser DayOfWeek
dayOfTheWeekP =
recTreeParser
[ ("monday", Monday),
("tuesday", Tuesday),
("wednesday", Wednesday),
("thursday", Thursday),
("friday", Friday),
("saturday", Saturday),
("sunday", Sunday)
]
recTreeParser :: [(String, a)] -> Parser a
recTreeParser tups = do
let pf = makeParseForest tups
s <- some letterChar
case lookupInParseForest s pf of
Nothing ->
fail $ "Could not parse any of these recursively unambiguously: " ++ show (map fst tups)
Just f -> pure f
lookupInParseForest :: [Char] -> Forest (Char, Maybe a) -> Maybe a
lookupInParseForest = gof
where
gof :: [Char] -> Forest (Char, Maybe a) -> Maybe a
gof cs = msum . map (got cs)
got :: [Char] -> Tree (Char, Maybe a) -> Maybe a
got [] _ = Nothing
got (c : cs) Node {..} =
let (tc, tma) = rootLabel
in if Char.toLower tc == Char.toLower c
then case cs of
[] -> tma
_ -> gof cs subForest
else Nothing
makeParseForest :: (Eq c) => [([c], a)] -> Forest (c, Maybe a)
makeParseForest = foldl insertf []
where
insertf :: (Eq c) => Forest (c, Maybe a) -> ([c], a) -> Forest (c, Maybe a)
insertf for ([], _) = for
insertf for (c : cs, a) =
case find ((== c) . fst . rootLabel) for of
Nothing ->
let got [] = Nothing
got (c_ : cs_) = Just $ Node (c_, Just a) $ maybeToList $ got cs_
in case got (c : cs) of
Nothing -> for -- Should not happen, but is fine
Just t -> t : for
Just n ->
flip map for $ \t ->
let (tc, _) = rootLabel t
in if tc == c
then n {rootLabel = (tc, Nothing), subForest = insertf (subForest n) (cs, a)}
else t
signed' :: (Num a) => Parser a -> Parser a
signed' p = sign <*> p
where
sign = (id <$ char '+') <|> (negate <$ char '-')
choice' :: [Parser a] -> Parser a
choice' [] = empty
choice' [x] = x
choice' (a : as) = try a <|> choice' as
choice'' :: [Parser a] -> Parser a
choice'' = choice' . map (<* eof)