rel8-1.7.0.0: src/Rel8/Type/Parser/Time.hs
{-# language OverloadedStrings #-}
{-# language TypeApplications #-}
module Rel8.Type.Parser.Time
( calendarDiffTime
, day
, localTime
, timeOfDay
, utcTime
)
where
-- attoparsec
import qualified Data.Attoparsec.ByteString.Char8 as A
-- base
import Control.Applicative ((<|>), optional)
import Data.Bits ((.&.))
import Data.Bool (bool)
import Data.Fixed (Fixed (MkFixed), Pico, divMod')
import Data.Functor (void)
import Data.Int (Int64)
import Prelude
-- bytestring
import qualified Data.ByteString as BS
-- time
import Data.Time.Calendar (Day, addDays, fromGregorianValid)
import Data.Time.Clock (DiffTime, UTCTime (UTCTime))
import Data.Time.Format.ISO8601 (iso8601ParseM)
import Data.Time.LocalTime
( CalendarDiffTime (CalendarDiffTime)
, LocalTime (LocalTime)
, TimeOfDay (TimeOfDay)
, sinceMidnight
)
-- utf8
import qualified Data.ByteString.UTF8 as UTF8
day :: A.Parser Day
day = do
y <- A.decimal <* A.char '-'
m <- twoDigits <* A.char '-'
d <- twoDigits
maybe (fail "Day: invalid date") pure $ fromGregorianValid y m d
timeOfDay :: A.Parser TimeOfDay
timeOfDay = do
h <- twoDigits
m <- A.char ':' *> twoDigits
s <- A.char ':' *> secondsParser
if h < 24 && m < 60 && s <= 60
then pure $ TimeOfDay h m s
else fail "TimeOfDay: invalid time"
localTime :: A.Parser LocalTime
localTime = LocalTime <$> day <* separator <*> timeOfDay
where
separator = A.char ' ' <|> A.char 'T'
utcTime :: A.Parser UTCTime
utcTime = do
LocalTime date time <- localTime
tz <- timeZone
let
(days, time') = (sinceMidnight time + tz) `divMod'` oneDay
where
oneDay = 24 * 60 * 60
date' = addDays days date
pure $ UTCTime date' time'
calendarDiffTime :: A.Parser CalendarDiffTime
calendarDiffTime = iso8601 <|> postgres
where
iso8601 = A.takeByteString >>= iso8601ParseM . UTF8.toString
at = optional (A.char '@') *> A.skipSpace
plural unit = A.skipSpace <* (unit <* optional "s") <* A.skipSpace
parseMonths = sql <|> postgresql
where
sql = A.signed $ do
years <- A.decimal <* A.char '-'
months <- A.decimal <* A.skipSpace
pure $ years * 12 + months
postgresql = do
at
years <- A.signed A.decimal <* plural "year" <|> pure 0
months <- A.signed A.decimal <* plural "mon" <|> pure 0
pure $ years * 12 + months
parseTime = (+) <$> parseDays <*> time
where
time = realToFrac <$> (sql <|> postgresql)
where
sql = A.signed $ do
h <- A.signed A.decimal <* A.char ':'
m <- twoDigits <* A.char ':'
s <- secondsParser
pure $ fromIntegral (((h * 60) + m) * 60) + s
postgresql = do
h <- A.signed A.decimal <* plural "hour" <|> pure 0
m <- A.signed A.decimal <* plural "min" <|> pure 0
s <- secondsParser <* plural "sec" <|> pure 0
pure $ fromIntegral @Int (((h * 60) + m) * 60) + s
parseDays = do
days <- A.signed A.decimal <* (plural "days" <|> skipSpace1) <|> pure 0
pure $ fromIntegral @Int days * 24 * 60 * 60
postgres = do
months <- parseMonths
time <- parseTime
ago <- (True <$ (A.skipSpace *> "ago")) <|> pure False
pure $ CalendarDiffTime (bool id negate ago months) (bool id negate ago time)
secondsParser :: A.Parser Pico
secondsParser = do
integral <- twoDigits
mfractional <- optional (A.char '.' *> A.takeWhile1 A.isDigit)
pure $ case mfractional of
Nothing -> fromIntegral integral
Just fractional -> parseFraction (fromIntegral integral) fractional
where
parseFraction integral digits = MkFixed (fromIntegral (n * 10 ^ e))
where
e = max 0 (12 - BS.length digits)
n = BS.foldl' go (integral :: Int64) (BS.take 12 digits)
where
go acc digit = 10 * acc + fromIntegral (fromEnum digit .&. 0xf)
twoDigits :: A.Parser Int
twoDigits = do
u <- A.digit
l <- A.digit
pure $ fromEnum u .&. 0xf * 10 + fromEnum l .&. 0xf
timeZone :: A.Parser DiffTime
timeZone = 0 <$ A.char 'Z' <|> diffTime
diffTime :: A.Parser DiffTime
diffTime = A.signed $ do
h <- twoDigits
m <- A.char ':' *> twoDigits <|> pure 0
s <- A.char ':' *> secondsParser <|> pure 0
pure $ sinceMidnight $ TimeOfDay h m s
skipSpace1 :: A.Parser ()
skipSpace1 = void $ A.takeWhile1 A.isSpace