packages feed

packstream-bolt-0.1.0.0: src/Data/PackStream/Timestamp.hs

{-# LANGUAGE PatternSynonyms     #-}

-- | Internal module. Not part of the public API.
module Data.PackStream.Timestamp
    ( PSTimestamp

    , mptsFromPosixSeconds
    , mptsFromPosixSeconds2
    , mptsToPosixSeconds2

    , mptsFromPosixNanoseconds
    , mptsToPosixNanoseconds

    , mptsToUTCTime
    , mptsFromUTCTime
    , mptsFromUTCTimeLossy
    ) where

import           Compat.Prelude

import           Data.Fixed
import           Data.Kind               (Type)
import qualified Data.Time.Clock         as Time
import qualified Data.Time.Clock.POSIX   as Time

-- | A PackStream timestamp
--
-- The representable range is @[-292277022657-01-27 08:29:52 UTC .. 292277026596-12-04 15:30:07.999999999 UTC]@ with nanosecond precision.
type PSTimestamp :: Type
data PSTimestamp = PSTimestamp !Int64 !Word32
                 deriving stock (Eq, Ord, Show, Read)

instance Bounded PSTimestamp where
  minBound = PSTimestamp minBound 0
  maxBound = PSTimestamp maxBound 999999999

instance NFData PSTimestamp where rnf (PSTimestamp _ _) = ()

-- | Construct 'PSTimestamp' from amount of integral seconds since Unix epoch
mptsFromPosixSeconds :: Int64 -> PSTimestamp
mptsFromPosixSeconds s = PSTimestamp s 0

-- | Construct 'PSTimestamp' from amount of seconds and nanoseconds (must be \( \leq 10^9 \) ) passed since Unix epoch
mptsFromPosixSeconds2 :: Int64 -> Word32 -> Maybe PSTimestamp
mptsFromPosixSeconds2 s ns
  | ns <= 999999999 = Just $! PSTimestamp s ns
  | otherwise       = Nothing

-- | Deconstruct 'PSTimestamp' into amount of seconds and nanoseconds passed since Unix epoch
mptsToPosixSeconds2 :: PSTimestamp -> (Int64, Word32)
mptsToPosixSeconds2 (PSTimestamp s ns) = (s, ns)

-- | Construct 'PSTimestamp' from total amount of nanoseconds passed since Unix epoch
mptsFromPosixNanoseconds :: Integer -> Maybe PSTimestamp
mptsFromPosixNanoseconds ns0
  | minI <= ns0, ns0 <= maxI  = Just $! PSTimestamp (fromInteger s) (fromInteger ns)
  | otherwise                 = Nothing
  where
    (s,ns) = divMod ns0 1000000000
    maxI = mptsToPosixNanoseconds maxBound
    minI = mptsToPosixNanoseconds minBound

-- | Deconstruct 'PSTimestamp' into total amount of nanoseconds passed since Unix epoch
mptsToPosixNanoseconds :: PSTimestamp -> Integer
mptsToPosixNanoseconds (PSTimestamp s ns) = (toInteger s * 1000000000) + toInteger ns

-- >>> mptsToUTCTime minBound
-- -292277022657-01-27 08:29:52 UTC

-- >>> mptsToUTCTime maxBound
-- 292277026596-12-04 15:30:07.999999999 UTC

-- >>> mptsToUTCTime (PSTimestamp 0 0)
-- 1970-01-01 00:00:00 UTC

-- >>> mptsToUTCTime (PSTimestamp 0xffffffff 0)
-- 2106-02-07 06:28:15 UTC

-- >>> mptsToUTCTime (PSTimestamp 0x3ffffffff 999999999)
-- 2514-05-30 01:53:03.999999999 UTC

-- | Convert 'PSTimestamp' into 'Time.UTCTime'
mptsToUTCTime :: PSTimestamp -> Time.UTCTime
mptsToUTCTime = picoseconds2utc . (*1000) . mptsToPosixNanoseconds

-- >>> mptsFromUTCTime (mptsToUTCTime minBound) == Just minBound
-- True

-- >>> mptsFromUTCTime (mptsToUTCTime maxBound) == Just maxBound
-- True

utc2picoseconds :: Time.UTCTime -> Integer
utc2picoseconds utc = ps
  where -- NB: this exploits the RULE from time:
    -- "realToFrac/NominalDiffTime->Pico"       realToFrac = \(MkNominalDiffTime ps) -> ps
    MkFixed ps = realToFrac (Time.utcTimeToPOSIXSeconds utc) :: Pico

-- NB: exploits the RULE
-- "realToFrac/Pico->NominalDiffTime"       realToFrac = MkNominalDiffTime
picoseconds2utc :: Integer -> Time.UTCTime
picoseconds2utc ps = Time.posixSecondsToUTCTime (realToFrac (MkFixed ps :: Pico))

-- | Convert 'Time.UTCTime' into 'PSTimestamp'
--
-- This conversion can fail (i.e. result in 'Nothing') if either the conversion cannot be performed lossless, either because the range of 'PSTimestamp' was exceeded or because of sub-nanosecond fractions.
--
-- See also 'mptsFromUTCTimeLossy'
mptsFromUTCTime :: Time.UTCTime -> Maybe PSTimestamp
mptsFromUTCTime t
  | rest /= 0 = Nothing
  | otherwise = mptsFromPosixNanoseconds ns0
  where
    (ns0,rest) = divMod (utc2picoseconds t) 1000

-- | Version of 'mptsFromUTCTime' which performs a lossy conversion into 'PSTimestamp'
--
-- * sub-nanosecond precision is silently truncated (in the sense of 'floor') to nanosecond precision
--
-- * time values exceeding the range of 'PSTimestamp' are clamped to 'minBound' and 'maxBound' respectively
--
mptsFromUTCTimeLossy :: Time.UTCTime -> PSTimestamp
mptsFromUTCTimeLossy t
  | Just mpts <- mptsFromPosixNanoseconds ns0 = mpts
  | ns0 < 0 = minBound
  | otherwise = maxBound
  where
    ns0 = div  (utc2picoseconds t) 1000