module Data.Time.Exts.Parser (
runParser
, ParserState(..)
, defaultParserState
, ps_year
, ps_mon
, ps_mday
, ps_wday
, ps_hour
, ps_min
, ps_sec
, ps_frac
, ps_ampm
, ps_zone
) where
import Control.Applicative ((<|>))
import Control.Arrow ((***))
import Control.Monad (foldM, join, replicateM)
import Control.Monad.State.Strict (State, execState)
import Data.Attoparsec.Text (Parser, char, digit, many', option, parseOnly, string, take, takeWhile1, try)
import Data.Char (isAlpha)
import Data.Foldable (asum)
import Data.Int (Int64)
import Data.Text (Text, length, pack, toLower, unpack)
import Data.Time (TimeZone(..), utc)
import Data.Time.Exts.Base (Calendar(..), Day, DayOfWeek(..), Hour, Minute, Month(..), Year)
import Data.Time.Exts.Format (Format)
import Data.Time.Zones (LocalToUTCResult(..), TZ, localTimeToUTCFull)
import Data.Time.Zones.Internal (int64PairToLocalTime)
import Lens.Simple (Setter', (.=), makeLenses)
import Prelude hiding (length, take)
import System.Locale (TimeLocale(..))
data ParserState (cal :: Calendar) =
ParserState
{ _ps_year :: Year
, _ps_mon :: Month cal
, _ps_mday :: Day
, _ps_wday :: DayOfWeek cal
, _ps_hour :: Hour
, _ps_min :: Minute
, _ps_sec :: Double
, _ps_frac :: Double -> Double
, _ps_ampm :: Hour -> Hour
, _ps_zone :: Int64 -> Either String TimeZone
}
defaultParserState :: ParserState 'Gregorian
defaultParserState = ParserState 1970 January 1 Thursday 0 0 0.0 id id (const (return utc))
makeLenses ''ParserState
runParser
:: Bounded (Month cal)
=> Enum (DayOfWeek cal)
=> Enum (Month cal)
=> TimeLocale
-> Maybe TZ
-> ParserState cal
-> Format
-> Text
-> Either String (ParserState cal)
runParser locale tzdata state format input = join (fmap (flip parseOnly input . fmap (flip execState state . sequence) . sequence) (parseOnly (many' (create locale tzdata)) format))
create
:: Bounded (Month cal)
=> Enum (DayOfWeek cal)
=> Enum (Month cal)
=> TimeLocale
-> Maybe TZ
-> Parser (Parser (State (ParserState cal) ()))
create locale tzdata =
percent
<|> match "%A" ps_wday (dayLong locale)
<|> match "%B" ps_mon (monthLong locale)
<|> match "%H" ps_hour (int 2)
<|> match "%I" ps_hour (int 2)
<|> match "%M" ps_min (int 2)
<|> match "%P" ps_ampm (period locale toLower)
<|> match "%S" ps_sec (second)
<|> match "%Y" ps_year (int 4)
<|> match "%Z" ps_zone (zone tzdata)
<|> match "%a" ps_wday (dayShort locale)
<|> match "%b" ps_mon (monthShort locale)
<|> match "%d" ps_mday (int 2)
<|> match "%e" ps_mday (int'2)
<|> match "%f" ps_frac (decimal)
<|> match "%h" ps_mon (monthShort locale)
<|> match "%l" ps_hour (int'2)
<|> match "%m" ps_mon (month)
<|> match "%p" ps_ampm (period locale id)
<|> match "%y" ps_year (year)
<|> match "%z" ps_zone (zone tzdata)
<|> america
<|> iso8601
<|> clock12 locale
<|> clock24
<|> clock24Short
<|> text
percent :: Parser (Parser (State (ParserState cal) ()))
percent = do
_ <- string "%%"
return $ do
_ <- char '%'
return $ return ()
match
:: Enum (DayOfWeek cal)
=> Enum (Month cal)
=> Text
-> Setter' (ParserState cal) a
-> Parser a
-> Parser (Parser (State (ParserState cal) ()))
match code field parser = do
_ <- string code
return $ do
p <- parser
return $ field .= p
america :: Bounded (Month cal) => Enum (Month cal) => Parser (Parser (State (ParserState cal) ()))
america = do
_ <- string "%D"
return $ do
m <- month
_ <- char '/'
d <- int 2
_ <- char '/'
y <- year
return $ do
ps_year .= y
ps_mon .= m
ps_mday .= d
iso8601 :: Bounded (Month cal) => Enum (Month cal) => Parser (Parser (State (ParserState cal) ()))
iso8601 = do
_ <- string "%F"
return $ do
y <- int 4
_ <- char '-'
m <- month
_ <- char '-'
d <- int 2
return $ do
ps_year .= y
ps_mon .= m
ps_mday .= d
clock12 :: TimeLocale -> Parser (Parser (State (ParserState cal) ()))
clock12 locale = do
_ <- string "%r"
return $ do
h <- int 2
_ <- char ':'
m <- int 2
_ <- char ':'
s <- second
_ <- char ' '
p <- period locale id
return $ do
ps_hour .= h
ps_min .= m
ps_sec .= s
ps_ampm .= p
clock24 :: Parser (Parser (State (ParserState cal) ()))
clock24 = do
_ <- string "%T"
return $ do
h <- int 2
_ <- char ':'
m <- int 2
_ <- char ':'
s <- second
return $ do
ps_hour .= h
ps_min .= m
ps_sec .= s
clock24Short :: Parser (Parser (State (ParserState cal) ()))
clock24Short = do
_ <- string "%R"
return $ do
h <- int 2
_ <- char ':'
m <- int 2
return $ do
ps_hour .= h
ps_min .= m
text :: Parser (Parser (State (ParserState cal) ()))
text = do
src <- takeWhile1 (/='%')
return $ do
tgt <- take $ length src
if src == tgt
then return $ return ()
else fail "text: mismatch"
fromList :: [(Text, a)] -> Parser a
fromList = asum . fmap (uncurry (*>) . (string *** return))
int :: Integral a => Read a => Int -> Parser a
int = fmap (fromInteger . read) . flip replicateM digit
int'2 :: Integral a => Read a => Parser a
int'2 = int 2 <|> (char ' ' *> int 1)
year :: Parser Year
year = fix <$> int 2 where fix n = n + if n < 70 then 2000 else 1900
month :: forall cal . Bounded (Month cal) => Enum (Month cal) => Parser (Month cal)
month = do
n <- int 2
if fromEnum (minBound :: Month cal) <= n &&
fromEnum (maxBound :: Month cal) >= n
then return $! toEnum n
else fail "month: out of bounds"
monthShort :: Enum (Month cal) => TimeLocale -> Parser (Month cal)
monthShort = fromList . zipWith (\n (_, t) -> (pack t, toEnum n)) [1..] . months
monthLong :: Enum (Month cal) => TimeLocale -> Parser (Month cal)
monthLong = fromList . zipWith (\n (t, _) -> (pack t, toEnum n)) [1..] . months
dayShort :: Enum (DayOfWeek cal) => TimeLocale -> Parser (DayOfWeek cal)
dayShort = fromList . zipWith (\n (_, t) -> (pack t, toEnum n)) [1..] . wDays
dayLong :: Enum (DayOfWeek cal) => TimeLocale -> Parser (DayOfWeek cal)
dayLong = fromList . zipWith (\n (t, _) -> (pack t, toEnum n)) [1..] . wDays
second :: Parser Double
second = toEnum <$> int 2
decimal :: Parser (Double -> Double)
decimal = char '.' *> do
(n, m) <- foldM step (0, 0) [1.. 9]
return $ (+) (toEnum n * 10 ** ( toEnum m))
where step :: (Int, Int) -> Int -> Parser (Int, Int)
step acc@(!n, _) m = option acc (try (fmap ((, m) . (+) (10 * n) . subtract 48 . fromEnum) digit))
period :: TimeLocale -> (Text -> Text) -> Parser (Hour -> Hour)
period TimeLocale { amPm = (am, pm) } format = fromList
[
(format (pack am), \ case 12 -> 00; x -> x),
(format (pack pm), \ case 12 -> 12; x -> x + 12)
]
zone :: Maybe TZ -> Parser (Int64 -> Either String TimeZone)
zone = \ case
Nothing -> fail "zone: no time zone data"
Just tzdata -> do
name <- unpack <$> takeWhile1 isAlpha
return $ \ base -> do
let time = int64PairToLocalTime base 0
case localTimeToUTCFull tzdata time of
LTUUnique {..} | timeZoneName _ltuZone == name -> Right _ltuZone
LTUAmbiguous {..} | timeZoneName _ltuFirstZone == name -> Right _ltuFirstZone
LTUAmbiguous {..} | timeZoneName _ltuSecondZone == name -> Right _ltuSecondZone
_ -> Left $ "unmatched acronym: " ++ name