-------------------------------------------------------------------------------- -- Copyright (c) 2014, Enzo Haussecker, Steve Severance. All rights reserved. -- -------------------------------------------------------------------------------- {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UnicodeSyntax #-} {-# OPTIONS -Wall #-} -- | Timestamp parsers and related utilities. module Data.Time.Exts.Parser ( -- ** Utilities FormatText , ParseError(..) -- ** Parse Unix Timestamps , parseUnixDate , parseUnixTime , parseUnixTimeMillis , parseUnixTimeMicros , parseUnixTimeNanos , parseUnixTimePicos , parseUnixDateTime , parseUnixDateTimeMillis , parseUnixDateTimeMicros , parseUnixDateTimeNanos , parseUnixDateTimePicos -- ** Parse UTC and Local Timestamps , parseLocalDate , parseLocalDateTime , parseLocalDateTimeMillis , parseLocalDateTimeMicros , parseLocalDateTimeNanos , parseLocalDateTimePicos -- ** Parse Unix Timestamps With Parameters , parseUnixDate' , parseUnixTime' , parseUnixTimeMillis' , parseUnixTimeMicros' , parseUnixTimeNanos' , parseUnixTimePicos' , parseUnixDateTime' , parseUnixDateTimeMillis' , parseUnixDateTimeMicros' , parseUnixDateTimeNanos' , parseUnixDateTimePicos' -- ** Parse UTC and Local Timestamps With Parameters , 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(..)) -- | The format string is composed of various %-codes, each -- representing time-related information described below. -- -- [@%%@] A literal '%' character. -- -- [@%A@] The full weekday name according to the current locale. -- -- [@%a@] The abbreviated weekday name according to the current locale. -- -- [@%B@] The full month name according to the current locale. -- -- [@%b@] The abbreviated month name according to the current locale. -- -- [@%D@] Equivalent to %m\/%d\/%y. -- -- [@%d@] The day of the month as a decimal number (range 01 to 31). -- -- [@%e@] Like %d, the day of the month as a decimal number, but a leading zero is replaced by a space. -- -- [@%F@] Equivalent to %Y-%m-%d (the ISO 8601 date format). -- -- [@%H@] The hour as a decimal number using a 24-hour clock (range 00 to 23). -- -- [@%h@] Equivalent to %b. -- -- [@%I@] The hour as a decimal number using a 12-hour clock (range 01 to 12). -- -- [@%l@] Like %I, the hour as a decimal number using a 12-hour clock, but a leading zero is replaced by a space. -- -- [@%M@] The minute as a decimal number (range 00 to 59). -- -- [@%m@] The month as a decimal number (range 01 to 12). -- -- [@%P@] Like %p, the period of the day according to the current locale, but lowercase. -- -- [@%p@] The period of the day according to the current locale. -- -- [@%Q@] The fraction of the second as a decimal number (range 0 to 999999999999). -- -- [@%R@] Equivalent to %H:%M. -- -- [@%r@] Equivalent to %I:%M:%S %p. -- -- [@%S@] The second as a decimal number (range 00 to 60). -- -- [@%T@] Equivalent to %H:%M:%S. -- -- [@%Y@] The year as a decimal number (range 1970 to 9999). -- -- [@%y@] The year as a decimal number without a century (range 00 to 99). -- -- [@%Z@] The timezone abbreviation. type FormatText = Text -- | Error handling type. newtype ParseError = ParseError String deriving (Show,Typeable) instance Exception ParseError instance IsString ParseError where fromString = ParseError -- | A struct with date, time, and time zone -- components, plus component modifiers. 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] -- | Parse a Unix date. -- -- > >>> parseUnixDate "%A, %B %e, %Y" "Tuesday, March 4, 2014" -- > Right 2014-03-04 -- parseUnixDate :: FormatText -> Text -> Either ParseError UnixDate parseUnixDate = parseUnixDate' def -- | Same as @parseUnixDate@, except takes an additional locale parameter. -- -- > >>> let german = defaultTimeLocale { wDays = [("Sonntag","So"),("Montag","Mo")... -- > >>> parseUnixDate' german "%A, %B %e, %Y" "Dienstag, März 4, 2014" -- > Right 2014-03-04 -- 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 -- | Parse a Unix time. -- -- > >>> parseUnixTime "%T" "15:32:19" -- > Right 15:32:19 -- parseUnixTime :: FormatText -> Text -> Either ParseError UnixTime parseUnixTime = parseUnixTime' def -- | Same as @parseUnixTime@, except takes an additional locale parameter. -- -- > >>> let albanian = defaultTimeLocale { wDays = [("e diel","Die"),("e hënë ","Hën")... -- > >>> parseUnixTime' albanian "%l:%M:%S %p" "12:28:47 PD" -- > Right 00:28:47 -- 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 -- | Parse a Unix time with millisecond granularity. -- -- > >>> parseUnixTimeMillis "%I:%M:%S.%Q %p" "09:41:09.313 PM" -- > Right 21:41:09.313 -- parseUnixTimeMillis :: FormatText -> Text -> Either ParseError UnixTimeMillis parseUnixTimeMillis = parseUnixTimeMillis' def -- | Same as @parseUnixTimeMillis@, except takes an additional locale parameter. -- -- > >>> let urdu = defaultTimeLocale { wDays = [("پير","پير"),("اتوار","اتوار")... -- > >>> parseUnixTimeMillis' urdu "%l:%M:%S.%Q %p" " 3:12:47.624 ش" -- > Right 15:12:47.624 -- 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 -- | Parse a Unix time with microsecond granularity. -- -- > >>> parseUnixTimeMicros "%R:%S.%Q" "03:15:50.513439" -- > Right 03:15:50.513439 -- parseUnixTimeMicros :: FormatText -> Text -> Either ParseError UnixTimeMicros parseUnixTimeMicros = parseUnixTimeMicros' def -- | Same as @parseUnixTimeMicros@, except takes an additional locale parameter. -- -- > >>> let chinese = defaultTimeLocale { wDays = [("星期日","日"),("星期一","一")... -- > >>> parseUnixTimeMicros' chinese "%p%I:%M:%S.%Q" "下午11:46:18.130561" -- > Right 23:46:18.130561 -- 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 -- | Parse a Unix time with nanosecond granularity. -- -- > >>> parseUnixTimeNanos "%l:%M:%S.%Q %P" " 1:27:44.001256754 pm" -- > Right 13:27:44.001256754 -- parseUnixTimeNanos :: FormatText -> Text -> Either ParseError UnixTimeNanos parseUnixTimeNanos = parseUnixTimeNanos' def -- | Same as @parseUnixTimeNanos@, except takes an additional locale parameter. -- -- > >>> let swahili = defaultTimeLocale { wDays = [("Jumapili","J2"),("Jumatatu","J3")... -- > >>> parseUnixTimeNanos' swahili "%H:%M:%S.%Q %p" "12:05:50.547621324 asubuhi" -- > Right 00:05:50.547621324 -- 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 -- | Parse a Unix time with picosecond granularity. -- -- > >>> parseUnixTimePicos "%T.%Q" "13:09:23.247795919586" -- > Right 13:09:23.247795919586 -- parseUnixTimePicos :: FormatText -> Text -> Either ParseError UnixTimePicos parseUnixTimePicos = parseUnixTimePicos' def -- | Same as @parseUnixTimePicos@, except takes an additional locale parameter. -- -- > >>> let japanese = defaultTimeLocale { wDays = [("日曜日","日"),("月曜日","月")... -- > >>> parseUnixTimePicos' japanese "%I:%M:%S.%Q %p" "04:20:15.340563315063 午前" -- > Right 04:20:15.340563315063 -- 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 -- | Parse a Unix date and time. -- -- > >>> parseUnixDateTime "%FT%TZ" "2014-02-27T11:31:20Z" -- > Right 2014-02-27 11:31:20 -- parseUnixDateTime :: FormatText -> Text -> Either ParseError UnixDateTime parseUnixDateTime = parseUnixDateTime' def -- | Same as @parseUnixDateTime@, except takes an additional locale parameter. -- -- > >>> let somali = defaultTimeLocale { wDays = [("Axad","Axa"),("Isniin","Isn")... -- > >>> parseUnixDateTime' somali "%A, %B %e, %r %Y" "Salaaso, Bisha Saddexaad 11, 03:41:33 galabnimo 2014" -- > Right 2014-03-11 15:41:33 -- 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 -- | Parse a Unix date and time with millisecond granularity. -- -- > >>> parseUnixDateTimeMillis "%a %B %e %I:%M:%S.%Q %p %Y" "Wed March 5 06:53:04.475 PM 2014" -- > Right 2014-03-05 18:53:04.475 -- parseUnixDateTimeMillis :: FormatText -> Text -> Either ParseError UnixDateTimeMillis parseUnixDateTimeMillis = parseUnixDateTimeMillis' def -- | Same as @parseUnixDateTimeMillis@, except takes an additional locale parameter. -- -- > >>> let turkish = defaultTimeLocale { wDays = [("Pazar","Paz"),("Pazartesi","Pzt")... -- > >>> parseUnixDateTimeMillis' turkish "%a %B %e %I:%M:%S.%Q %p %Y" "Prş Mart 13 07:22:54.324 ÖS 2014" -- > Right 2014-03-13 19:22:54.324 -- 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 -- | Parse a Unix date and time with microsecond granularity. -- -- > >>> parseUnixDateTimeMicros "%D %T.%Q" "03/06/14 17:26:55.148415" -- > Right 2014-03-06 17:26:55.148415 -- parseUnixDateTimeMicros :: FormatText -> Text -> Either ParseError UnixDateTimeMicros parseUnixDateTimeMicros = parseUnixDateTimeMicros' def -- | Same as @parseUnixDateTimeMicros@, except takes an additional locale parameter. -- -- > >>> let angika = defaultTimeLocale { wDays = [("रविवार","रवि"),("सोमवार","सोम")... -- > >>> parseUnixDateTimeMicros' angika "%A %d %B %Y %I:%M:%S.%Q %p" "शुक्रवार 07 मार्च 2014 07:10:50.283025 अपराह्न" -- > Right 2014-03-07 19:10:50.283025 -- 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 -- | Parse a Unix date and time with nanosecond granularity. -- -- > >>> parseUnixDateTimeNanos "%d.%m.%Y %I:%M:%S.%Q %p" "18.03.2014 07:06:43.774295132 PM" -- > Right 2014-03-18 19:06:43.774295132 -- parseUnixDateTimeNanos :: FormatText -> Text -> Either ParseError UnixDateTimeNanos parseUnixDateTimeNanos = parseUnixDateTimeNanos' def -- | Same as @parseUnixDateTimeNanos@, except takes an additional locale parameter. -- -- > >>> let russian = defaultTimeLocale { wDays = [("Воскресенье","Вс"),("Понедельник","Пн")... -- > >>> parseUnixDateTimeNanos' russian "%a %d %b %Y %T.%Q" "Ср 11 дек 2013 22:17:42.146648836" -- > Right 2013-12-11 22:17:42.146648836 -- 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 -- | Parse a Unix date and time with picosecond granularity. -- -- > >>> parseUnixDateTimePicos "%FT%T.%QZ" "2014-03-03T17:58:15.916795765305Z" -- > Right 2014-03-03 17:58:15.916795765305 -- parseUnixDateTimePicos :: FormatText -> Text -> Either ParseError UnixDateTimePicos parseUnixDateTimePicos = parseUnixDateTimePicos' def -- | Same as @parseUnixDateTimePicos@, except takes an additional locale parameter. -- -- > >>> let norwegian = defaultTimeLocale { wDays = [("søndag","sø."),("mandag","ma.")... -- > >>> parseUnixDateTimePicos' norwegian "%a %d. %b %Y kl. %H.%M.%S.%Q" "fr. 07. mars 2014 kl. 21.11.55.837472109433" -- > Right 2014-03-07 21:11:55.837472109433 -- 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 -- | Parse a local date. -- -- > >>> parseLocalDate "%A, %B %e, %Y (%Z)" "Monday, March 17, 2014 (PST)" -- > Right 2014-03-17 PST -- parseLocalDate :: FormatText -> Text -> Either ParseError LocalDate parseLocalDate = parseLocalDate' def Universal -- | Same as @parseLocalDate@, except takes an additional locale and city parameter. -- -- > >>> parseLocalDate' defaultTimeLocale Kolkata "%A, %B %e, %Y (%Z)" "Monday, March 17, 2014 (IST)" -- > Right 2014-03-17 IST -- -- Note that the city parameter is required to distinguish between the India and Israel. 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 -- | Parse a local date and time. -- -- > >>> parseLocalDateTime "%a %b %e %H:%M:%S %Z %Y" "Fri Mar 14 09:29:53 EST 2014" -- > Right 2014-03-14 09:29:53 EST -- parseLocalDateTime :: FormatText -> Text -> Either ParseError LocalDateTime parseLocalDateTime = parseLocalDateTime' def Universal -- | Same as @parseLocalDateTime@, except takes an additional locale and city parameter. -- -- > >>> let french = defaultTimeLocale { wDays = [("dimanche","dim."),("lundi","lun.")... -- > >>> parseLocalDateTime' french Paris "%a %d %b %T %Z %Y" "ven. 07 mars 22:49:03 UTC 2014" -- > Right 2014-03-07 22:49:03 UTC -- 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 -- | Parse a local date and time with millisecond granularity. -- -- > >>> parseLocalDateTimeMillis "%B %e %Y %I:%M:%S.%Q %p %Z" "July 1 2012 01:59:60.215 AM EET" -- > Right 2012-07-01 01:59:60.215 EET -- -- Note that the timestamp in the example above corresponds to a leap second. parseLocalDateTimeMillis :: FormatText -> Text -> Either ParseError LocalDateTimeMillis parseLocalDateTimeMillis = parseLocalDateTimeMillis' def Universal -- | Same as @parseLocalDateTimeMillis@, except takes an additional locale and city parameter. -- -- > >>> parseLocalDateTimeMillis' defaultTimeLocale Chicago "%B %e %Y %I:%M:%S.%Q %p %Z" "July 13 2013 12:15:30.985 AM CDT" -- > Right 2013-07-13 00:15:30.985 CDT -- -- Note that the city parameter is required to distinguish between the United States and China. 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 -- | Parse a local date and time with microsecond granularity. -- -- > >>> parseLocalDateTimeMicros "%F %T.%Q (%Z)" "2014-03-04 02:45:42.827495 (HKT)" -- > Right 2014-03-04 02:45:42.827495 HKT -- parseLocalDateTimeMicros :: FormatText -> Text -> Either ParseError LocalDateTimeMicros parseLocalDateTimeMicros = parseLocalDateTimeMicros' def Universal -- | Same as @parseLocalDateTimeMicros@, except takes an additional locale and city parameter. -- -- > >>> let spanish = defaultTimeLocale { wDays = [("domingo","dom"),("lunes","lun")... -- > >>> parseLocalDateTimeMicros' spanish Paris "%a %d %b %I:%M:%S.%Q %P %Y %Z" "dom 26 ene 04:27:16.743312 pm 2014 CET" -- > Right 2014-01-26 16:27:16.743312 CET -- 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 -- | Parse a local date and time with nanosecond granularity. -- -- > >>> parseLocalDateTimeNanos "%b. %d, %T.%Q %Z %Y" "Mar. 09, 18:53:55.856423459 UTC 2014" -- > Right 2014-03-09 18:53:55.856423459 UTC -- parseLocalDateTimeNanos :: FormatText -> Text -> Either ParseError LocalDateTimeNanos parseLocalDateTimeNanos = parseLocalDateTimeNanos' def Universal -- | Same as @parseLocalDateTimeNanos@, except takes an additional locale and city parameter. -- -- > >>> let italian = defaultTimeLocale { wDays = [("domenica","dom"),("lunedì","lun")... -- > >>> parseLocalDateTimeNanos' italian Paris "%a %e %b %Y %T.%Q %Z" "sab 12 apr 2014 04:59:21.528207540 CEST" -- > Right 2014-04-12 04:59:21.528207540 CEST -- 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 -- | Parse a local date and time with picosecond granularity. -- -- > >>> parseLocalDateTimePicos "%d.%m.%Y %T.%Q %Z" "09.04.2014 05:22:56.587234905781 SGT" -- > Right 2014-04-09 05:22:56.587234905781 SGT -- parseLocalDateTimePicos :: FormatText -> Text -> Either ParseError LocalDateTimePicos parseLocalDateTimePicos = parseLocalDateTimePicos' def Universal -- | Same as @parseLocalDateTimePicos@, except takes an additional locale and city parameter. -- -- > >>> parseLocalDateTimePicos' defaultTimeLocale Shanghai "%a %b %d %Y %T.%Q %Z" "Sat Mar 08 2014 22:51:47.264356423524 CST" -- > Right 2014-03-08 22:51:47.264356423524 CST -- -- Note that the city parameter is required to distinguish between the United States and China. 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 -- | Initialize timestamp components. initTZ :: TZ initTZ = TZ 1970 January 1 Thursday 0 0 0.0 id id utc -- | Parse timestamp components. 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 -- | Parse format text. 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 -- | Create a format text parser. 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 -- | Match a percent literal. matchLit :: Text -> Parser (Parser (State TZ ())) matchLit code = string code *> return (char '%' *> return (return ())) -- | Match a percent code and update the field -- with the value returned by the parser. matchSet :: Text -> (TZ :-> a) -> Parser a -> Parser (Parser (State TZ ())) matchSet code field parser = string code *> return (puts field <$> parser) -- | Match a year-month-day percent code and update -- the fields with the values returned by the 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 -- | Match a month-day-year percent code and update -- the fields with the values returned by the parser. 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 -- | Match a hour-minute percent code and update the -- fields with the values returned by the parser. 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 -- | Match a hour-minute-second percent code and update -- the fields with the values returned by the parser. 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 -- | Match a hour-minute-second-period percent code and -- update the fields with the values returned by the parser. 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 -- | Match any other character sequence. 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" -- | Parse an integral type of exactly @n@ digits. 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 -- | Parse an integral type of two digits -- or one digit preceded by a space. 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 -- | Parse a year in two digit format. yearTwo :: Parser Year yearTwo = f <$> fixInt 2 where f y = if y <= 69 then 2000 + y else 1900 + y -- | Parse a month in two digit format. monthInt :: Parser Month monthInt = do m <- fixInt 2 if 1 <= m && m <= 12 then return $! toEnum (m-1) else fail $ "monthInt: out of bounds" -- | Parse a month in short text format. monthShort :: TimeLocale -> Parser Month monthShort = fromList . flip L.zip monthList . L.map (pack . snd) . months -- | Parse a month in long text format. monthLong :: TimeLocale -> Parser Month monthLong = fromList . flip L.zip monthList . L.map (pack . fst) . months -- | Parse a day of week in short text format. weekShort :: TimeLocale -> Parser DayOfWeek weekShort = fromList . flip L.zip weekList . L.map (pack . snd) . wDays -- | Parse a day of week in long text format. weekLong :: TimeLocale -> Parser DayOfWeek weekLong = fromList . flip L.zip weekList . L.map (pack . fst) . wDays -- | Parse a second in two digit format. second :: Parser Double second = (realToFrac :: Int -> Double) <$> fixInt 2 -- | Parse a decimal in zero to twelve digit format. 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) -- | Parse period symbols. 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 -- | Parse a time zone. 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 -- | Create a parser from a list of key-value pairs. fromList :: [(Text, a)] -> Parser a fromList = L.foldl1 (<|>) . L.map (uncurry (*>) . (string *** return)) -- | List of days of the week. weekList :: [DayOfWeek] weekList = [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] -- | List of months of the year. monthList :: [Month] monthList = [January, February, March, April, May, June, July, August, September, October, November, December]