summaryrefslogtreecommitdiff
path: root/lib/Postdelay/TimeSpec.hs
blob: f61d84e8459682fbad44ab8be16fb88297f04f07 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
{-# LANGUAGE FlexibleContexts, ScopedTypeVariables, ViewPatterns, ConstraintKinds, TupleSections, RecordWildCards, TypeFamilies #-}

module Postdelay.TimeSpec
  ( timeSpec
  , timeZone
  
  , spaceConsumer, lexeme
  ) where

import Postdelay.TimeSpec.Utils
import Postdelay.TimeSpec.Units
import Postdelay.PrioMap

import Text.Megaparsec

import Control.Monad.IO.Class
import Control.Applicative
import Control.Lens hiding ((#))
import Control.Exception (IOException)
import Control.Monad.Catch hiding (try)
import Control.Monad.RWS hiding ((<>))

import Data.Functor
import Data.Bool
import Data.Semigroup hiding (option)
import Data.Monoid (Endo(..))
import Data.Foldable
import Data.VectorSpace
import Data.Maybe

import Data.Time as Time hiding (months)
import Data.Time.Calendar.WeekDate
import Data.Time.Lens
import Data.Time.Zones


data ModPrio = Offset | Assign | Shift | Default
  deriving (Eq, Ord, Enum, Bounded, Show)

  
seconds' :: Timeable t => Lens' t Time
seconds' = seconds . iso (% Second) (# Second)

utcOffset :: Iso' TimeZone Time
utcOffset = iso ((% Minute) . fromIntegral . timeZoneMinutes) (minutesToTimeZone . round . (# Minute))

weekDate :: Dateable t => Lens' t (Integer, Int, Int)
weekDate = date . iso toWeekDate (\(y, w, d) -> fromWeekDate y w d)

shiftBack :: (MonadReader LocalTime m, MonadWriter (PrioEndo ModPrio LocalTime) m)
          => Time -> m a -> m a
shiftBack by mod = do
  (result, modE) <- listen mod
  prev <- ask
  new <- asks $ view (prioEndo._Endo) modE
  when (new <= prev) $
    scribe (prio Shift) . Just . Endo $ flexDT.seconds' %~ ((^+^) by)
  return result

timeSpec :: forall s m. StringParser s m => m (Endo LocalTime)
timeSpec = label "Relative time specification" $ view prioEndo <$> choice
           [ flip (<>) <$> lexeme specBase <*> option mempty (offsets True)
           , lexeme (string' "now") *> offsets True
           , offsets False
           ]
  where
    specBase = toEndo <$> mkGramSepBy spaces [ dateSpec
                                             , timeOfDay
                                             ]
    
    toEndo :: [RWS LocalTime (PrioEndo ModPrio LocalTime) () ()] -> PrioEndo ModPrio LocalTime
    toEndo (sequence -> act) = mempty & prios .@~ (\i -> Just . Endo $ \t -> maybe t (($ t) . appEndo) . view (prio i) . snd $ execRWS act t ())

timeOfDay, dateSpec :: StringParser s m => m (RWS LocalTime (PrioEndo ModPrio LocalTime) () ())
timeOfDay = label "Time of day" $ withShift <$> choice
            [ TimeOfDay 0  0 0 <$ string' "midnight"
            , TimeOfDay 12 0 0 <$ string' "noon"
            , TimeOfDay 16 0 0 <$ string' "teatime"
            , try $ do
                h <- hour24 True
                m <- minute True
                s <- option 0 $ second True
                return $ TimeOfDay h m s
            , try $ do
                h <- hour12 False
                m <- option 0 $ char ':' *> minute False
                s <- option 0 $ char ':' *> second False
                spaceConsumer
                amPm <- (0 <$ string' "am") <|> (12 <$ string' "pm")
                return $ TimeOfDay (h + amPm) m s
            , try $ do
                h <- hour24 False
                m <- option 0 $ char ':' *> minute False
                s <- option 0 $ char ':' *> second False
                return $ TimeOfDay h m s
            ]
  where
    hour12 pad = label "Modulus 12 hour" . fmap (flip mod 12) . boundedNatural pad $ 1 <=..<= 12
    hour24 pad = label "Modulus 24 hour" . boundedNatural pad $ 0 <=..< 24
    minute pad = label "Minute" . boundedNatural pad $ 0 <=..< 60
    second pad = label "Second" . boundedRational pad $ 0 <=..< 61

    withShift :: TimeOfDay -> RWS LocalTime (PrioEndo ModPrio LocalTime) () ()
    withShift tod = shiftBack (1 % Day) $ scribe (prio' Assign . time) tod
    
dateSpec = label "Date" $ (>> scribe (prio' Default . time) midnight) <$> choice
           [ (scribe (prio' Assign . date) =<< view date) <$ string' "today"
           , (scribe (prio' Assign . date) =<< views date succ) <$ string' "tomorrow"
           , try $ do
               string' "next"
               spaces
               choice
                 [ string' "day" $> do
                     scribe (prio' Assign . flexDT.date.days) =<< views (date.days) succ
                 , string' "week" $> do
                     scribe (prio' Assign . flexDT.date.days) =<< views (date.days) (+ 7)
                     scribe (prio' Assign . weekDate._3) 1
                 , string' "month" $> do
                     scribe (prio' Assign . flexDT.date.months) =<< views (date.months) succ
                     scribe (prio' Assign . date.days) 1
                 , string' "year" $> do
                     scribe (prio' Assign . flexDT.date.years) =<< views (date.years) succ
                     scribe (prio' Assign . date.months) 1
                     scribe (prio' Assign . date.days) 1
                 ]
           , try $ do
               let daySuffix = optional $ choice [ string ".", string' "st", string' "nd", string' "rd", string' "th" ]
               (m, d) <- choice [ try $ (,) <$> monthName <* spaces <*> dayNumber False <* daySuffix
                                , try $ flip (,) <$> dayNumber False <* daySuffix <* spaces <*> monthName
                                ]
               y <- optional . try $ do
                 spaces
                 optional . lexeme $ char ','
                 yearNumber <* lookAhead (spaces <|> eof)
               return $ scribeDate y m d
           , try $ do
               m <- monthNumber False
               char '/'
               d <- dayNumber False
               y <- optional . try $ char '/' *> yearNumber <* lookAhead (spaces <|> eof)
               return $ scribeDate y m d
           , try $ do
               d <- dayNumber False
               char '.'
               m <- monthNumber False
               y <- optional . try $ char '.' *> yearNumber <* lookAhead (spaces <|> eof)
               return $ scribeDate y m d
           , try $ do
               (Just -> y) <- yearNumber
               char '-'
               m <- monthNumber True
               char '-'
               d <- dayNumber True
               return $ scribeDate y m d
           , try $ do
               ds <- lookAhead $ length <$ (optional $ ($ ()) <$> sign) <*> some digitChar <* lookAhead (spaces <|> eof)
               let yDs = ds - 2 {- month-} - 2 {- day -}
               when (yDs <= 0) $
                 fail "Too few digits to interpret as concatenated date"
               (Just -> y) <- optSigned . boundedNatural True $ 10^(pred yDs) <=..< 10^yDs
               m <- monthNumber True
               d <- dayNumber True
               return $ scribeDate y m d
           , monthName <$$> \m -> shiftBack (1 % Year) $ do
               scribe (prio' Assign . flexDT.date.months) m
               scribe (prio' Assign . flexDT.date.days) 1
           , weekdayName <$$> \w -> shiftBack (1 % Week) $ do
               scribe (prio' Assign . weekDate._3) w
          ]
  where
    withYear y
      | Just y' <- y = (>>) $ scribe (prio' Assign . date.years) y'
      | otherwise = shiftBack (1 % Year)
    scribeDate y m d = withYear y $ do
      scribe (prio' Assign . date.months) m
      scribe (prio' Assign . flexDT.date.days) d
    (<$$>) = flip (<$>)
    monthName = choice $ zipWith (<$) [1..] [ string' "january" <|> string' "jan"
                                            , string' "febuary" <|> string' "feb"
                                            , string' "march" <|> string' "mar"
                                            , string' "april" <|> string' "apr"
                                            , string' "may"
                                            , string' "june" <|> string' "jun"
                                            , string' "july" <|> string' "jul"
                                            , string' "august" <|> string' "aug"
                                            , string' "september" <|> string' "sep"
                                            , string' "october" <|> string' "oct"
                                            , string' "november" <|> string' "nov"
                                            , string' "december" <|> string' "dec"
                                            ]
    weekdayName = choice $ zipWith (<$) [1..] [ string' "monday" <|> string' "mon"
                                              , string' "tuesday" <|> string' "tue"
                                              , string' "wednesday" <|> string' "wed"
                                              , string' "thursday" <|> string' "thu"
                                              , string' "friday" <|> string' "fri"
                                              , string' "saturday" <|> string' "sat"
                                              , string' "sunday" <|> string' "sun"
                                              ]
    dayNumber p = boundedNatural p (1 <=..<= 31)
    monthNumber p = boundedNatural p (1 <=..<= 12)
    yearNumber = optSigned $ boundedNatural False (0 <=..< PosInf)

offsets :: forall s m. StringParser s m
         => Bool -- ^ Require sign on first offset?
         -> m (PrioEndo ModPrio LocalTime)
offsets reqSgn = fmap (foldMap . review $ prioEndo' Offset) $ (:) <$> offset reqSgn <*> many (offset False)
  where
    asOffset :: Time -> Endo LocalTime
    asOffset by = Endo $ flexDT.seconds' %~ (^+^ by)
    offset :: Bool -> m (Endo LocalTime)
    offset (bool (optSigned, "Time offset") (signed, "Signed time offset") -> (sgn, desc))
      = asOffset <$> lexeme (sgn timeLength) <?> desc

timeZone :: (StringParser s m, MonadIO m) => m (Either TimeZone TZ)
timeZone = label "Timezone" $ (Left <$> numericTimezone) <|> (Right <$> namedTimezone)

numericTimezone :: StringParser s m => m TimeZone
numericTimezone = review utcOffset <$> (sign <*> ((^+^) <$> hour <*> minute)) <?> "Numeric timezone"
  where
    hour   = (% Hour)   <$> boundedNatural True (0 <=..<= 24)
    minute = (% Minute) <$> boundedNatural True (0 <=..<  60)

namedTimezone :: (StringParser s m, MonadIO m) => m TZ
namedTimezone = do
  n <- ident <?> "Named timezone identifier"
  tz <- liftIO $ do
    let
      fbHandler :: IO a -> (IOException -> IO a)
      fbHandler fb _ = fb
    foldl (\fb a -> a `catch` fbHandler fb) (return Nothing)
      [ Just <$> loadSystemTZ n
      , Just <$> loadTZFromDB n
      ]
  case tz of
    Nothing   -> fail $ "Could not resolve timezone: " ++ show n
    (Just tz) -> return tz
  where
    asciiAlphaNum = oneOf $ ['0'..'9'] ++ ['a'..'z'] ++ ['A'..'Z']
    ident = (++) <$> some asciiAlphaNum <*> option [] ((:) <$> oneOf "_-/.+" <*> ident)