module Data.Time.Exts.Parser (
FormatText
, ParseError(..)
, parseUnixDate
, parseUnixTime
, parseUnixTimeMillis
, parseUnixTimeMicros
, parseUnixTimeNanos
, parseUnixTimePicos
, parseUnixDateTime
, parseUnixDateTimeMillis
, parseUnixDateTimeMicros
, parseUnixDateTimeNanos
, parseUnixDateTimePicos
, parseLocalDate
, parseLocalDateTime
, parseLocalDateTimeMillis
, parseLocalDateTimeMicros
, parseLocalDateTimeNanos
, parseLocalDateTimePicos
, parseUnixDate'
, parseUnixTime'
, parseUnixTimeMillis'
, parseUnixTimeMicros'
, parseUnixTimeNanos'
, parseUnixTimePicos'
, parseUnixDateTime'
, parseUnixDateTimeMillis'
, parseUnixDateTimeMicros'
, parseUnixDateTimeNanos'
, parseUnixDateTimePicos'
, parseLocalDate'
, parseLocalDateTime'
, parseLocalDateTimeMillis'
, parseLocalDateTimeMicros'
, parseLocalDateTimeNanos'
, parseLocalDateTimePicos'
) where
import Control.Applicative ((<|>), (<$>), (*>))
import Control.Arrow ((***))
import Control.Exception (Exception)
import Control.Monad
import Control.Monad.State.Strict (execState, State)
import Data.Attoparsec.Text as P hiding (decimal)
import Data.Convertible (Convertible(..), prettyConvertError)
import Data.Char (isAlpha)
import Data.Default (def)
import Data.Label ((:->), mkLabels)
import Data.Label.Monadic (puts, modify)
import Data.List as L (foldl', foldl1, map, zip)
import Data.String (IsString(..))
import Data.Text as T
import Data.Time.Exts.Base hiding (TimeZone)
import Data.Time.Exts.Local
import Data.Time.Exts.Unix
import Data.Time.Exts.Zone
import Data.Typeable (Typeable)
import System.Locale (TimeLocale(..))
type FormatText = Text
newtype ParseError = ParseError String deriving (Show,Typeable)
instance Exception ParseError
instance IsString ParseError where
fromString = ParseError
data TZ = TZ {
_set_year :: Year
, _set_mon :: Month
, _set_mday :: Day
, _set_wday :: DayOfWeek
, _set_hour :: Hour
, _set_min :: Minute
, _set_sec :: Double
, _set_frac :: Double -> Double
, _set_ampm :: Hour -> Hour
, _set_zone :: TimeZone
}
mkLabels [''TZ]
parseUnixDate :: FormatText -> Text -> Either ParseError UnixDate
parseUnixDate = parseUnixDate' def
parseUnixDate' :: TimeLocale -> FormatText -> Text -> Either ParseError UnixDate
parseUnixDate' locale format text = fun <$> parseTimestamp locale Universal format text
where fun TZ{..} = createUnixDate _set_year _set_mon _set_mday
parseUnixTime :: FormatText -> Text -> Either ParseError UnixTime
parseUnixTime = parseUnixTime' def
parseUnixTime' :: TimeLocale -> FormatText -> Text -> Either ParseError UnixTime
parseUnixTime' locale format text = fun <$> parseTimestamp locale Universal format text
where fun TZ{..} = createUnixTime hour _set_min sec
where hour = _set_ampm _set_hour
sec = truncate _set_sec
parseUnixTimeMillis :: FormatText -> Text -> Either ParseError UnixTimeMillis
parseUnixTimeMillis = parseUnixTimeMillis' def
parseUnixTimeMillis' :: TimeLocale -> FormatText -> Text -> Either ParseError UnixTimeMillis
parseUnixTimeMillis' locale format text = fun <$> parseTimestamp locale Universal format text
where fun TZ{..} = createUnixTimeMillis hour _set_min sec mil
where hour = _set_ampm _set_hour
(,) sec mil = properFracMillis $ _set_frac _set_sec
parseUnixTimeMicros :: FormatText -> Text -> Either ParseError UnixTimeMicros
parseUnixTimeMicros = parseUnixTimeMicros' def
parseUnixTimeMicros' :: TimeLocale -> FormatText -> Text -> Either ParseError UnixTimeMicros
parseUnixTimeMicros' locale format text = fun <$> parseTimestamp locale Universal format text
where fun TZ{..} = createUnixTimeMicros hour _set_min sec mic
where hour = _set_ampm _set_hour
(,) sec mic = properFracMicros $ _set_frac _set_sec
parseUnixTimeNanos :: FormatText -> Text -> Either ParseError UnixTimeNanos
parseUnixTimeNanos = parseUnixTimeNanos' def
parseUnixTimeNanos' :: TimeLocale -> FormatText -> Text -> Either ParseError UnixTimeNanos
parseUnixTimeNanos' locale format text = fun <$> parseTimestamp locale Universal format text
where fun TZ{..} = createUnixTimeNanos hour _set_min sec nan
where hour = _set_ampm _set_hour
(,) sec nan = properFracNanos $ _set_frac _set_sec
parseUnixTimePicos :: FormatText -> Text -> Either ParseError UnixTimePicos
parseUnixTimePicos = parseUnixTimePicos' def
parseUnixTimePicos' :: TimeLocale -> FormatText -> Text -> Either ParseError UnixTimePicos
parseUnixTimePicos' locale format text = fun <$> parseTimestamp locale Universal format text
where fun TZ{..} = createUnixTimePicos hour _set_min sec pic
where hour = _set_ampm _set_hour
(,) sec pic = properFracPicos $ _set_frac _set_sec
parseUnixDateTime :: FormatText -> Text -> Either ParseError UnixDateTime
parseUnixDateTime = parseUnixDateTime' def
parseUnixDateTime' :: TimeLocale -> FormatText -> Text -> Either ParseError UnixDateTime
parseUnixDateTime' locale format text = fun <$> parseTimestamp locale Universal format text
where fun TZ{..} = createUnixDateTime _set_year _set_mon _set_mday hour _set_min sec
where hour = _set_ampm _set_hour
sec = truncate _set_sec
parseUnixDateTimeMillis :: FormatText -> Text -> Either ParseError UnixDateTimeMillis
parseUnixDateTimeMillis = parseUnixDateTimeMillis' def
parseUnixDateTimeMillis' :: TimeLocale -> FormatText -> Text -> Either ParseError UnixDateTimeMillis
parseUnixDateTimeMillis' locale format text = fun <$> parseTimestamp locale Universal format text
where fun TZ{..} = createUnixDateTimeMillis _set_year _set_mon _set_mday hour _set_min sec mil
where hour = _set_ampm _set_hour
(,) sec mil = properFracMillis $ _set_frac _set_sec
parseUnixDateTimeMicros :: FormatText -> Text -> Either ParseError UnixDateTimeMicros
parseUnixDateTimeMicros = parseUnixDateTimeMicros' def
parseUnixDateTimeMicros' :: TimeLocale -> FormatText -> Text -> Either ParseError UnixDateTimeMicros
parseUnixDateTimeMicros' locale format text = fun <$> parseTimestamp locale Universal format text
where fun TZ{..} = createUnixDateTimeMicros _set_year _set_mon _set_mday hour _set_min sec mic
where hour = _set_ampm _set_hour
(,) sec mic = properFracMicros $ _set_frac _set_sec
parseUnixDateTimeNanos :: FormatText -> Text -> Either ParseError UnixDateTimeNanos
parseUnixDateTimeNanos = parseUnixDateTimeNanos' def
parseUnixDateTimeNanos' :: TimeLocale -> FormatText -> Text -> Either ParseError UnixDateTimeNanos
parseUnixDateTimeNanos' locale format text = fun <$> parseTimestamp locale Universal format text
where fun TZ{..} = createUnixDateTimeNanos _set_year _set_mon _set_mday hour _set_min sec nan
where hour = _set_ampm _set_hour
(,) sec nan = properFracNanos $ _set_frac _set_sec
parseUnixDateTimePicos :: FormatText -> Text -> Either ParseError UnixDateTimePicos
parseUnixDateTimePicos = parseUnixDateTimePicos' def
parseUnixDateTimePicos' :: TimeLocale -> FormatText -> Text -> Either ParseError UnixDateTimePicos
parseUnixDateTimePicos' locale format text = fun <$> parseTimestamp locale Universal format text
where fun TZ{..} = createUnixDateTimePicos _set_year _set_mon _set_mday hour _set_min sec pic
where hour = _set_ampm _set_hour
(,) sec pic = properFracPicos $ _set_frac _set_sec
parseLocalDate :: FormatText -> Text -> Either ParseError LocalDate
parseLocalDate = parseLocalDate' def Universal
parseLocalDate' :: TimeLocale -> City -> FormatText -> Text -> Either ParseError LocalDate
parseLocalDate' locale city format text = fun <$> parseTimestamp locale city format text
where fun TZ{..} = createLocalDate _set_year _set_mon _set_mday _set_zone
parseLocalDateTime :: FormatText -> Text -> Either ParseError LocalDateTime
parseLocalDateTime = parseLocalDateTime' def Universal
parseLocalDateTime' :: TimeLocale -> City -> FormatText -> Text -> Either ParseError LocalDateTime
parseLocalDateTime' locale city format text = fun <$> parseTimestamp locale city format text
where fun TZ{..} = createLocalDateTime _set_year _set_mon _set_mday hour _set_min sec _set_zone
where hour = _set_ampm _set_hour
sec = truncate _set_sec
parseLocalDateTimeMillis :: FormatText -> Text -> Either ParseError LocalDateTimeMillis
parseLocalDateTimeMillis = parseLocalDateTimeMillis' def Universal
parseLocalDateTimeMillis' :: TimeLocale -> City -> FormatText -> Text -> Either ParseError LocalDateTimeMillis
parseLocalDateTimeMillis' locale city format text = fun <$> parseTimestamp locale city format text
where fun TZ{..} = createLocalDateTimeMillis _set_year _set_mon _set_mday hour _set_min sec mil _set_zone
where hour = _set_ampm _set_hour
(,) sec mil = properFracMillis $ _set_frac _set_sec
parseLocalDateTimeMicros :: FormatText -> Text -> Either ParseError LocalDateTimeMicros
parseLocalDateTimeMicros = parseLocalDateTimeMicros' def Universal
parseLocalDateTimeMicros' :: TimeLocale -> City -> FormatText -> Text -> Either ParseError LocalDateTimeMicros
parseLocalDateTimeMicros' locale city format text = fun <$> parseTimestamp locale city format text
where fun TZ{..} = createLocalDateTimeMicros _set_year _set_mon _set_mday hour _set_min sec mic _set_zone
where hour = _set_ampm _set_hour
(,) sec mic = properFracMicros $ _set_frac _set_sec
parseLocalDateTimeNanos :: FormatText -> Text -> Either ParseError LocalDateTimeNanos
parseLocalDateTimeNanos = parseLocalDateTimeNanos' def Universal
parseLocalDateTimeNanos' :: TimeLocale -> City -> FormatText -> Text -> Either ParseError LocalDateTimeNanos
parseLocalDateTimeNanos' locale city format text = fun <$> parseTimestamp locale city format text
where fun TZ{..} = createLocalDateTimeNanos _set_year _set_mon _set_mday hour _set_min sec nan _set_zone
where hour = _set_ampm _set_hour
(,) sec nan = properFracNanos $ _set_frac _set_sec
parseLocalDateTimePicos :: FormatText -> Text -> Either ParseError LocalDateTimePicos
parseLocalDateTimePicos = parseLocalDateTimePicos' def Universal
parseLocalDateTimePicos' :: TimeLocale -> City -> FormatText -> Text -> Either ParseError LocalDateTimePicos
parseLocalDateTimePicos' locale city format text = fun <$> parseTimestamp locale city format text
where fun TZ{..} = createLocalDateTimePicos _set_year _set_mon _set_mday hour _set_min sec pic _set_zone
where hour = _set_ampm _set_hour
(,) sec pic = properFracPicos $ _set_frac _set_sec
initTZ :: TZ
initTZ = TZ 1970 January 1 Thursday 0 0 0.0 id id utc
parseTimestamp
:: TimeLocale
-> City
-> FormatText
-> Text
-> Either ParseError TZ
parseTimestamp locale city format text =
either left Right $ do
parser <- parseFormat locale city format
parseOnly parser text
where left = Left . ParseError
parseFormat
:: TimeLocale
-> City
-> FormatText
-> Either String (Parser TZ)
parseFormat locale city =
fmap exec . parseOnly parser
where parser = many' $ createParser locale city
exec x = flip execState initTZ <$> sequence <$> sequence x
createParser
:: TimeLocale
-> City
-> Parser (Parser (State TZ ()))
createParser locale city =
matchLit "%%"
<|> matchSet "%A" set_wday (weekLong locale)
<|> matchSet "%a" set_wday (weekShort locale)
<|> matchSet "%B" set_mon (monthLong locale)
<|> matchSet "%b" set_mon (monthShort locale)
<|> matchMDY "%D" set_year set_mon set_mday
<|> matchSet "%d" set_mday (fixInt 2)
<|> matchSet "%e" set_mday padIntTwo
<|> matchYMD "%F" set_year set_mon set_mday
<|> matchSet "%H" set_hour (fixInt 2)
<|> matchSet "%h" set_mon (monthShort locale)
<|> matchSet "%I" set_hour (fixInt 2)
<|> matchSet "%l" set_hour padIntTwo
<|> matchSet "%M" set_min (fixInt 2)
<|> matchSet "%m" set_mon monthInt
<|> matchSet "%P" set_ampm (period locale toLower)
<|> matchSet "%p" set_ampm (period locale id)
<|> matchSet "%Q" set_frac fraction
<|> matchHM "%R" set_hour set_min
<|> matchT12 "%r" set_hour set_min set_sec locale
<|> matchSet "%S" set_sec second
<|> matchHMS "%T" set_hour set_min set_sec
<|> matchSet "%Y" set_year (fixInt 4)
<|> matchSet "%y" set_year yearTwo
<|> matchSet "%Z" set_zone (timezone city)
<|> matchTxt
matchLit
:: Text
-> Parser (Parser (State TZ ()))
matchLit code =
string code *>
return (char '%' *> return (return ()))
matchSet
:: Text
-> (TZ :-> a)
-> Parser a
-> Parser (Parser (State TZ ()))
matchSet code field parser =
string code *> return (puts field <$> parser)
matchYMD
:: Text
-> (TZ :-> Year )
-> (TZ :-> Month)
-> (TZ :-> Day )
-> Parser (Parser (State TZ ()))
matchYMD code _year _mon _day =
string code *> return parser where
parser = do
y <- fixInt 4; _ <- char '-'
m <- monthInt; _ <- char '-'
d <- fixInt 2
return $!
puts _year y *>
puts _mon m *>
puts _day d
matchMDY
:: Text
-> (TZ :-> Year )
-> (TZ :-> Month)
-> (TZ :-> Day )
-> Parser (Parser (State TZ ()))
matchMDY code _year _mon _day =
string code *> return parser where
parser = do
m <- monthInt; _ <- char '/'
d <- fixInt 2; _ <- char '/'
y <- yearTwo
return $!
puts _year y *>
puts _mon m *>
puts _day d
matchHM
:: Text
-> (TZ :-> Hour )
-> (TZ :-> Minute)
-> Parser (Parser (State TZ ()))
matchHM code _hour _min =
string code *> return parser where
parser = do
h <- fixInt 2; _ <- char ':'
m <- fixInt 2
return $!
puts _hour h *>
puts _min m
matchHMS
:: Text
-> (TZ :-> Hour )
-> (TZ :-> Minute)
-> (TZ :-> Double)
-> Parser (Parser (State TZ ()))
matchHMS code _hour _min _sec =
string code *> return parser where
parser = do
h <- fixInt 2; _ <- char ':'
m <- fixInt 2; _ <- char ':'
s <- second
return $!
puts _hour h *>
puts _min m *>
puts _sec s
matchT12
:: Text
-> (TZ :-> Hour )
-> (TZ :-> Minute)
-> (TZ :-> Double)
-> TimeLocale
-> Parser (Parser (State TZ ()))
matchT12 code _hour _min _sec locale =
string code *> return parser where
parser = do
h <- fixInt 2; _ <- char ':'
m <- fixInt 2; _ <- char ':'
s <- second ; _ <- char ' '
f <- period locale id
return $!
puts _hour h *>
puts _min m *>
puts _sec s *>
modify _hour f
matchTxt :: Parser (Parser (State TZ ()))
matchTxt = takeWhile1 (/='%') >>= return . \ src -> do
trg <- P.take $ T.length src
if src == trg then return (return ())
else fail "matchTxt: mismatch"
fixInt :: Integral a => Int -> Parser a
fixInt n = do
s <- replicateM n digit
return $! fromIntegral $ L.foldl' step 0 s
where step a c = a * 10 + fromEnum c 48
padIntTwo :: Integral a => Parser a
padIntTwo = do
let f a b = a * 10 + b
liftM2 f getDigit getDigit
<|> do char ' ' >> getDigit
where getDigit = do
d <- digit
return $! fromIntegral $ fromEnum d 48
yearTwo :: Parser Year
yearTwo = f <$> fixInt 2
where f y = if y <= 69 then 2000 + y else 1900 + y
monthInt :: Parser Month
monthInt = do
m <- fixInt 2
if 1 <= m && m <= 12
then return $! toEnum (m1)
else fail $ "monthInt: out of bounds"
monthShort :: TimeLocale -> Parser Month
monthShort = fromList . flip L.zip monthList . L.map (pack . snd) . months
monthLong :: TimeLocale -> Parser Month
monthLong = fromList . flip L.zip monthList . L.map (pack . fst) . months
weekShort :: TimeLocale -> Parser DayOfWeek
weekShort = fromList . flip L.zip weekList . L.map (pack . snd) . wDays
weekLong :: TimeLocale -> Parser DayOfWeek
weekLong = fromList . flip L.zip weekList . L.map (pack . fst) . wDays
second :: Parser Double
second = (realToFrac :: Int -> Double) <$> fixInt 2
fraction :: Parser (Double -> Double)
fraction = do
(,) n l <- foldM step (0,0) [1..12]
return $! (+ realToFrac n * 10 ** ( realToFrac l))
where step :: (Int, Int) -> Int -> Parser (Int, Int)
step acc@(n,_) l = option acc . try $ do
c <- digit
let n' = n * 10 + fromEnum c 48
return $! (n', l)
period :: TimeLocale -> (Text -> Text) -> Parser (Hour -> Hour)
period TimeLocale{amPm = (am, pm)} casify = fromList
[(toText am, \ case 12 -> 00; x -> x )
,(toText pm, \ case 12 -> 12; x -> x + 12)]
where toText = casify . pack
timezone :: City -> Parser TimeZone
timezone city = do
t <- takeWhile1 isAlpha
case safeConvert . TimeZoneAbbr city $ unpack t of
Left err -> fail $ prettyConvertError err
Right zone -> return $! zone
fromList :: [(Text, a)] -> Parser a
fromList = L.foldl1 (<|>) . L.map (uncurry (*>) . (string *** return))
weekList :: [DayOfWeek]
weekList = [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
monthList :: [Month]
monthList = [January, February, March, April, May, June, July, August, September, October, November, December]