nanotime-0.1.0: src/Nanotime.hs
module Nanotime
( TimeDelta (..)
, timeDeltaFromFracSecs
, timeDeltaFromNanos
, timeDeltaToFracSecs
, timeDeltaToNanos
, diffTimeDelta
, threadDelayDelta
, TimeLike (..)
, awaitDelta
, PosixTime (..)
, MonoTime (..)
, monoTimeToFracSecs
, monoTimeToNanos
, monoTimeFromFracSecs
, monoTimeFromNanos
, NtpTime (..)
, posixToNtp
, ntpToPosix
, assertingNonNegative
)
where
import Control.Concurrent (threadDelay)
import Data.Bits (Bits (..))
import Data.Fixed (Fixed (..), Pico)
import Data.Semigroup (Sum (..))
import Data.Time.Clock (nominalDiffTimeToSeconds)
import Data.Time.Clock.POSIX (getPOSIXTime)
import Data.Word (Word32, Word64)
import GHC.Clock (getMonotonicTimeNSec)
import GHC.Generics (Generic)
import GHC.Stack (HasCallStack)
assertingNonNegative :: (HasCallStack, Ord a, Num a, Show a) => a -> a
assertingNonNegative a =
if a < 0
then error ("Required non-negative value but got " ++ show a)
else a
-- | Non-negative time difference in nanoseconds since last event
-- Like a 'Nano' (`Fixed E9`) but a machine word.
newtype TimeDelta = TimeDelta {unTimeDelta :: Word64}
deriving stock (Eq, Show, Ord, Generic, Bounded)
deriving newtype (Num)
deriving (Semigroup, Monoid) via (Sum Word64)
-- | Return a 'TimeDelta' corresponding the the given number of fractional seconds.
-- (For example, 1.5 represents one and a half seconds.)
timeDeltaFromFracSecs :: (Real a, Show a) => a -> TimeDelta
timeDeltaFromFracSecs d = TimeDelta (round (1000000000 * toRational (assertingNonNegative d)))
-- | Return a 'TimeDelta' corresponding the the given number of nanoseconds.
-- (For example, 1000000000 represends one second.)
timeDeltaFromNanos :: (Integral a, Show a) => a -> TimeDelta
timeDeltaFromNanos = TimeDelta . fromIntegral . assertingNonNegative
timeDeltaToFracSecs :: (Fractional a) => TimeDelta -> a
timeDeltaToFracSecs (TimeDelta n) = fromIntegral n / 1000000000
timeDeltaToNanos :: TimeDelta -> Word64
timeDeltaToNanos = unTimeDelta
-- | Return the difference of two time deltas
diffTimeDelta
:: TimeDelta
-- ^ the "larger" delta
-> TimeDelta
-- ^ the "smaller" delta
-> Maybe TimeDelta
-- ^ difference between the two (Nothing if negative)
diffTimeDelta (TimeDelta big) (TimeDelta small) =
if big <= small
then Nothing
else Just (TimeDelta (big - small))
threadDelayDelta :: TimeDelta -> IO ()
threadDelayDelta (TimeDelta td) = threadDelay (fromIntegral (div td 1000))
class (Ord t) => TimeLike t where
diffTime :: t -> t -> Maybe TimeDelta
addTime :: t -> TimeDelta -> t
currentTime :: IO t
awaitDelta :: (TimeLike t) => t -> TimeDelta -> IO t
awaitDelta m t = do
let target = addTime m t
cur <- currentTime
case diffTime target cur of
Nothing -> pure cur
Just td -> target <$ threadDelayDelta td
newtype PosixTime = PosixTime {unPosixTime :: Word64}
deriving stock (Eq, Show, Ord, Generic, Bounded)
e9W :: Word64
e9W = 1000000000
picoToNanoWord :: Pico -> Word64
picoToNanoWord (MkFixed i) = fromInteger (div i 1000)
picoFromNanoWord :: Word64 -> Pico
picoFromNanoWord j = MkFixed (1000 * toInteger j)
instance TimeLike PosixTime where
diffTime (PosixTime t2) (PosixTime t1) =
if t2 <= t1 then Nothing else Just (TimeDelta (t2 - t1))
addTime (PosixTime t) (TimeDelta d) = PosixTime (t + d)
currentTime = fmap (PosixTime . picoToNanoWord . nominalDiffTimeToSeconds) getPOSIXTime
-- | Monotonic time in nanoseconds since some unspecified epoch (see 'getMonotonicTimeNs')
newtype MonoTime = MonoTime {unMonoTime :: Word64}
deriving stock (Eq, Show, Ord, Generic, Bounded)
monoTimeFromFracSecs :: (Real a, Show a) => a -> MonoTime
monoTimeFromFracSecs d = MonoTime (round (1000000000 * toRational (assertingNonNegative d)))
monoTimeFromNanos :: (Integral a, Show a) => a -> MonoTime
monoTimeFromNanos = MonoTime . fromIntegral . assertingNonNegative
monoTimeToFracSecs :: (Fractional a) => MonoTime -> a
monoTimeToFracSecs (MonoTime n) = fromIntegral n / 1000000000
monoTimeToNanos :: MonoTime -> Word64
monoTimeToNanos = unMonoTime
instance TimeLike MonoTime where
diffTime (MonoTime t2) (MonoTime t1) =
if t2 <= t1 then Nothing else Just (TimeDelta (t2 - t1))
addTime (MonoTime t) (TimeDelta d) = MonoTime (t + d)
currentTime = fmap MonoTime getMonotonicTimeNSec
newtype NtpTime = NtpTime {unNtpTime :: Word64}
deriving stock (Eq, Show, Ord, Generic, Bounded)
nanoWordToSplit :: Word64 -> (Word32, Word32)
nanoWordToSplit j =
let whole = div j e9W
part = j - e9W * whole
in (fromIntegral whole, fromIntegral part)
nanoWordFromSplit :: Word32 -> Word32 -> Word64
nanoWordFromSplit whole part = e9W * fromIntegral whole + fromIntegral part
ntpFromSplit :: Word32 -> Word32 -> NtpTime
ntpFromSplit whole part = NtpTime (shiftL (fromIntegral whole) 32 .|. fromIntegral part)
ntpToSplit :: NtpTime -> (Word32, Word32)
ntpToSplit (NtpTime k) = (fromIntegral (shiftR k 32), fromIntegral k)
-- Difference in nano seconds between 1/1/1900 and 1/1/1970
-- 1900 is the NTP epoch, 1970 is the unix epoch
ntpEpochDiffSeconds :: Word32
ntpEpochDiffSeconds = 2208988800
posixToNtp :: PosixTime -> NtpTime
posixToNtp (PosixTime j) =
let (whole, part) = nanoWordToSplit j
whole' = whole + ntpEpochDiffSeconds
in ntpFromSplit whole' part
ntpToPosix :: NtpTime -> PosixTime
ntpToPosix k =
let (whole, part) = ntpToSplit k
whole' = whole - ntpEpochDiffSeconds
in PosixTime (nanoWordFromSplit whole' part)
-- Probably best to do time arithmetic directly on PosixTime
instance TimeLike NtpTime where
diffTime n2 n1 = diffTime (ntpToPosix n2) (ntpToPosix n1)
addTime n d = posixToNtp (addTime (ntpToPosix n) d)
currentTime = fmap posixToNtp currentTime