{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
module Data.OrgMode.Parse.Attoparsec.Time
( parsePlannings
, parseClock
, parseTimestamp
)
where
import Control.Applicative
import qualified Data.Attoparsec.ByteString as Attoparsec.ByteString
import Data.Attoparsec.Combinator as Attoparsec
import Data.Attoparsec.Text
import Data.Attoparsec.Types as Attoparsec (Parser)
import qualified Data.ByteString.Char8 as BS
import Data.Functor (($>))
import Data.HashMap.Strict (HashMap, fromList)
import Data.Maybe (listToMaybe)
import Data.Monoid ()
import qualified Data.OrgMode.Parse.Attoparsec.Util as Util
import Data.OrgMode.Types
import Data.Semigroup ((<>))
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Thyme.Format (buildTime, timeParser)
import Data.Thyme.LocalTime (Hours, Minutes)
import Prelude hiding (repeat)
import System.Locale (defaultTimeLocale)
parsePlannings :: Attoparsec.Parser Text (HashMap PlanningKeyword Timestamp)
parsePlannings = fromList <$> many' (skipSpace *> planning <* Util.skipOnlySpace)
where
planning = (,) <$> pType <* char ':' <*> (skipSpace *> parseTimestamp)
pType = choice [string "SCHEDULED" $> SCHEDULED
,string "DEADLINE" $> DEADLINE
,string "CLOSED" $> CLOSED
]
parseClock :: Attoparsec.Parser Text Clock
parseClock = Clock <$> ((,) <$> (skipSpace *> string "CLOCK: " *> ts) <*> dur)
where
ts = optional parseTimestamp
dur = optional (string " => " *> skipSpace *> parseHM)
parseTimestamp :: Attoparsec.Parser Text Timestamp
parseTimestamp = do
(ts1, tsb1, act) <- transformBracketedDateTime <$> parseBracketedDateTime
blk2 <- fmap (fmap transformBracketedDateTime) optionalBracketedDateTime
case (tsb1, blk2) of
(Nothing, Nothing) ->
pure (Timestamp ts1 act Nothing)
(Nothing, Just (ts2, Nothing, _)) ->
pure (Timestamp ts1 act (Just ts2))
(Nothing, Just _) ->
fail "Illegal time range in second timerange timestamp"
(Just (h',m'), Nothing) ->
pure (Timestamp ts1 act
(Just $ ts1 {hourMinute = Just (h',m')
,repeater = Nothing
,delay = Nothing}))
(Just _, Just _) ->
fail "Illegal mix of time range and timestamp range"
where
optionalBracketedDateTime =
optional (string "--" *> parseBracketedDateTime)
parseBracketedDateTime :: Attoparsec.Parser Text BracketedDateTime
parseBracketedDateTime = do
openingBracket <- char '<' <|> char '['
brkDateTime <- BracketedDateTime <$>
parseDate <* skipSpace
<*> optionalParse parseDay
<*> optionalParse parseTime'
<*> maybeListParse parseRepeater
<*> maybeListParse parseDelay
<*> pure (activeBracket openingBracket)
closingBracket <- char '>' <|> char ']'
finally brkDateTime openingBracket closingBracket
where
optionalParse p = optional p <* skipSpace
maybeListParse p = listToMaybe <$> many' p <* skipSpace
activeBracket ((=='<') -> active) =
if active then Active else Inactive
finally bkd ob cb | complementaryBracket ob /= cb =
fail "mismatched timestamp brackets"
| otherwise = return bkd
complementaryBracket '<' = '>'
complementaryBracket '[' = ']'
complementaryBracket x = x
transformBracketedDateTime :: BracketedDateTime
-> (DateTime, Maybe (Hours, Minutes), ActiveState)
transformBracketedDateTime BracketedDateTime{..} =
maybe dateStamp timeStamp timePart
where
defdt = DateTime datePart dayNamePart Nothing repeat delayPart
timeStamp (AbsoluteTime (hs,ms)) =
( defdt { hourMinute = Just (hs,ms) }
, Nothing
, activeState
)
timeStamp (TimeStampRange (t0,t1)) =
( defdt { hourMinute = Just t0 }
, Just t1
, activeState
)
dateStamp = (defdt, Nothing, activeState)
parseDay :: Attoparsec.Parser Text Text
parseDay = Text.pack <$> some (Attoparsec.satisfyElem isDayChar)
where
isDayChar :: Char -> Bool
isDayChar = (`notElem` nonDayChars)
nonDayChars = "]+0123456789>\r\n -" :: String
parseTime' :: Attoparsec.Parser Text TimePart
parseTime' = stampRng <|> stampAbs
where
stampRng = do
beg <- parseHM <* char '-'
end <- parseHM
pure $ TimeStampRange (beg,end)
stampAbs = AbsoluteTime <$> parseHM
parseDate :: Attoparsec.Parser Text YearMonthDay
parseDate = consumeDate >>= either bad good . dateParse
where
bad e = fail $ "failure parsing date: " <> e
good t = pure $ buildTime t
consumeDate = manyTill anyChar $ char ' '
dateParse = Attoparsec.ByteString.parseOnly dpCombinator . BS.pack
dpCombinator = timeParser defaultTimeLocale "%Y-%m-%d"
parseHM :: Attoparsec.Parser Text (Hours, Minutes)
parseHM = (,) <$> decimal <* char ':' <*> decimal
parseTimeUnit :: Attoparsec.Parser Text TimeUnit
parseTimeUnit =
choice [ char 'h' $> UnitHour
, char 'd' $> UnitDay
, char 'w' $> UnitWeek
, char 'm' $> UnitMonth
, char 'y' $> UnitYear
]
parseRepeater :: Attoparsec.Parser Text Repeater
parseRepeater =
Repeater
<$> choice
[ string "++" $> RepeatCumulate
, char '+' $> RepeatCatchUp
, string ".+" $> RepeatRestart
]
<*> decimal
<*> parseTimeUnit
parseDelay :: Attoparsec.Parser Text Delay
parseDelay =
Delay
<$> choice
[ string "--" $> DelayFirst
, char '-' $> DelayAll
]
<*> decimal
<*> parseTimeUnit