summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--default.nix6
-rw-r--r--lib/Postdelay/Scan.hs37
-rw-r--r--lib/Postdelay/TimeSpec.hs326
-rw-r--r--lib/Postdelay/Types.hs2
-rw-r--r--postdelay.cabal4
-rw-r--r--postdelay.nix7
-rw-r--r--postdelay.nix.gup2
-rw-r--r--result.gup6
-rw-r--r--shell.nix25
9 files changed, 385 insertions, 30 deletions
diff --git a/default.nix b/default.nix
deleted file mode 100644
index 0ba6150..0000000
--- a/default.nix
+++ /dev/null
@@ -1,6 +0,0 @@
1argumentPackages@{ ... }:
2
3let
4 defaultPackages = with (import <nixpkgs> {}); haskellPackages;
5 pkgs = defaultPackages // argumentPackages;
6in pkgs.callPackage ./postdelay.nix {}
diff --git a/lib/Postdelay/Scan.hs b/lib/Postdelay/Scan.hs
index e6de0cf..fba9f35 100644
--- a/lib/Postdelay/Scan.hs
+++ b/lib/Postdelay/Scan.hs
@@ -1,4 +1,4 @@
1{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, FlexibleContexts #-} 1{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, FlexibleContexts, RecordWildCards, ViewPatterns #-}
2 2
3module Postdelay.Scan 3module Postdelay.Scan
4 ( scan 4 ( scan
@@ -12,13 +12,17 @@ import Postdelay.TimeSpec
12 12
13import Control.Monad 13import Control.Monad
14import Control.Monad.IO.Class 14import Control.Monad.IO.Class
15import Control.Monad.Error.Class 15import Control.Monad.Except
16import Control.Monad.Reader
17import Control.Monad.List
18import Control.Exception.Base
16 19
17import Text.Parsec.Char 20import Text.Parsec.Char
18import Text.Parsec.Prim 21import Text.Parsec.Prim
19import Text.Parsec.Combinator 22import Text.Parsec.Combinator
20import Text.Parsec.Error (ParseError(..)) 23import Text.Parsec.Error (ParseError(..))
21import Text.ParserCombinators.Parsec.Rfc2822 24import Text.ParserCombinators.Parsec.Rfc2822
25import Codec.MIME.Decode (decodeWords)
22 26
23import Data.CaseInsensitive (CI) 27import Data.CaseInsensitive (CI)
24import qualified Data.CaseInsensitive as CI 28import qualified Data.CaseInsensitive as CI
@@ -26,18 +30,39 @@ import qualified Data.CaseInsensitive as CI
26import Data.Either 30import Data.Either
27import Data.Foldable 31import Data.Foldable
28import Data.Semigroup 32import Data.Semigroup
33import Data.Time
34import Data.List
35import Data.Ord
36import Data.Ratio
37
38import Data.Time.Zones
39import System.Time (CalendarTime(..))
40
41import Debug.Trace
29 42
30 43
31scan :: (MonadIO m, MonadError ParseError m) => String -> m (Maybe Delay) 44scan :: (MonadIO m, MonadError ParseError m) => String -> m (Maybe Delay)
32scan = fmap getOption . extractDelay <=< either throwError return . parse message "" 45scan = fmap getOption . extractDelay <=< either throwError return . parse message ""
33 46
34extractDelay :: forall m. (MonadIO m, MonadError ParseError m) => Message -> m (Option Delay) 47extractDelay :: forall m. (MonadIO m, MonadError ParseError m) => Message -> m (Option Delay)
35extractDelay (Message headers _) = foldMap pure <$> mapM parseDelay delayHeaders 48extractDelay (Message headers _)
49 = let latestCtx = maximumBy (comparing baseTime) dateHeaders
50 in foldMap pure <$> mapM (flip runReaderT latestCtx . parseDelay) delayHeaders
36 where 51 where
37 delayHeaders :: [Field] 52 delayHeaders :: [Field]
38 delayHeaders = do 53 delayHeaders = do
39 h@(OptionalField field content) <- headers 54 (OptionalField field content) <- headers
40 guard $ CI.mk field == "X-Delay" 55 guard $ CI.mk field == "X-Delay"
41 return h 56 return . OptionalField field $ decodeWords content
42 parseDelay :: Field -> m Delay 57 dateHeaders :: [TimeCtx]
58 dateHeaders = do
59 (Date CalendarTime{..}) <- headers
60 let tz = minutesToTimeZone . round $ ctTZ % 60
61 return $ TimeCtx
62 { baseTime = localTimeToUTC tz $ LocalTime
63 (fromGregorian (fromIntegral ctYear) (fromEnum ctMonth + 1) ctDay)
64 (TimeOfDay ctHour ctMin $ fromIntegral ctSec + fromIntegral ctPicosec * 1e-12)
65 , tz = Left tz
66 }
67 parseDelay :: Field -> ReaderT TimeCtx m Delay
43 parseDelay (OptionalField field content) = either throwError return =<< runParserT (Until <$> pTimeSpec) () field content 68 parseDelay (OptionalField field content) = either throwError return =<< runParserT (Until <$> pTimeSpec) () field content
diff --git a/lib/Postdelay/TimeSpec.hs b/lib/Postdelay/TimeSpec.hs
index b080dcc..af8d801 100644
--- a/lib/Postdelay/TimeSpec.hs
+++ b/lib/Postdelay/TimeSpec.hs
@@ -1,14 +1,332 @@
1{-# LANGUAGE FlexibleContexts, ScopedTypeVariables, ViewPatterns, ConstraintKinds, TupleSections, RecordWildCards #-}
2
1module Postdelay.TimeSpec 3module Postdelay.TimeSpec
2 ( pTimeSpec 4 ( pTimeSpec
5 , TimeCtx(..)
3 ) where 6 ) where
4 7
8import Control.Monad
5import Control.Monad.IO.Class 9import Control.Monad.IO.Class
6import Data.Time 10import Control.Monad.Reader.Class
11import Control.Monad.Error.Class
7 12
8import Text.Parsec.Char 13import Text.Parsec.Char hiding (digit)
14import qualified Text.Parsec.Char as Parsec (digit)
9import Text.Parsec.Prim 15import Text.Parsec.Prim
10import Text.Parsec.Combinator 16import Text.Parsec.Combinator
11import Text.Parsec.Error (ParseError(..)) 17import Text.Parsec.Error (ParseError(..))
18import Text.Read (readMaybe)
19
20import Data.CaseInsensitive (CI)
21import qualified Data.CaseInsensitive as CI
22
23import Data.Time
24import Data.Time.Calendar.WeekDate
25import Data.Time.Zones
26import Data.Function
27import Data.Maybe
28import Data.Foldable
29import Data.Ord
30import Data.List
31import Data.Tuple
32import Data.Bool
33
34import Control.Exception (IOException)
35
36import Debug.Trace
37
38
39type MonadTP m = (MonadIO m, MonadReader TimeCtx m)
40
41data TimeCtx = TimeCtx
42 { baseTime :: UTCTime
43 , tz :: Either TimeZone TZ
44 }
45
46
47spaced :: Stream s m Char => ParsecT s u m a -> ParsecT s u m a
48spaced p = spaces *> p <* spaces
49
50string' :: Stream s m Char => String -> ParsecT s u m String
51string' = mapM $ satisfy . ((==) `on` CI.mk)
52
53choice' :: (Stream s m t, Foldable f) => f (ParsecT s u m a) -> ParsecT s u m a
54choice' (toList -> f)
55 | [p] <- f = p
56 | (p:ps) <- f = try p <|> choice' ps
57 | otherwise = mzero
58
59strChoice' :: Stream s m Char => [String] -> ParsecT s u m String
60strChoice' = choice' . map string' . sortBy (comparing $ Down . length)
61
62natural :: (Stream s m Char, Num a) => ParsecT s u m a
63natural = foldl' (\init last -> init * 10 + last) 0 <$> many1 digit
64
65decimal :: (Stream s m Char, Num a, Fractional a) => ParsecT s u m a
66decimal = do
67 w <- foldl' (\init last -> init * 10 + last) 0 <$> many1 digit
68 f <- option 0 $ do
69 char '.'
70 foldr' (\head tail -> head + tail / 10) 0 <$> many1 digit
71 return $ w + f / 10
72
73digit :: (Stream s m Char, Num a) => ParsecT s u m a
74digit = fromIntegral . (\c -> fromEnum c - fromEnum '0') <$> Parsec.digit
75
76ensure :: MonadPlus m => (a -> Bool) -> a -> m a
77ensure p x = bool (const mzero) return (p x) $ x
78
79
80pTimeSpec :: MonadTP m => ParsecT String () m UTCTime
81pTimeSpec = choice' [ flip addUTCTime <$> spaced pSpecBase <*> spaced pSpecOffset <?> "Absolute time + offset"
82 , flip addUTCTime <$> asks baseTime <*> spaced pSpecOffset <?> "Time offset"
83 , spaced pSpecBase <?> "Absolute time"
84 ]
85 <* eof <?> "Time specification"
86
87pSpecBase :: forall m. MonadTP m => ParsecT String () m UTCTime
88pSpecBase = choice'
89 [ utcTime <$> spaced pDate <*> spaced pTime
90 , flip utcTime <$> spaced pTime <*> spaced pDate
91 , do
92 proto@(UTCTime{..}) <- utcTime <$> (utctDay <$> asks baseTime) <*> spaced pTime
93 now <- asks baseTime
94 return $ if proto < now
95 then UTCTime (succ utctDay) utctDayTime
96 else proto
97 , utcTime <$> spaced pDate <*> ((dayFractionToTimeOfDay 0, ) <$> asks tz)
98 , spaced (string' "now") *> asks baseTime
99 ] <?> "Base specification"
100 where
101 utcTime :: Day -> (TimeOfDay, Either TimeZone TZ) -> UTCTime
102 utcTime d (t, Right tz) = localTimeToUTCTZ tz (LocalTime d t)
103 utcTime d (t, Left tz) = localTimeToUTC tz (LocalTime d t)
104
105pSpecOffset :: MonadTP m => ParsecT String () m NominalDiffTime
106pSpecOffset = (+) <$> pSpecOffset' <*> option 0 (try (many $ space <|> char ',' <|> char ';') >> pSpecOffset)
107 where
108 pSpecOffset' = option id (spaced pSign) <*> ((*) <$> spaced pNumber <*> spaced pSpecOffsetConst) <?> "Time offset"
109 pNumber = fromInteger <$> natural <?> "Offset multiplier"
110
111pSign :: MonadTP m => ParsecT String () m (NominalDiffTime -> NominalDiffTime)
112pSign = choice [ id <$ char '+'
113 , negate <$ char '-'
114 ] <?> "Offset sign"
115
116pSpecOffsetConst :: MonadTP m => ParsecT String () m NominalDiffTime
117pSpecOffsetConst = choice' [ 1e-12 <$ strChoice' [ "ps"
118 , "picosecond"
119 , "picoseconds"
120 ]
121 , 1e-9 <$ strChoice' [ "ns"
122 , "nanosecond"
123 , "nanoseconds"
124 ]
125 , 1e-6 <$ strChoice' [ "us", "µs"
126 , "microsecond"
127 , "microseconds"
128 ]
129 , 1e-3 <$ strChoice' [ "ms"
130 , "millisecond"
131 , "milliseconds"
132 ]
133 , 1e-2 <$ strChoice' [ "ds"
134 , "decisecond"
135 , "deciseconds"
136 ]
137 , 1e-1 <$ strChoice' [ "cs"
138 , "centisecond"
139 , "centiseconds"
140 ]
141 , 1 <$ strChoice' [ "s"
142 , "second"
143 , "seconds"
144 ]
145 , 60 <$ strChoice' [ "min"
146 , "minute"
147 , "minutes"
148 ]
149 , 3600 <$ strChoice' [ "h"
150 , "hour"
151 , "hours"
152 ]
153 , 24 * 3600 <$ strChoice' [ "d"
154 , "day"
155 , "days"
156 ]
157 , 7 * 24 * 3600 <$ strChoice' [ "week"
158 , "weeks"
159 ]
160 , 30 * 24 * 3600 <$ strChoice' [ "month"
161 , "months"
162 ]
163 , 365 * 24 * 3600 <$ strChoice' [ "year"
164 , "years"
165 ]
166 ] <?> "Offset unit"
167
168pDate :: MonadTP m => ParsecT String () m Day
169pDate = choice' [ do
170 (m, d) <- choice' [ do
171 m <- spaced pMonthName
172 d <- spaced natural
173 optional . spaced $ char ','
174 return (m, d)
175 , fmap swap . (,) <$> spaced natural <*> spaced pMonthName
176 ]
177 y <- spaced natural
178 fromGregorian' y m d
179 , do
180 now <- asks baseTime
181 (m, d) <- choice' [ (,) <$> spaced pMonthName <*> spaced natural
182 , fmap swap . (,) <$> spaced natural <*> spaced pMonthName
183 ]
184 let
185 (y, _, _) = toGregorian $ utctDay now
186 proto <- fromGregorian' y m d
187 if proto < utctDay now
188 then fromGregorian' (y + 1) m d
189 else return proto
190 , do
191 now <- asks baseTime
192 optional $ spaces *> pNext <* many1 space
193 d <- spaced pWeekdayName
194 let
195 (y, w, _) = toWeekDate $ utctDay now
196 proto <- fromWeekDate' y w d
197 if proto < utctDay now
198 then maybe (fromWeekDate' (y + 1) 1 d) return $ fromWeekDateValid y (w + 1) d
199 else return proto
200 , do
201 spaced $ string' "today"
202 utctDay <$> asks baseTime
203 , do
204 spaced $ string' "tomorrow"
205 succ . utctDay <$> asks baseTime
206 , do
207 y <- natural
208 char '-'
209 m <- natural
210 char '-'
211 d <- natural
212 fromGregorian' y m d
213 , do
214 d <- natural
215 char '.'
216 m <- natural
217 char '.'
218 y <- natural
219 fromGregorian' y m d
220 , do
221 m <- natural
222 char '/'
223 d <- natural
224 char '/'
225 y <- natural
226 fromGregorian' y m d
227 , do
228 ds <- many1 digit
229 when (length ds < 5) $ fail "Insufficient digits to interpret as concatenated date"
230 let
231 d2 : d1 : m2 : m1 : ys = reverse ds
232 d = 10 * d1 + d2
233 m = 10 * m1 + m2
234 y = foldl' (\init last -> init * 10 + last) 0 . map fromIntegral $ reverse ys
235 fromGregorian' y m d
236 , do
237 pNext
238 many1 space
239 fmap utctDay . addUTCTime <$> pSpecOffsetConst <*> asks baseTime
240 ] <?> "Day specification"
241 where
242 fromGregorian' y m d = maybe (fail "Invalid gregorian date") return $ fromGregorianValid y m d
243 fromWeekDate' y w d = maybe (fail "Invalid iso8601 date") return $ fromWeekDateValid y w d
244 pNext = string' "next"
245
246pMonthName :: MonadTP m => ParsecT String () m Int
247pMonthName = choice' (zipWith (<$) [1..] [ strChoice' [ "January", "Jan" ]
248 , strChoice' [ "Febuary", "Feb" ]
249 , strChoice' [ "March", "Mar" ]
250 , strChoice' [ "April", "Apr" ]
251 , strChoice' [ "May" ]
252 , strChoice' [ "June", "Jun" ]
253 , strChoice' [ "July", "Jul" ]
254 , strChoice' [ "August", "Aug" ]
255 , strChoice' [ "September", "Sep" ]
256 , strChoice' [ "October", "Oct" ]
257 , strChoice' [ "November", "Nov" ]
258 , strChoice' [ "December", "Dec" ]
259 ]) <?> "Month name"
260
261pWeekdayName :: MonadTP m => ParsecT String () m Int
262pWeekdayName = choice' (zipWith (<$) [1..] [ strChoice' [ "Monday", "Mon" ]
263 , strChoice' [ "Tuesday", "Tue" ]
264 , strChoice' [ "Wednesday", "Wed" ]
265 , strChoice' [ "Thursday", "Thu" ]
266 , strChoice' [ "Friday", "Fri" ]
267 , strChoice' [ "Saturday", "Sat" ]
268 , strChoice' [ "Sunday", "Sun" ]
269 ])
270
271pTime :: MonadTP m => ParsecT String () m (TimeOfDay, Either TimeZone TZ)
272pTime = choice' [ (,) <$> spaced pTimeBase <*> spaced pTimeZone
273 , (,) <$> spaced pTimeBase <*> asks tz
274 ] <?> "Time of day and timezone specification"
275
276data AMPM = AM | PM
277 deriving (Eq, Ord, Enum)
278
279pTimeBase :: MonadTP m => ParsecT String () m TimeOfDay
280pTimeBase = choice' [ do
281 h <- pHour12
282 m <- option 0 $ char ':' >> pMinute
283 s <- option 0 $ char ':' >> pSecond
284 amPM <- spaced pAMPM
285 let h' = h + fromEnum amPM * 12
286 return $ TimeOfDay h' m s
287 , do
288 h <- pHour
289 m <- option 0 $ char ':' >> pMinute
290 s <- option 0 $ char ':' >> pSecond
291 return $ TimeOfDay h m s
292 , do
293 h <- ensure (<= 24) =<< (\d u -> 10 * d + u) <$> digit <*> digit
294 m <- option 0 $ ensure (< 60) =<< (\d u -> 10 * d + u) <$> digit <*> digit
295 s <- option 0 $ pSecond
296 return $ TimeOfDay h m s
297 , TimeOfDay 0 0 0 <$ string' "midnight"
298 , TimeOfDay 12 0 0 <$ string' "noon"
299 , TimeOfDay 16 0 0 <$ string' "teatime"
300 ] <?> "Time of day specification"
301 where
302 pAMPM = choice [ AM <$ string' "AM"
303 , PM <$ string' "PM"
304 ]
305 pHour12 = (`rem` 12) <$> (ensure (<= 12) =<< natural)
306
307 pHour = (`rem` 24) <$> (ensure (<= 24) =<< natural)
308 pMinute = ensure (< 60) =<< natural
309 pSecond = decimal
12 310
13pTimeSpec :: MonadIO m => ParsecT String () m UTCTime 311pTimeZone :: MonadTP m => ParsecT String () m (Either TimeZone TZ)
14pTimeSpec = undefined 312pTimeZone = choice' [ do
313 sgn <- choice [ id <$ char '+'
314 , negate <$ char '-'
315 ]
316 hs <- (\d u -> 10 * d + u) <$> digit <*> digit
317 ms <- option 0 $ (\d u -> 10 * d + u) <$> digit <*> digit
318 return . Left . minutesToTimeZone $ hs * 60 + ms
319 , do
320 n <- many1 $ letter <|> char '/'
321 tz <- liftIO $ do
322 let
323 fbHandler :: IO a -> (IOException -> IO a)
324 fbHandler fb _ = fb
325 foldl (\fb a -> a `catchError` fbHandler fb) (return Nothing)
326 [ Just <$> loadSystemTZ n
327 , Just <$> loadTZFromDB n
328 ]
329 case tz of
330 Nothing -> fail $ "Could not resolve timezone: " ++ n
331 (Just tz) -> return $ Right tz
332 ]
diff --git a/lib/Postdelay/Types.hs b/lib/Postdelay/Types.hs
index 3f66fb8..d5f8325 100644
--- a/lib/Postdelay/Types.hs
+++ b/lib/Postdelay/Types.hs
@@ -7,7 +7,7 @@ import Data.Semigroup
7import Data.Time.Clock (UTCTime) 7import Data.Time.Clock (UTCTime)
8 8
9newtype Delay = Until { releaseTime :: UTCTime } 9newtype Delay = Until { releaseTime :: UTCTime }
10 deriving (Eq, Ord) 10 deriving (Eq, Ord, Show)
11 11
12instance Semigroup Delay where 12instance Semigroup Delay where
13 (Until a) <> (Until b) = Until $ max a b 13 (Until a) <> (Until b) = Until $ max a b
diff --git a/postdelay.cabal b/postdelay.cabal
index 957e6d5..6b02951 100644
--- a/postdelay.cabal
+++ b/postdelay.cabal
@@ -26,7 +26,11 @@ library
26 , parsec >=3.1 && <4 26 , parsec >=3.1 && <4
27 , case-insensitive >=1.2 && <2 27 , case-insensitive >=1.2 && <2
28 , hsemail >=1.7 && <2 28 , hsemail >=1.7 && <2
29 , mime >=0.4 && <1
29 , mtl >=2.2 && <3 30 , mtl >=2.2 && <3
31 , tz >=0.1 && <1
32 , old-time >=1.1 && <2
33 , list-t >=1 && <2
30 hs-source-dirs: lib 34 hs-source-dirs: lib
31 default-language: Haskell2010 35 default-language: Haskell2010
32 36
diff --git a/postdelay.nix b/postdelay.nix
index 66dae61..701af04 100644
--- a/postdelay.nix
+++ b/postdelay.nix
@@ -1,5 +1,5 @@
1{ mkDerivation, base, case-insensitive, hsemail, mtl, parsec 1{ mkDerivation, base, case-insensitive, hsemail, list-t, mime, mtl
2, stdenv, time, transformers 2, old-time, parsec, stdenv, time, transformers, tz
3}: 3}:
4mkDerivation { 4mkDerivation {
5 pname = "postdelay"; 5 pname = "postdelay";
@@ -8,7 +8,8 @@ mkDerivation {
8 isLibrary = true; 8 isLibrary = true;
9 isExecutable = true; 9 isExecutable = true;
10 libraryHaskellDepends = [ 10 libraryHaskellDepends = [
11 base case-insensitive hsemail mtl parsec time 11 base case-insensitive hsemail list-t mime mtl old-time parsec time
12 tz
12 ]; 13 ];
13 executableHaskellDepends = [ base transformers ]; 14 executableHaskellDepends = [ base transformers ];
14 homepage = "https://git.yggdrasil.li/gkleen/pub/postdelay"; 15 homepage = "https://git.yggdrasil.li/gkleen/pub/postdelay";
diff --git a/postdelay.nix.gup b/postdelay.nix.gup
index 976221f..bc92dcd 100644
--- a/postdelay.nix.gup
+++ b/postdelay.nix.gup
@@ -2,4 +2,4 @@
2 2
3gup -u ${2:r}.cabal 3gup -u ${2:r}.cabal
4cd ${2:h} 4cd ${2:h}
5cabal2nix ./. >! ${1} \ No newline at end of file 5cabal2nix ./. >! ${1}
diff --git a/result.gup b/result.gup
index 5c03e49..f1c92fa 100644
--- a/result.gup
+++ b/result.gup
@@ -1,4 +1,8 @@
1#!/usr/bin/env zsh 1#!/usr/bin/env zsh
2 2
3gup --always
3gup -u postdelay.nix default.nix 4gup -u postdelay.nix default.nix
4nix-build -o ${1} ./default.nix \ No newline at end of file 5
6nix-build -o ${1} ./default.nix
7
8find . \( \( -name '.gup' -or -name '.git' \) -prune \) -or \( -type f -exec shasum '{}' ';' \) | gup --contents \ No newline at end of file
diff --git a/shell.nix b/shell.nix
index a4274e7..27b7d06 100644
--- a/shell.nix
+++ b/shell.nix
@@ -1,9 +1,18 @@
1{ pkgs ? (import <nixpkgs> {}) 1{ nixpkgs ? import <nixpkgs> {}, compiler ? "default" }:
2}:
3 2
4pkgs.lib.overrideDerivation (pkgs.haskellPackages.callPackage ./default.nix {}) (oldArgs: { 3let
5 buildInputs = with pkgs; [ cabal2nix gup ]; 4 inherit (nixpkgs) pkgs;
6 shellHook = '' 5
7 export PROMPT_INFO=${oldArgs.name} 6 haskellPackages = if compiler == "default"
8 ''; 7 then pkgs.haskellPackages
9}) 8 else pkgs.haskell.packages.${compiler};
9
10 drv = haskellPackages.callPackage ./postdelay.nix {};
11in
12 pkgs.stdenv.lib.overrideDerivation drv.env (oldAttrs: {
13 buildInputs = oldAttrs.buildInputs ++ (with pkgs; [ cabal2nix gup ]);
14 shellHook = ''
15 ${oldAttrs.shellHook}
16 export PROMPT_INFO="${oldAttrs.name}"
17 '';
18 })