{-# LANGUAGE OverloadedStrings #-}
module Data.Time.ISO8601.Interval (
IntervalSpec (..)
, Interval (..)
, interval
, isoTime
, parseInterval
, formatInterval
, formatIntervalB
) where
import Data.Time.ISO8601.Duration
import Control.Applicative
import Data.Attoparsec.ByteString (Parser)
import Data.Attoparsec.ByteString.Char8 as AP
import Data.ByteString.Lex.Integral (readDecimal)
import Data.ByteString (ByteString)
import Data.ByteString.Lazy (toStrict)
import Data.ByteString.Builder
import Data.Monoid ((<>))
import Data.String (IsString(..))
import Data.Time
import Data.Time.Calendar (fromGregorian)
import Data.Time.Calendar.WeekDate (fromWeekDateValid)
data IntervalSpec
= StartEnd UTCTime UTCTime
| StartDuration UTCTime Duration
| DurationEnd Duration UTCTime
| JustDuration Duration
deriving ( Eq, Show )
data Interval
= Interval IntervalSpec
| RecurringInterval IntervalSpec (Maybe Integer)
deriving ( Eq, Show )
parseInterval :: ByteString -> Either String Interval
parseInterval = parseOnly (interval <* endOfInput)
interval :: Parser Interval
interval = recurringInterval
<|> simpleInterval
where
recurringInterval =
flip RecurringInterval <$> (char 'R' *> optional decimal <* char '/')
<*> intervalSpec
simpleInterval = Interval <$> intervalSpec
intervalSpec :: Parser IntervalSpec
intervalSpec = startEnd
<|> startDuration
<|> durationEnd
<|> justDuration
where
startEnd = StartEnd <$> isoTime <*> (char '/' *> isoTime)
startDuration = StartDuration <$> isoTime <*> (char '/' *> duration)
durationEnd = DurationEnd <$> duration <*> (char '/' *> isoTime)
justDuration = JustDuration <$> duration
isoTime :: Parser UTCTime
isoTime = do
d <- dayWeek <|> day
dt <- option 0 (oChar 'T' *> diffTime <* oChar 'Z')
return (UTCTime d dt)
day :: Parser Day
day = day2 <|> day1
where day1 = fromGregorian <$> decimalN 4
<*> option 1 (char '-' *> intN 2)
<*> option 1 (char '-' *> intN 2)
day2 = fromGregorian <$> decimalN 4 <*> intN 2 <*> intN 2
dayWeek :: Parser Day
dayWeek = maybe (fail "Invalid week day") return =<< go
where
go = fromWeekDateValid
<$> decimalN 4
<*> ("-W" *> intN 2)
<*> option 1 (char '-' *> intN 1)
diffTime :: Parser DiffTime
diffTime = do
h <- fromIntegral <$> intN 2
m <- fromIntegral <$> option 0 (oChar ':' *> intN 2)
s <- maybe 0 realToFrac <$> optional (oChar ':' *> scientific)
return (s + m*60 + h*3600)
intN :: Int -> Parser Int
intN = decimalN
decimalN :: Integral a => Int -> Parser a
decimalN n =
maybe (fail "not an int") (return . fst) =<< fmap readDecimal (AP.take n)
oChar :: Char -> Parser (Maybe Char)
oChar = optional . char
formatInterval :: Interval -> ByteString
formatInterval = toStrict . toLazyByteString . formatIntervalB
formatIntervalB :: Interval -> Builder
formatIntervalB (RecurringInterval i r) = "R" <> maybe "" bShow r <> "/"
<> formatIntervalSpec i
formatIntervalB (Interval i) = formatIntervalSpec i
formatIntervalSpec :: IntervalSpec -> Builder
formatIntervalSpec (StartEnd s1 s2) = formatIsoTime s1 <> "/" <> formatIsoTime s2
formatIntervalSpec (StartDuration s1 s2) = formatIsoTime s1 <> "/" <> formatDurationB s2
formatIntervalSpec (DurationEnd s1 s2) = formatDurationB s1 <> "/" <> formatIsoTime s2
formatIntervalSpec (JustDuration s) = formatDurationB s
formatIsoTime :: UTCTime -> Builder
formatIsoTime = fromString . formatTime defaultTimeLocale "%FT%T%QZ"
bShow :: Show a => a -> Builder
bShow = fromString . show