Z-Data-1.0.0.0: Z/Data/Builder/Time.hs
{-|
Module: Z.Data.Parser.Time
Description : Builders for types from time.
Copyright: (c) 2015-2016 Bryan O'Sullivan
(c) 2020 Dong Han
License: BSD3
Maintainer: Dong <winterland1989@gmail.com>
Stability: experimental
Portability: portable
Builders for dates and times.
-}
module Z.Data.Builder.Time
( day
, timeOfDay
, timeZone
, utcTime
, localTime
, zonedTime
-- * internal
, twoDigits
, toGregorian'
, toGregorianInt64
) where
import Control.Monad
import Data.Time
import Data.Word
import Data.Fixed
import Data.Int
import Z.Data.Builder.Base (Builder)
import qualified Z.Data.Builder.Base as B
import qualified Z.Data.Builder.Numeric as B
import Z.Data.Builder.Numeric (i2wDec)
import Z.Data.ASCII
-- | @YYYY-mm-dd@.
--
day :: Day -> Builder ()
{-# INLINE day #-}
day dd = encodeYear yr <> B.encodePrim (HYPHEN, mh, ml, HYPHEN, dh, dl)
where
(yr, m, d) = toGregorian' dd
(mh, ml) = twoDigits m
(dh, dl) = twoDigits d
encodeYear y
| y >= 1000 = B.integer y
| y >= 0 = B.encodePrim (padYear y)
| y >= -999 = B.encodePrim (MINUS, padYear y)
| otherwise = B.integer y
padYear y =
let (ab,c) = (fromIntegral y :: Int) `quotRem` 10
(a, b) = ab `quotRem` 10
in (DIGIT_0, i2wDec a, i2wDec b, i2wDec c)
-- | Faster 'toGregorian' with 'toGregorianInt64' as the common case path.
toGregorian' :: Day -> (Integer, Int, Int)
{-# INLINE toGregorian' #-}
toGregorian' dd@(ModifiedJulianDay mjd)
| -9223372036854775808 <= mjd && mjd <= 9223372036854097232 = toGregorianInt64 (fromIntegral mjd)
| otherwise = toGregorian dd
-- | Faster common case for small days (-9223372036854775808 ~ 9223372036854097232).
--
toGregorianInt64 :: Int64 -> (Integer, Int, Int)
{-# INLINABLE toGregorianInt64 #-}
toGregorianInt64 mjd = year' `seq` month `seq` day_ `seq` (year', month, day_)
where
a = mjd + 678575
quadcent = div a 146097
b = mod a 146097
cent = min (div b 36524) 3
c = b - (cent * 36524)
quad = div c 1461
d = mod c 1461
y = min (div d 365) 3
yd = fromIntegral (d - (y * 365) + 1)
year = quadcent * 400 + cent * 100 + quad * 4 + y + 1
year' = fromIntegral year
isLeap = (rem year 4 == 0) && ((rem year 400 == 0) || not (rem year 100 == 0))
(month, day_) = findMonthDay (if isLeap then monthListLeap else monthList) yd 1
findMonthDay :: [Int] -> Int -> Int -> (Int, Int)
findMonthDay (n : ns) !yd_ !m | yd_ > n = findMonthDay ns (yd_ - n) (m + 1)
findMonthDay _ !yd_ !m = (m, yd_)
monthList :: [Int]
{-# NOINLINE monthList #-}
monthList = [ 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 ]
monthListLeap :: [Int]
{-# NOINLINE monthListLeap #-}
monthListLeap = [ 31 , 29 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 ]
-- | @HH-MM-SS@.
timeOfDay :: TimeOfDay -> Builder ()
{-# INLINE timeOfDay #-}
timeOfDay t = timeOfDay64 (toTimeOfDay64 t)
-- | Timezone format in @+HH:MM@, with single letter @Z@ for @+00:00@.
timeZone :: TimeZone -> Builder ()
{-# INLINE timeZone #-}
timeZone (TimeZone off _ _)
| off == 0 = B.word8 LETTER_Z
| otherwise = B.encodePrim (s, hh, hl, COLON, mh, ml)
where !s = if off < 0 then MINUS else PLUS
(hh, hl) = twoDigits h
(mh, ml) = twoDigits m
(h,m) = abs off `quotRem` 60
-- | Write 'UTCTime' in ISO8061 @YYYY-MM-DDTHH:MM:SS.SSSZ@(time zone will always be @Z@).
utcTime :: UTCTime -> Builder ()
{-# INLINE utcTime #-}
utcTime (UTCTime d s) = dayTime d (diffTimeOfDay64 s) >> B.word8 LETTER_Z
-- | Write 'LocalTime' in ISO8061 @YYYY-MM-DDTHH:MM:SS.SSS@.
localTime :: LocalTime -> Builder ()
{-# INLINE localTime #-}
localTime (LocalTime d t) = dayTime d (toTimeOfDay64 t)
-- | Write 'ZonedTime' in ISO8061 @YYYY-MM-DD HH:MM:SS.SSSZ@.
zonedTime :: ZonedTime -> Builder ()
{-# INLINE zonedTime #-}
zonedTime (ZonedTime t z) = localTime t >> timeZone z
--------------------------------------------------------------------------------
-- | Like TimeOfDay, but using a fixed-width integer for seconds.
type TimeOfDay64 = (Int, Int, Int64)
diffTimeOfDay64 :: DiffTime -> TimeOfDay64
{-# INLINE diffTimeOfDay64 #-}
diffTimeOfDay64 t
| t >= 86400 = (23, 59, 60000000000000 + pico (t - 86400))
| otherwise = (fromIntegral h, fromIntegral m, s)
where (h,mp) = pico t `quotRem` 3600000000000000
(m,s) = mp `quotRem` 60000000000000
pico = fromIntegral . diffTimeToPicoseconds
toTimeOfDay64 :: TimeOfDay -> TimeOfDay64
{-# INLINE toTimeOfDay64 #-}
toTimeOfDay64 (TimeOfDay h m (MkFixed s)) = (h, m, fromIntegral s)
dayTime :: Day -> TimeOfDay64 -> Builder ()
{-# INLINE dayTime #-}
dayTime d t = day d >> B.word8 LETTER_T >> timeOfDay64 t
timeOfDay64 :: TimeOfDay64 -> Builder ()
{-# INLINABLE timeOfDay64 #-}
timeOfDay64 (!h, !m, !s) = do
B.encodePrim (hh, hl, COLON, mh, ml, COLON, sh, sl)
when (frac /= 0) $ do
B.encodePrim DOT
replicateM_ (12 - n1) (B.word8 DIGIT_0)
forM_ ds (B.word8 . B.i2wDec)
where
(real, frac) = s `quotRem` 1000000000000 -- number of picoseconds in 1 second
(hh, hl) = twoDigits h
(mh, ml) = twoDigits m
(sh, sl) = twoDigits (fromIntegral real)
(frac', n0) = ctz frac 0
(ds, n1) = toDigits frac' [] n0
ctz :: Int64 -> Int -> (Int64, Int)
ctz !x !n =
let (x', r) = x `quotRem` 10
in if r == 0
then ctz x' (n+1)
else (x, n)
toDigits :: Int64 -> [Int64] -> Int -> ([Int64], Int)
toDigits !x !acc !n =
let (x', r) = x `quotRem` 10
in if x' == 0
then ((r:acc), n+1)
else toDigits x' (r:acc) (n+1)
twoDigits :: Int -> (Word8, Word8)
{-# INLINE twoDigits #-}
twoDigits a = (i2wDec hi, i2wDec lo)
where (hi,lo) = a `quotRem` 10