katip-0.8.6.0: src/Katip/Format/Time.hs
-- | Time and memory efficient time encoding helper functions.
module Katip.Format.Time
( formatAsLogTime,
formatAsIso8601,
)
where
import Control.Monad.ST (ST)
import Data.Int (Int64)
import Data.Text (Text)
import qualified Data.Text.Array as TA
import Data.Text.Internal (Text (..))
import Data.Time (Day, DiffTime, UTCTime (..), toGregorian)
import Data.Word (Word16)
import Unsafe.Coerce (unsafeCoerce)
-- Note: All functions here are optimized to never allocate anything
-- on heap. At least on ghc 8.0.1 no extra strictness annotations are
-- seem to be needed.
--
-- Exported functions are INLINEABLE
-- | Format 'UTCTime' into a short human readable format.
--
-- >>> formatAsLogTime $ UTCTime (fromGregorian 2016 1 23) 5025.123456789012
-- "2016-01-23 01:23:45"
formatAsLogTime :: UTCTime -> Text
formatAsLogTime (UTCTime day time) = toText $
TA.run2 $ do
buf <- TA.new 19 -- length "2016-10-20 12:34:56"
_ <- writeDay buf 0 day
TA.unsafeWrite buf 10 0x20 -- space
_ <- writeTimeOfDay False buf 11 (diffTimeOfDay64 time)
return (buf, 19)
where
toText (arr, len) = Text arr 0 len
{-# INLINEABLE formatAsLogTime #-}
-- | Format 'UTCTime' into a Iso8601 format.
--
-- Note that this function may overcommit up to 12*2 bytes, depending
-- on sub-second precision. If this is an issue, make a copy with a
-- 'Data.Text.copy'.
--
-- >>> formatAsIso8601 $ UTCTime (fromGregorian 2016 1 23) 5025.123456789012
-- "2016-11-23T01:23:45.123456789012Z"
-- >>> formatAsIso8601 $ UTCTime (fromGregorian 2016 1 23) 5025.123
-- "2016-01-23T01:23:45.123Z"
-- >>> formatAsIso8601 $ UTCTime (fromGregorian 2016 1 23) 5025
-- "2016-01-23T01:23:45Z"
--
formatAsIso8601 :: UTCTime -> Text
formatAsIso8601 (UTCTime day time) = toText $
TA.run2 $ do
buf <- TA.new 33 -- length "2016-10-20 12:34:56.123456789012Z"
_ <- writeDay buf 0 day
TA.unsafeWrite buf 10 0x54 -- T
next <- writeTimeOfDay True buf 11 (diffTimeOfDay64 time)
TA.unsafeWrite buf next 0x5A -- Z
return (buf, next + 1)
where
toText (arr, len) = Text arr 0 len
{-# INLINEABLE formatAsIso8601 #-}
-- | Writes the @YYYY-MM-DD@ part of timestamp
writeDay :: TA.MArray s -> Int -> Day -> ST s Int
writeDay buf off day =
do
TA.unsafeWrite buf (off + 0) (digit y1)
TA.unsafeWrite buf (off + 1) (digit y2)
TA.unsafeWrite buf (off + 2) (digit y3)
TA.unsafeWrite buf (off + 3) (digit y4)
TA.unsafeWrite buf (off + 4) 0x2d -- dash
TA.unsafeWrite buf (off + 5) m1
TA.unsafeWrite buf (off + 6) m2
TA.unsafeWrite buf (off + 7) 0x2d -- dash
TA.unsafeWrite buf (off + 8) d1
TA.unsafeWrite buf (off + 9) d2
return (off + 10)
where
(yr, m, d) = toGregorian day
(y1, ya) = fromIntegral (abs yr) `quotRem` 1000
(y2, yb) = ya `quotRem` 100
(y3, y4) = yb `quotRem` 10
T m1 m2 = twoDigits m
T d1 d2 = twoDigits d
{-# INLINE writeDay #-}
-- | Write time of day, optionally with sub seconds
writeTimeOfDay :: Bool -> TA.MArray s -> Int -> TimeOfDay64 -> ST s Int
writeTimeOfDay doSubSeconds buf off (TOD hh mm ss) =
do
TA.unsafeWrite buf off h1
TA.unsafeWrite buf (off + 1) h2
TA.unsafeWrite buf (off + 2) 0x3A -- colon
TA.unsafeWrite buf (off + 3) m1
TA.unsafeWrite buf (off + 4) m2
TA.unsafeWrite buf (off + 5) 0x3A -- colon
TA.unsafeWrite buf (off + 6) s1
TA.unsafeWrite buf (off + 7) s2
if doSubSeconds && frac /= 0
then writeFracSeconds buf (off + 8) frac
else return (off + 8)
where
T h1 h2 = twoDigits hh
T m1 m2 = twoDigits mm
T s1 s2 = twoDigits (fromIntegral real)
(real, frac) = ss `quotRem` pico
pico = 1000000000000 -- number of picoseconds in 1 second
writeFracSeconds :: TA.MArray s -> Int -> Int64 -> ST s Int
writeFracSeconds buf off frac =
do
TA.unsafeWrite buf off 0x2e -- period
if mills == 0
then do
writeTrunc6 buf (off + 1) (fromIntegral mics)
else do
writeDigit6 buf (off + 1) (fromIntegral mics)
writeTrunc6 buf (off + 7) (fromIntegral mills)
where
(mics, mills) = frac `quotRem` micro
micro = 1000000 -- number of microseconds in 1 second
writeDigit6 :: TA.MArray s -> Int -> Int -> ST s ()
writeDigit6 buf off i =
do
writeDigit3 buf off f1
writeDigit3 buf (off + 3) f2
where
(f1, f2) = i `quotRem` 1000
{-# INLINE writeDigit6 #-}
writeDigit3 :: TA.MArray s -> Int -> Int -> ST s ()
writeDigit3 buf off i =
do
TA.unsafeWrite buf off (digit d1)
TA.unsafeWrite buf (off + 1) (digit d2)
TA.unsafeWrite buf (off + 2) (digit d3)
where
(d1, d) = i `quotRem` 100
(d2, d3) = d `quotRem` 10
{-# INLINE writeDigit3 #-}
writeTrunc6 :: TA.MArray s -> Int -> Int -> ST s Int
writeTrunc6 buf off i =
if f2 == 0
then writeTrunc3 buf off f1
else do
writeDigit3 buf off f1
writeTrunc3 buf (off + 3) f2
where
(f1, f2) = i `quotRem` 1000
{-# INLINE writeTrunc6 #-}
writeTrunc3 :: TA.MArray s -> Int -> Int -> ST s Int
writeTrunc3 buf off i
| d == 0 = do
TA.unsafeWrite buf off (digit d1)
return (off + 1)
| d3 == 0 = do
TA.unsafeWrite buf off (digit d1)
TA.unsafeWrite buf (off + 1) (digit d2)
return (off + 2)
| otherwise = do
TA.unsafeWrite buf off (digit d1)
TA.unsafeWrite buf (off + 1) (digit d2)
TA.unsafeWrite buf (off + 2) (digit d3)
return (off + 3)
where
(d1, d) = i `quotRem` 100
(d2, d3) = d `quotRem` 10
{-# INLINE writeTrunc3 #-}
-- Following code was adapted from aeson package.
--
-- Copyright: (c) 2015-2016 Bryan O'Sullivan
-- License: BSD3
data T = T {-# UNPACK #-} !Word16 {-# UNPACK #-} !Word16
twoDigits :: Int -> T
twoDigits a = T (digit hi) (digit lo)
where
(hi, lo) = a `quotRem` 10
digit :: Int -> Word16
digit x = fromIntegral (x + 48)
data TimeOfDay64
= TOD
{-# UNPACK #-} !Int
{-# UNPACK #-} !Int
{-# UNPACK #-} !Int64
diffTimeOfDay64 :: DiffTime -> TimeOfDay64
diffTimeOfDay64 t = TOD (fromIntegral h) (fromIntegral m) s
where
(h, mp) = fromIntegral pico `quotRem` 3600000000000000
(m, s) = mp `quotRem` 60000000000000
pico = unsafeCoerce t :: Integer