{-# 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 Data.Text (Text)
import qualified Data.Text as Text
import Data.Thyme.Format (buildTime, timeParser)
import Data.Thyme.LocalTime (Hours, Minutes)
import System.Locale (defaultTimeLocale)
import Data.Semigroup ((<>))
import Data.OrgMode.Types
parsePlannings :: Attoparsec.Parser Text (HashMap PlanningKeyword Timestamp)
parsePlannings = fromList <$> many' (skipSpace *> planning <* skipSpace)
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 = option Nothing (Just <$> parseTimestamp)
dur = option Nothing (Just <$> (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 =
option Nothing (Just <$> (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 = option Nothing (Just <$> 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