1 {-# LANGUAGE FlexibleContexts #-}
2 module Hcompta.Filter.Date.Read where
4 import Control.Monad (Monad(..))
6 import Data.Char (Char)
7 import Data.Eq (Eq(..))
8 import Data.Functor ((<$>))
9 import Data.Maybe (Maybe(..), maybe)
10 import qualified Data.Time.Calendar as Time
11 import qualified Data.Time.LocalTime as Time
12 import Data.Time.LocalTime (TimeZone(..))
13 import Data.Typeable ()
14 import qualified Text.Parsec as R hiding
26 import Text.Parsec (Stream, ParsecT, (<|>), (<?>))
27 import Prelude (($), Int, Integer, Num(..), Show(..), id)
29 import Hcompta.Date (Date)
30 import qualified Hcompta.Lib.Parsec as R
33 = Error_year_or_day_is_missing
34 | Error_invalid_date (Integer, Int, Int)
35 | Error_invalid_time_of_day (Int, Int, Integer)
38 -- | Read a 'Date' in @[YYYY\/]MM\/DD[_HH:MM[:SS][TZ]]@ format.
40 :: (Stream s (R.Error_State e m) Char, Monad m)
41 => (Error -> e) -> Maybe Integer -> ParsecT s u (R.Error_State e m) Date
42 date err def_year = (do
43 let read_2_or_1_digits = R.try (R.count 2 R.digit) <|> R.count 1 R.digit
45 day_sep <- date_separator
46 n1 <- read_2_or_1_digits
47 n2 <- R.option Nothing $ R.try $ do
49 Just <$> read_2_or_1_digits
51 case (n2, def_year) of
52 (Nothing, Nothing) -> R.fail_with "date" (err $ Error_year_or_day_is_missing)
53 (Nothing, Just year) -> return (year, n0, n1)
54 (Just d, _) -> return (R.integer_of_digits 10 n0, n1, d)
55 let month = fromInteger $ R.integer_of_digits 10 m
56 let dom = fromInteger $ R.integer_of_digits 10 d
57 day <- case Time.fromGregorianValid year month dom of
58 Nothing -> R.fail_with "date" (err $ Error_invalid_date (year, month, dom))
59 Just day -> return day
60 (hour, minu, sec, tz) <-
61 R.option (0, 0, 0, Time.utc) $ R.try $ do
63 hour <- read_2_or_1_digits
65 minu <- read_2_or_1_digits
66 sec <- R.option Nothing $ R.try $ do
68 Just <$> read_2_or_1_digits
69 tz <- R.option Time.utc $ R.try $ do
70 -- R.skipMany $ R.space_horizontal
73 ( fromInteger $ R.integer_of_digits 10 hour
74 , fromInteger $ R.integer_of_digits 10 minu
75 , maybe 0 (R.integer_of_digits 10) sec
77 tod <- case Time.makeTimeOfDayValid hour minu (fromInteger sec) of
78 Nothing -> R.fail_with "date" (err $ Error_invalid_time_of_day (hour, minu, sec))
79 Just tod -> return tod
80 return $ Time.localTimeToUTC tz (Time.LocalTime day tod)
83 -- | Parse the year, month and day separator: "/" or "-".
84 date_separator :: Stream s m Char => ParsecT s u m Char
85 date_separator = R.satisfy (\c -> c == '/' || c == '-')
87 -- | Parse the hour, minute and second separator: ":".
88 hour_separator :: Stream s m Char => ParsecT s u m Char
89 hour_separator = R.char ':'
91 -- | Parse either "-" into 'negate', or "+" or "" into 'id'.
92 sign :: (Stream s m Char, Num i) => ParsecT s u m (i -> i)
94 (R.char '-' >> return negate)
95 <|> (R.char '+' >> return id)
98 time_zone :: Stream s m Char => ParsecT s u m TimeZone
100 -- DOC: http://www.timeanddate.com/time/zones/
101 -- TODO: only a few time zones are suported below.
102 -- TODO: check the timeZoneSummerOnly values
104 [ R.char 'A' >> R.choice
105 [ R.string "ST" >> return (TimeZone ((-4) * 60) False "AST")
106 , R.string "DT" >> return (TimeZone ((-3) * 60) True "ADT")
107 , return (TimeZone ((-1) * 60) False "A")
109 , R.char 'B' >> R.choice
110 [ R.string "ST" >> return (TimeZone ((-11) * 60) False "BST")
111 , R.string "DT" >> return (TimeZone ((-10) * 60) True "BDT")
113 , R.char 'C' >> R.choice
114 [ R.char 'E' >> R.choice
115 [ R.string "T" >> return (TimeZone ((1) * 60) True "CET")
116 , R.string "ST" >> return (TimeZone ((2) * 60) False "CEST")
118 , R.string "ST" >> return (TimeZone ((-6) * 60) False "CST")
119 , R.string "DT" >> return (TimeZone ((-5) * 60) True "CDT")
121 , R.char 'E' >> R.choice
122 [ R.string "ST" >> return (TimeZone ((-5) * 60) False "EST")
123 , R.string "DT" >> return (TimeZone ((-4) * 60) True "EDT")
125 , R.string "GMT" >> return (TimeZone 0 False "GMT")
126 , R.char 'H' >> R.choice
127 [ R.string "ST" >> return (TimeZone ((-10) * 60) False "HST")
128 , R.string "DT" >> return (TimeZone (( -9) * 60) True "HDT")
130 , R.char 'M' >> R.choice
131 [ R.string "ST" >> return (TimeZone ((-7) * 60) False "MST")
132 , R.string "DT" >> return (TimeZone ((-6) * 60) True "MDT")
133 , return (TimeZone ((-12) * 60) False "M")
135 , R.char 'N' >> R.choice
136 [ R.string "ST" >> return (TimeZone ((-3) * 60 - 30) False "NST")
137 , return (TimeZone (1 * 60) False "N")
139 , R.char 'P' >> R.choice
140 [ R.string "ST" >> return (TimeZone ((-8) * 60) False "PST")
141 , R.string "DT" >> return (TimeZone ((-7) * 60) True "PDT")
143 , R.char 'Y' >> R.choice
144 [ R.string "ST" >> return (TimeZone ((-9) * 60) False "YST")
145 , R.string "DT" >> return (TimeZone ((-8) * 60) True "YDT")
146 , return (TimeZone (12 * 60) False "Y")
148 , R.char 'Z' >> return (TimeZone 0 False "Z")
152 time_zone_digits :: Stream s m Char => ParsecT s u m TimeZone
153 {-# INLINEABLE time_zone_digits #-}
154 time_zone_digits = do
156 hour <- R.integer_of_digits 10 <$> R.count 2 R.digit
157 _ <- R.option ':' (R.char ':')
158 minute <- R.integer_of_digits 10 <$> R.count 2 R.digit
160 { timeZoneMinutes = sign_ (fromInteger hour * 60 + fromInteger minute)
161 , timeZoneSummerOnly = False
162 , timeZoneName = Time.timeZoneOffsetString tz