-- | 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