diff options
| author | Gregor Kleen <aethoago@141.li> | 2017-02-15 15:10:51 +0100 |
|---|---|---|
| committer | Gregor Kleen <aethoago@141.li> | 2017-02-15 15:10:51 +0100 |
| commit | ee09f262f9b8c7c6a4042071cdfff3e22adbef86 (patch) | |
| tree | baeb1d9ee726881d25e0762c21f750850efb37f2 /lib | |
| parent | 8a24b41b333bce25e698d2e4b87f4b4f6548772c (diff) | |
| download | postdelay-ee09f262f9b8c7c6a4042071cdfff3e22adbef86.tar postdelay-ee09f262f9b8c7c6a4042071cdfff3e22adbef86.tar.gz postdelay-ee09f262f9b8c7c6a4042071cdfff3e22adbef86.tar.bz2 postdelay-ee09f262f9b8c7c6a4042071cdfff3e22adbef86.tar.xz postdelay-ee09f262f9b8c7c6a4042071cdfff3e22adbef86.zip | |
Establish framework
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/Postdelay/Scan.hs | 39 | ||||
| -rw-r--r-- | lib/Postdelay/TimeSpec.hs | 336 | ||||
| -rw-r--r-- | lib/Postdelay/TimeSpec/Units.hs | 111 | ||||
| -rw-r--r-- | lib/Postdelay/TimeSpec/Utils.hs | 35 | ||||
| -rw-r--r-- | lib/Postdelay/Utils.hs | 21 |
5 files changed, 183 insertions, 359 deletions
diff --git a/lib/Postdelay/Scan.hs b/lib/Postdelay/Scan.hs index 2f0a78a..888a237 100644 --- a/lib/Postdelay/Scan.hs +++ b/lib/Postdelay/Scan.hs | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | {-# LANGUAGE OverloadedStrings, ScopedTypeVariables, FlexibleContexts, RecordWildCards, ViewPatterns, TupleSections #-} | 1 | {-# LANGUAGE OverloadedStrings, ScopedTypeVariables, FlexibleContexts, RecordWildCards, ViewPatterns, TupleSections, RankNTypes #-} |
| 2 | 2 | ||
| 3 | module Postdelay.Scan | 3 | module Postdelay.Scan |
| 4 | ( scan | 4 | ( scan |
| @@ -7,20 +7,18 @@ module Postdelay.Scan | |||
| 7 | ) where | 7 | ) where |
| 8 | 8 | ||
| 9 | import Postdelay.Types | 9 | import Postdelay.Types |
| 10 | import Postdelay.Utils | ||
| 11 | import Postdelay.TimeSpec | 10 | import Postdelay.TimeSpec |
| 12 | 11 | ||
| 13 | import Control.Monad | 12 | import Control.Monad |
| 14 | import Control.Monad.IO.Class | 13 | import Control.Monad.IO.Class |
| 15 | import Control.Monad.Except | 14 | import Control.Monad.Catch |
| 16 | import Control.Monad.Reader | 15 | import Control.Monad.Reader |
| 17 | import Control.Monad.List | 16 | import Control.Monad.List |
| 18 | import Control.Exception.Base | 17 | import Control.Exception.Base |
| 18 | import Control.Lens | ||
| 19 | 19 | ||
| 20 | import Text.Parsec.Char | 20 | import qualified Text.Parsec as P |
| 21 | import Text.Parsec.Prim | 21 | import Text.Megaparsec |
| 22 | import Text.Parsec.Combinator | ||
| 23 | import Text.Parsec.Error (ParseError(..)) | ||
| 24 | import Text.ParserCombinators.Parsec.Rfc2822 | 22 | import Text.ParserCombinators.Parsec.Rfc2822 |
| 25 | import Codec.MIME.Decode (decodeWords) | 23 | import Codec.MIME.Decode (decodeWords) |
| 26 | 24 | ||
| @@ -40,17 +38,20 @@ import System.Time (CalendarTime(..)) | |||
| 40 | 38 | ||
| 41 | import Debug.Trace | 39 | import Debug.Trace |
| 42 | 40 | ||
| 41 | instance Exception P.ParseError | ||
| 43 | 42 | ||
| 44 | scan :: (MonadIO m, MonadError ParseError m) => String -> m (Maybe Delay) | ||
| 45 | scan = fmap getOption . extractDelay <=< either throwError return . parse message "" | ||
| 46 | 43 | ||
| 47 | extractDelay :: forall m. (MonadIO m, MonadError ParseError m) => Message -> m (Option Delay) | 44 | scan :: (MonadIO m, MonadThrow m) => String -> m (Maybe Delay) |
| 45 | scan = fmap getOption . extractDelay <=< either throwM return . P.parse message "" | ||
| 46 | |||
| 47 | extractDelay :: forall m. (MonadIO m, MonadThrow m) => Message -> m (Option Delay) | ||
| 48 | extractDelay (Message headers _) = do | 48 | extractDelay (Message headers _) = do |
| 49 | zones <- zoneHeaders | 49 | tz <- foldr' (flip (<>)) (Left dateTz) <$> zoneHeaders |
| 50 | let (baseTime, dateTz) = maximumBy (comparing fst) dateHeaders | 50 | let apply f = Until (baseTime & localT tz %~ appEndo f) |
| 51 | tz = foldr' (flip (<>)) (Left dateTz) zones | 51 | fmap apply . foldMap pure <$> mapM parseDelay delayHeaders |
| 52 | foldMap pure <$> mapM (flip runReaderT TimeCtx{..} . parseDelay) delayHeaders | ||
| 53 | where | 52 | where |
| 53 | (baseTime, dateTz) = maximumBy (comparing fst) dateHeaders | ||
| 54 | |||
| 54 | delayHeaders :: [Field] | 55 | delayHeaders :: [Field] |
| 55 | delayHeaders = do | 56 | delayHeaders = do |
| 56 | (OptionalField field content) <- headers | 57 | (OptionalField field content) <- headers |
| @@ -67,7 +68,11 @@ extractDelay (Message headers _) = do | |||
| 67 | zoneHeaders = runListT $ do | 68 | zoneHeaders = runListT $ do |
| 68 | (OptionalField field content) <- ListT $ return headers | 69 | (OptionalField field content) <- ListT $ return headers |
| 69 | guard $ CI.mk field == "X-Timezone" | 70 | guard $ CI.mk field == "X-Timezone" |
| 70 | Right r <- runParserT (spaces *> pTimeZone <* spaces <* eof) () field content | 71 | Right r <- runParserT (spaceConsumer *> lexeme pTimeZone <* eof) field content |
| 71 | return r | 72 | return r |
| 72 | parseDelay :: Field -> ReaderT TimeCtx m Delay | 73 | parseDelay :: Field -> m (Endo LocalTime) |
| 73 | parseDelay (OptionalField field content) = either throwError return =<< runParserT (Until <$> pTimeSpec) () field content | 74 | parseDelay (OptionalField field content) = either throwM return =<< runParserT (spaceConsumer *> lexeme pTimeSpec <* eof) field content |
| 75 | |||
| 76 | localT :: Either TimeZone TZ -> Iso' UTCTime LocalTime | ||
| 77 | localT (Left tz) = iso (utcToLocalTime tz) (localTimeToUTC tz) | ||
| 78 | localT (Right tz) = iso (utcToLocalTimeTZ tz) (localTimeToUTCTZ tz) | ||
diff --git a/lib/Postdelay/TimeSpec.hs b/lib/Postdelay/TimeSpec.hs index 676dabf..5c41180 100644 --- a/lib/Postdelay/TimeSpec.hs +++ b/lib/Postdelay/TimeSpec.hs | |||
| @@ -1,335 +1,29 @@ | |||
| 1 | {-# LANGUAGE FlexibleContexts, ScopedTypeVariables, ViewPatterns, ConstraintKinds, TupleSections, RecordWildCards #-} | 1 | {-# LANGUAGE FlexibleContexts, ScopedTypeVariables, ViewPatterns, ConstraintKinds, TupleSections, RecordWildCards, TypeFamilies #-} |
| 2 | 2 | ||
| 3 | module Postdelay.TimeSpec | 3 | module Postdelay.TimeSpec |
| 4 | ( pTimeSpec | 4 | ( pTimeSpec |
| 5 | , pTimeZone | 5 | , pTimeZone |
| 6 | , TimeCtx(..) | 6 | |
| 7 | , spaceConsumer, lexeme | ||
| 7 | ) where | 8 | ) where |
| 8 | 9 | ||
| 9 | import Control.Monad | 10 | import Postdelay.TimeSpec.Utils |
| 10 | import Control.Monad.IO.Class | 11 | import Postdelay.TimeSpec.Units |
| 11 | import Control.Monad.Reader.Class | ||
| 12 | import Control.Monad.Error.Class | ||
| 13 | 12 | ||
| 14 | import Text.Parsec.Char hiding (digit) | 13 | import Text.Megaparsec |
| 15 | import qualified Text.Parsec.Char as Parsec (digit) | 14 | |
| 16 | import Text.Parsec.Prim | 15 | import Control.Monad.IO.Class |
| 17 | import Text.Parsec.Combinator | 16 | import Control.Applicative |
| 18 | import Text.Parsec.Error (ParseError(..)) | ||
| 19 | import Text.Read (readMaybe) | ||
| 20 | 17 | ||
| 21 | import Data.CaseInsensitive (CI) | 18 | import Data.Semigroup |
| 22 | import qualified Data.CaseInsensitive as CI | 19 | import Data.Monoid (Endo(..)) |
| 23 | 20 | ||
| 24 | import Data.Time | 21 | import Data.Time |
| 25 | import Data.Time.Calendar.WeekDate | ||
| 26 | import Data.Time.Zones | 22 | import Data.Time.Zones |
| 27 | import Data.Function | ||
| 28 | import Data.Maybe | ||
| 29 | import Data.Foldable | ||
| 30 | import Data.Ord | ||
| 31 | import Data.List | ||
| 32 | import Data.Tuple | ||
| 33 | import Data.Bool | ||
| 34 | |||
| 35 | import Control.Exception (IOException) | ||
| 36 | |||
| 37 | import Debug.Trace | ||
| 38 | |||
| 39 | |||
| 40 | type MonadTP m = (MonadIO m, MonadReader TimeCtx m) | ||
| 41 | |||
| 42 | data TimeCtx = TimeCtx | ||
| 43 | { baseTime :: UTCTime | ||
| 44 | , tz :: Either TimeZone TZ | ||
| 45 | } | ||
| 46 | |||
| 47 | |||
| 48 | spaced :: Stream s m Char => ParsecT s u m a -> ParsecT s u m a | ||
| 49 | spaced p = spaces *> p <* spaces | ||
| 50 | |||
| 51 | string' :: Stream s m Char => String -> ParsecT s u m String | ||
| 52 | string' = mapM $ satisfy . ((==) `on` CI.mk) | ||
| 53 | |||
| 54 | choice' :: (Stream s m t, Foldable f) => f (ParsecT s u m a) -> ParsecT s u m a | ||
| 55 | choice' (toList -> f) | ||
| 56 | | [p] <- f = p | ||
| 57 | | (p:ps) <- f = try p <|> choice' ps | ||
| 58 | | otherwise = mzero | ||
| 59 | |||
| 60 | strChoice' :: Stream s m Char => [String] -> ParsecT s u m String | ||
| 61 | strChoice' = choice' . map string' . sortBy (comparing $ Down . length) | ||
| 62 | |||
| 63 | natural :: (Stream s m Char, Num a) => ParsecT s u m a | ||
| 64 | natural = foldl' (\init last -> init * 10 + last) 0 <$> many1 digit | ||
| 65 | |||
| 66 | decimal :: (Stream s m Char, Num a, Fractional a) => ParsecT s u m a | ||
| 67 | decimal = do | ||
| 68 | w <- foldl' (\init last -> init * 10 + last) 0 <$> many1 digit | ||
| 69 | f <- option 0 $ do | ||
| 70 | char '.' | ||
| 71 | foldr' (\head tail -> head + tail / 10) 0 <$> many1 digit | ||
| 72 | return $ w + f / 10 | ||
| 73 | |||
| 74 | digit :: (Stream s m Char, Num a) => ParsecT s u m a | ||
| 75 | digit = fromIntegral . (\c -> fromEnum c - fromEnum '0') <$> Parsec.digit | ||
| 76 | |||
| 77 | ensure :: MonadPlus m => (a -> Bool) -> a -> m a | ||
| 78 | ensure p x = bool (const mzero) return (p x) $ x | ||
| 79 | |||
| 80 | |||
| 81 | pTimeSpec :: MonadTP m => ParsecT String () m UTCTime | ||
| 82 | pTimeSpec = choice' [ flip addUTCTime <$> spaced pSpecBase <*> spaced pSpecOffset <?> "Absolute time + offset" | ||
| 83 | , flip addUTCTime <$> asks baseTime <*> spaced pSpecOffset <?> "Time offset" | ||
| 84 | , spaced pSpecBase <?> "Absolute time" | ||
| 85 | ] | ||
| 86 | <* eof <?> "Time specification" | ||
| 87 | |||
| 88 | pSpecBase :: forall m. MonadTP m => ParsecT String () m UTCTime | ||
| 89 | pSpecBase = choice' | ||
| 90 | [ utcTime <$> spaced pDate <*> spaced pTime | ||
| 91 | , flip utcTime <$> spaced pTime <*> spaced pDate | ||
| 92 | , do | ||
| 93 | proto@(UTCTime{..}) <- utcTime <$> (utctDay <$> asks baseTime) <*> spaced pTime | ||
| 94 | now <- asks baseTime | ||
| 95 | return $ if proto < now | ||
| 96 | then UTCTime (succ utctDay) utctDayTime | ||
| 97 | else proto | ||
| 98 | , utcTime <$> spaced pDate <*> ((dayFractionToTimeOfDay 0, ) <$> asks tz) | ||
| 99 | , spaced (string' "now") *> asks baseTime | ||
| 100 | ] <?> "Base specification" | ||
| 101 | where | ||
| 102 | utcTime :: Day -> (TimeOfDay, Either TimeZone TZ) -> UTCTime | ||
| 103 | utcTime d (t, Right tz) = localTimeToUTCTZ tz (LocalTime d t) | ||
| 104 | utcTime d (t, Left tz) = localTimeToUTC tz (LocalTime d t) | ||
| 105 | |||
| 106 | pSpecOffset :: MonadTP m => ParsecT String () m NominalDiffTime | ||
| 107 | pSpecOffset = (+) <$> pSpecOffset' <*> option 0 (try (many $ space <|> char ',' <|> char ';') >> pSpecOffset) | ||
| 108 | where | ||
| 109 | pSpecOffset' = option id (spaced pSign) <*> ((*) <$> spaced pNumber <*> spaced pSpecOffsetConst) <?> "Time offset" | ||
| 110 | pNumber = fromInteger <$> natural <?> "Offset multiplier" | ||
| 111 | |||
| 112 | pSign :: MonadTP m => ParsecT String () m (NominalDiffTime -> NominalDiffTime) | ||
| 113 | pSign = choice [ id <$ char '+' | ||
| 114 | , negate <$ char '-' | ||
| 115 | ] <?> "Offset sign" | ||
| 116 | |||
| 117 | pSpecOffsetConst :: MonadTP m => ParsecT String () m NominalDiffTime | ||
| 118 | pSpecOffsetConst = choice' [ 1e-12 <$ strChoice' [ "ps" | ||
| 119 | , "picosecond" | ||
| 120 | , "picoseconds" | ||
| 121 | ] | ||
| 122 | , 1e-9 <$ strChoice' [ "ns" | ||
| 123 | , "nanosecond" | ||
| 124 | , "nanoseconds" | ||
| 125 | ] | ||
| 126 | , 1e-6 <$ strChoice' [ "us", "µs" | ||
| 127 | , "microsecond" | ||
| 128 | , "microseconds" | ||
| 129 | ] | ||
| 130 | , 1e-3 <$ strChoice' [ "ms" | ||
| 131 | , "millisecond" | ||
| 132 | , "milliseconds" | ||
| 133 | ] | ||
| 134 | , 1e-2 <$ strChoice' [ "ds" | ||
| 135 | , "decisecond" | ||
| 136 | , "deciseconds" | ||
| 137 | ] | ||
| 138 | , 1e-1 <$ strChoice' [ "cs" | ||
| 139 | , "centisecond" | ||
| 140 | , "centiseconds" | ||
| 141 | ] | ||
| 142 | , 1 <$ strChoice' [ "s" | ||
| 143 | , "second" | ||
| 144 | , "seconds" | ||
| 145 | ] | ||
| 146 | , 60 <$ strChoice' [ "min" | ||
| 147 | , "minute" | ||
| 148 | , "minutes" | ||
| 149 | ] | ||
| 150 | , 3600 <$ strChoice' [ "h" | ||
| 151 | , "hour" | ||
| 152 | , "hours" | ||
| 153 | ] | ||
| 154 | , 24 * 3600 <$ strChoice' [ "d" | ||
| 155 | , "day" | ||
| 156 | , "days" | ||
| 157 | ] | ||
| 158 | , 7 * 24 * 3600 <$ strChoice' [ "week" | ||
| 159 | , "weeks" | ||
| 160 | ] | ||
| 161 | , 30 * 24 * 3600 <$ strChoice' [ "month" | ||
| 162 | , "months" | ||
| 163 | ] | ||
| 164 | , 365 * 24 * 3600 <$ strChoice' [ "year" | ||
| 165 | , "years" | ||
| 166 | ] | ||
| 167 | ] <?> "Offset unit" | ||
| 168 | |||
| 169 | pDate :: MonadTP m => ParsecT String () m Day | ||
| 170 | pDate = choice' [ do | ||
| 171 | (m, d) <- choice' [ do | ||
| 172 | m <- spaced pMonthName | ||
| 173 | d <- spaced natural | ||
| 174 | optional . spaced $ char ',' | ||
| 175 | return (m, d) | ||
| 176 | , fmap swap . (,) <$> spaced natural <*> spaced pMonthName | ||
| 177 | ] | ||
| 178 | y <- spaced natural | ||
| 179 | fromGregorian' y m d | ||
| 180 | , do | ||
| 181 | now <- asks baseTime | ||
| 182 | (m, d) <- choice' [ (,) <$> spaced pMonthName <*> spaced natural | ||
| 183 | , fmap swap . (,) <$> spaced natural <*> spaced pMonthName | ||
| 184 | ] | ||
| 185 | let | ||
| 186 | (y, _, _) = toGregorian $ utctDay now | ||
| 187 | proto <- fromGregorian' y m d | ||
| 188 | if proto < utctDay now | ||
| 189 | then fromGregorian' (y + 1) m d | ||
| 190 | else return proto | ||
| 191 | , do | ||
| 192 | now <- asks baseTime | ||
| 193 | optional $ spaces *> pNext <* many1 space | ||
| 194 | d <- spaced pWeekdayName | ||
| 195 | let | ||
| 196 | (y, w, _) = toWeekDate $ utctDay now | ||
| 197 | proto <- fromWeekDate' y w d | ||
| 198 | if proto < utctDay now | ||
| 199 | then maybe (fromWeekDate' (y + 1) 1 d) return $ fromWeekDateValid y (w + 1) d | ||
| 200 | else return proto | ||
| 201 | , do | ||
| 202 | spaced $ string' "today" | ||
| 203 | utctDay <$> asks baseTime | ||
| 204 | , do | ||
| 205 | spaced $ string' "tomorrow" | ||
| 206 | succ . utctDay <$> asks baseTime | ||
| 207 | , do | ||
| 208 | y <- natural | ||
| 209 | char '-' | ||
| 210 | m <- natural | ||
| 211 | char '-' | ||
| 212 | d <- natural | ||
| 213 | fromGregorian' y m d | ||
| 214 | , do | ||
| 215 | d <- natural | ||
| 216 | char '.' | ||
| 217 | m <- natural | ||
| 218 | char '.' | ||
| 219 | y <- natural | ||
| 220 | fromGregorian' y m d | ||
| 221 | , do | ||
| 222 | m <- natural | ||
| 223 | char '/' | ||
| 224 | d <- natural | ||
| 225 | char '/' | ||
| 226 | y <- natural | ||
| 227 | fromGregorian' y m d | ||
| 228 | , do | ||
| 229 | ds <- many1 digit | ||
| 230 | when (length ds < 5) $ fail "Insufficient digits to interpret as concatenated date" | ||
| 231 | let | ||
| 232 | d2 : d1 : m2 : m1 : ys = reverse ds | ||
| 233 | d = 10 * d1 + d2 | ||
| 234 | m = 10 * m1 + m2 | ||
| 235 | y = foldl' (\init last -> init * 10 + last) 0 . map fromIntegral $ reverse ys | ||
| 236 | fromGregorian' y m d | ||
| 237 | , do | ||
| 238 | pNext | ||
| 239 | many1 space | ||
| 240 | fmap utctDay . addUTCTime <$> pSpecOffsetConst <*> asks baseTime | ||
| 241 | ] <?> "Day specification" | ||
| 242 | where | ||
| 243 | fromGregorian' y m d = maybe (fail "Invalid gregorian date") return $ fromGregorianValid y m d | ||
| 244 | fromWeekDate' y w d = maybe (fail "Invalid iso8601 date") return $ fromWeekDateValid y w d | ||
| 245 | pNext = string' "next" | ||
| 246 | |||
| 247 | pMonthName :: MonadTP m => ParsecT String () m Int | ||
| 248 | pMonthName = choice' (zipWith (<$) [1..] [ strChoice' [ "January", "Jan" ] | ||
| 249 | , strChoice' [ "Febuary", "Feb" ] | ||
| 250 | , strChoice' [ "March", "Mar" ] | ||
| 251 | , strChoice' [ "April", "Apr" ] | ||
| 252 | , strChoice' [ "May" ] | ||
| 253 | , strChoice' [ "June", "Jun" ] | ||
| 254 | , strChoice' [ "July", "Jul" ] | ||
| 255 | , strChoice' [ "August", "Aug" ] | ||
| 256 | , strChoice' [ "September", "Sep" ] | ||
| 257 | , strChoice' [ "October", "Oct" ] | ||
| 258 | , strChoice' [ "November", "Nov" ] | ||
| 259 | , strChoice' [ "December", "Dec" ] | ||
| 260 | ]) <?> "Month name" | ||
| 261 | |||
| 262 | pWeekdayName :: MonadTP m => ParsecT String () m Int | ||
| 263 | pWeekdayName = choice' (zipWith (<$) [1..] [ strChoice' [ "Monday", "Mon" ] | ||
| 264 | , strChoice' [ "Tuesday", "Tue" ] | ||
| 265 | , strChoice' [ "Wednesday", "Wed" ] | ||
| 266 | , strChoice' [ "Thursday", "Thu" ] | ||
| 267 | , strChoice' [ "Friday", "Fri" ] | ||
| 268 | , strChoice' [ "Saturday", "Sat" ] | ||
| 269 | , strChoice' [ "Sunday", "Sun" ] | ||
| 270 | ]) | ||
| 271 | |||
| 272 | pTime :: MonadTP m => ParsecT String () m (TimeOfDay, Either TimeZone TZ) | ||
| 273 | pTime = choice' [ (,) <$> spaced pTimeBase <*> spaced pTimeZone | ||
| 274 | , (,) <$> spaced pTimeBase <*> asks tz | ||
| 275 | ] <?> "Time of day and timezone specification" | ||
| 276 | |||
| 277 | data AMPM = AM | PM | ||
| 278 | deriving (Eq, Ord, Enum) | ||
| 279 | 23 | ||
| 280 | pTimeBase :: MonadTP m => ParsecT String () m TimeOfDay | ||
| 281 | pTimeBase = choice' [ do | ||
| 282 | h <- pHour12 | ||
| 283 | m <- option 0 $ char ':' >> pMinute | ||
| 284 | s <- option 0 $ char ':' >> pSecond | ||
| 285 | amPM <- spaced pAMPM | ||
| 286 | let h' = h + fromEnum amPM * 12 | ||
| 287 | return $ TimeOfDay h' m s | ||
| 288 | , do | ||
| 289 | h <- pHour | ||
| 290 | m <- option 0 $ char ':' >> pMinute | ||
| 291 | s <- option 0 $ char ':' >> pSecond | ||
| 292 | return $ TimeOfDay h m s | ||
| 293 | , do | ||
| 294 | h <- ensure (<= 24) =<< (\d u -> 10 * d + u) <$> digit <*> digit | ||
| 295 | m <- option 0 $ ensure (< 60) =<< (\d u -> 10 * d + u) <$> digit <*> digit | ||
| 296 | s <- option 0 $ pSecond | ||
| 297 | return $ TimeOfDay h m s | ||
| 298 | , TimeOfDay 0 0 0 <$ string' "midnight" | ||
| 299 | , TimeOfDay 12 0 0 <$ string' "noon" | ||
| 300 | , TimeOfDay 16 0 0 <$ string' "teatime" | ||
| 301 | ] <?> "Time of day specification" | ||
| 302 | where | ||
| 303 | pAMPM = choice [ AM <$ string' "AM" | ||
| 304 | , PM <$ string' "PM" | ||
| 305 | ] | ||
| 306 | pHour12 = (`rem` 12) <$> (ensure (<= 12) =<< natural) | ||
| 307 | 24 | ||
| 308 | pHour = (`rem` 24) <$> (ensure (<= 24) =<< natural) | 25 | pTimeSpec :: StringParser s m => m (Endo LocalTime) |
| 309 | pMinute = ensure (< 60) =<< natural | 26 | pTimeSpec = empty |
| 310 | pSecond = decimal | ||
| 311 | 27 | ||
| 312 | pTimeZone :: MonadIO m => ParsecT String () m (Either TimeZone TZ) | 28 | pTimeZone :: (StringParser s m, MonadIO m) => m (Either TimeZone TZ) |
| 313 | pTimeZone = choice' [ do | 29 | pTimeZone = empty |
| 314 | sgn <- choice [ id <$ char '+' | ||
| 315 | , negate <$ char '-' | ||
| 316 | ] | ||
| 317 | hs <- (\d u -> 10 * d + u) <$> digit <*> digit | ||
| 318 | ms <- option 0 $ (\d u -> 10 * d + u) <$> digit <*> digit | ||
| 319 | return . Left . minutesToTimeZone $ hs * 60 + ms | ||
| 320 | , do | ||
| 321 | let | ||
| 322 | ident = (++) <$> many1 alphaNum <*> option "" ((:) <$> oneOf "_-/.+" <*> ident) | ||
| 323 | n <- ident | ||
| 324 | tz <- liftIO $ do | ||
| 325 | let | ||
| 326 | fbHandler :: IO a -> (IOException -> IO a) | ||
| 327 | fbHandler fb _ = fb | ||
| 328 | foldl (\fb a -> a `catchError` fbHandler fb) (return Nothing) | ||
| 329 | [ Just <$> loadSystemTZ n | ||
| 330 | , Just <$> loadTZFromDB n | ||
| 331 | ] | ||
| 332 | case tz of | ||
| 333 | Nothing -> fail $ "Could not resolve timezone: " ++ n | ||
| 334 | (Just tz) -> return $ Right tz | ||
| 335 | ] | ||
diff --git a/lib/Postdelay/TimeSpec/Units.hs b/lib/Postdelay/TimeSpec/Units.hs new file mode 100644 index 0000000..330997a --- /dev/null +++ b/lib/Postdelay/TimeSpec/Units.hs | |||
| @@ -0,0 +1,111 @@ | |||
| 1 | {-# LANGUAGE TemplateHaskell, TypeFamilies, GADTs, RankNTypes, DataKinds, ViewPatterns, ScopedTypeVariables #-} | ||
| 2 | |||
| 3 | module Postdelay.TimeSpec.Units | ||
| 4 | ( Time | ||
| 5 | |||
| 6 | , Second, Minute, Hour | ||
| 7 | , Day, Week, Month, Year | ||
| 8 | |||
| 9 | , timeLength | ||
| 10 | |||
| 11 | , module Data.Units.SI.Prefixes | ||
| 12 | ) where | ||
| 13 | |||
| 14 | import Postdelay.TimeSpec.Utils | ||
| 15 | |||
| 16 | import Control.Applicative | ||
| 17 | |||
| 18 | import Data.Metrology | ||
| 19 | import Data.Metrology.TH | ||
| 20 | import Data.Metrology.SI.Mono () | ||
| 21 | |||
| 22 | import Data.Units.SI | ||
| 23 | import Data.Units.SI.Prefixes | ||
| 24 | import Data.Units.SI.Parser | ||
| 25 | import qualified Data.Dimensions.SI as D | ||
| 26 | |||
| 27 | import Data.Foldable | ||
| 28 | import Data.Function | ||
| 29 | import Data.VectorSpace | ||
| 30 | |||
| 31 | import Data.Fixed (Fixed, HasResolution) | ||
| 32 | import qualified Data.Fixed as Fixed | ||
| 33 | |||
| 34 | |||
| 35 | import Text.Megaparsec | ||
| 36 | |||
| 37 | |||
| 38 | declareDerivedUnit "Day" [t| Hour |] 24 Nothing | ||
| 39 | declareDerivedUnit "Week" [t| Day |] 7 Nothing | ||
| 40 | declareDerivedUnit "Month" [t| Day |] 30 Nothing | ||
| 41 | declareDerivedUnit "Year" [t| Day |] 365.25 Nothing | ||
| 42 | |||
| 43 | type Time = MkQu_DLN D.Time DefaultLCSU Fixed.Pico | ||
| 44 | |||
| 45 | data Prefix where | ||
| 46 | Prefix :: forall p. (UnitPrefix p, Show p) => p -> Prefix | ||
| 47 | |||
| 48 | instance HasResolution p => AdditiveGroup (Fixed p) where | ||
| 49 | zeroV = 0 | ||
| 50 | (^+^) = (+) | ||
| 51 | negateV = negate | ||
| 52 | (^-^) = (-) | ||
| 53 | |||
| 54 | instance HasResolution p => VectorSpace (Fixed p) where | ||
| 55 | type Scalar (Fixed p) = Fixed p | ||
| 56 | (*^) = (*) | ||
| 57 | |||
| 58 | |||
| 59 | timeLength :: StringParser s m => m Time | ||
| 60 | timeLength = (*^) <$> lexeme rational <*> timeUnit | ||
| 61 | |||
| 62 | rational :: forall s m n. (StringParser s m, Fractional n, Num n) => m n | ||
| 63 | rational = combine <$> some digitChar <*> option [] (char '.' *> many digitChar) | ||
| 64 | where | ||
| 65 | combine :: [Char] -> [Char] -> n | ||
| 66 | combine (map asN -> whole) (map asN -> fractional) | ||
| 67 | = foldl' (\i l -> i * 10 + l) 0 whole + (foldr' (\h t -> h + t / 10) 0 fractional) / 10 | ||
| 68 | asN :: Char -> n | ||
| 69 | asN c = fromIntegral $ ((-) `on` fromEnum) c '0' | ||
| 70 | |||
| 71 | timeUnit :: StringParser s m => m Time | ||
| 72 | timeUnit = label "Unit of time" . choice $ | ||
| 73 | [ 1 % Second <$ choice [ string' "seconds" | ||
| 74 | , string' "second" | ||
| 75 | , string' "secs" | ||
| 76 | , string' "sec" | ||
| 77 | ] | ||
| 78 | , 1 % Minute <$ choice [ string' "minutes" | ||
| 79 | , string' "minute" | ||
| 80 | , string' "mins" | ||
| 81 | , string' "min" | ||
| 82 | ] | ||
| 83 | , 1 % Hour <$ choice [ string' "hours" | ||
| 84 | , string' "hour" | ||
| 85 | ] | ||
| 86 | , 1 % Day <$ choice [ string' "days" | ||
| 87 | , string' "day" | ||
| 88 | ] | ||
| 89 | , 1 % Week <$ choice [ string' "weeks" | ||
| 90 | , string' "week" | ||
| 91 | ] | ||
| 92 | , 1 % Month <$ choice [ string' "months" | ||
| 93 | , string' "month" | ||
| 94 | ] | ||
| 95 | , 1 % Year <$ choice [ string' "years" | ||
| 96 | , string' "year" | ||
| 97 | ] | ||
| 98 | ] ++ | ||
| 99 | [ (% Second) <$> option 1 siPrefix <* string "s" | ||
| 100 | , (% Hour) <$> option 1 siPrefix <* string "h" | ||
| 101 | , (% Day) <$> option 1 siPrefix <* string "d" | ||
| 102 | , (% Year) <$> option 1 siPrefix <* choice [ string "a", string "yr", string "yrs" ] | ||
| 103 | ] | ||
| 104 | |||
| 105 | siPrefix :: (StringParser s m, Fractional n) => m n | ||
| 106 | siPrefix = label "SI prefix" . choice $ map (\(Prefix p) -> multiplier p <$ string (show p)) | ||
| 107 | [ Prefix Deca, Prefix Hecto, Prefix Kilo, Prefix Mega, Prefix Giga | ||
| 108 | , Prefix Tera, Prefix Peta, Prefix Exa, Prefix Zetta, Prefix Yotta | ||
| 109 | , Prefix Deci, Prefix Centi, Prefix Milli, Prefix Micro, Prefix Nano | ||
| 110 | , Prefix Pico, Prefix Femto, Prefix Atto, Prefix Zepto, Prefix Yocto | ||
| 111 | ] | ||
diff --git a/lib/Postdelay/TimeSpec/Utils.hs b/lib/Postdelay/TimeSpec/Utils.hs new file mode 100644 index 0000000..e4ba732 --- /dev/null +++ b/lib/Postdelay/TimeSpec/Utils.hs | |||
| @@ -0,0 +1,35 @@ | |||
| 1 | {-# LANGUAGE RankNTypes, ConstraintKinds, TypeFamilies, FlexibleContexts, ScopedTypeVariables, ViewPatterns #-} | ||
| 2 | |||
| 3 | module Postdelay.TimeSpec.Utils where | ||
| 4 | |||
| 5 | import Control.Applicative | ||
| 6 | import Control.Monad | ||
| 7 | import Control.Lens | ||
| 8 | |||
| 9 | import Data.Time | ||
| 10 | import Data.Time.Zones | ||
| 11 | |||
| 12 | import Data.AdditiveGroup | ||
| 13 | |||
| 14 | import Text.Megaparsec | ||
| 15 | import Text.Megaparsec.Prim (MonadParsec) | ||
| 16 | import qualified Text.Megaparsec.Lexer as L | ||
| 17 | |||
| 18 | |||
| 19 | type StringParser s m = (MonadParsec Dec s m, Token s ~ Char) | ||
| 20 | |||
| 21 | |||
| 22 | spaceConsumer :: StringParser s m => m () | ||
| 23 | spaceConsumer = L.space (void spaceChar) empty empty | ||
| 24 | |||
| 25 | lexeme :: StringParser s m => m a -> m a | ||
| 26 | lexeme = L.lexeme spaceConsumer | ||
| 27 | |||
| 28 | signed, optSigned :: (StringParser s m, AdditiveGroup n) => m n -> m n | ||
| 29 | signed = (<*>) (lexeme sign) | ||
| 30 | optSigned = (<*>) (option id $ lexeme sign) | ||
| 31 | |||
| 32 | sign :: (StringParser s m, AdditiveGroup n) => m (n -> n) | ||
| 33 | sign = choice [ id <$ char '+' | ||
| 34 | , negateV <$ char '-' | ||
| 35 | ] | ||
diff --git a/lib/Postdelay/Utils.hs b/lib/Postdelay/Utils.hs deleted file mode 100644 index d716b4d..0000000 --- a/lib/Postdelay/Utils.hs +++ /dev/null | |||
| @@ -1,21 +0,0 @@ | |||
| 1 | {-# LANGUAGE FlexibleContexts #-} | ||
| 2 | |||
| 3 | module Postdelay.Utils | ||
| 4 | ( hoistParsecT | ||
| 5 | ) where | ||
| 6 | |||
| 7 | import Control.Monad.Error.Class | ||
| 8 | |||
| 9 | import Data.Functor | ||
| 10 | import Data.Either | ||
| 11 | |||
| 12 | import Data.Functor.Identity | ||
| 13 | import Text.Parsec.Prim | ||
| 14 | import Text.Parsec.Error | ||
| 15 | |||
| 16 | hoistParsecT :: (Monad m, Stream s Identity t, Stream s m t) => ParsecT s u Identity a -> ParsecT s u m a | ||
| 17 | hoistParsecT p = do | ||
| 18 | st <- getParserState | ||
| 19 | let res = runParser p' undefined "" undefined | ||
| 20 | p' = setParserState st >> ((,) <$> getState <*> p) | ||
| 21 | either (fail . show) (\(st', res) -> putState st' $> res) $ res | ||
