packages feed

nanotime-0.3.2: src/Nanotime.hs

module Nanotime
  ( Sign (..)
  , TimeDelta (..)
  , timeDeltaFromFracSecs
  , timeDeltaFromNanos
  , timeDeltaToFracSecs
  , timeDeltaToNanos
  , threadDelayDelta
  , showTimeDelta
  , TimeLike (..)
  , awaitDelta
  , PosixTime (..)
  , showPosixTime
  , MonoTime (..)
  , monoTimeToFracSecs
  , monoTimeToNanos
  , monoTimeFromFracSecs
  , monoTimeFromNanos
  , NtpTime (..)
  , posixToNtp
  , ntpToPosix
  )
where

import Control.Concurrent (threadDelay)
import Data.Bits (Bits (..))
import Data.Fixed (Fixed (..), Pico)
import Data.Ratio ((%))
import Data.Time.Clock (nominalDiffTimeToSeconds)
import Data.Time.Clock.POSIX (getPOSIXTime, posixSecondsToUTCTime)
import Data.Time.Format.ISO8601 (iso8601Show)
import Data.Word (Word32, Word64)
import GHC.Clock (getMonotonicTimeNSec)
import GHC.Stack (HasCallStack)
import Numeric (showFFloat)

-- | Sign (negative or positive) of a magnitude of time difference
data Sign = SignNeg | SignPos
  deriving stock (Eq, Ord, Show, Enum, Bounded)

-- | Signed time difference in nanoseconds since last event
-- Like a 'Nano' (`Fixed E9`) but using a machine word with explicit sign.
data TimeDelta = TimeDelta
  { tdSign :: !Sign
  , tdMag :: !Word64
  }
  deriving stock (Show)

instance Eq TimeDelta where
  TimeDelta s1 m1 == TimeDelta s2 m2 =
    (m1 == 0 && m2 == 0) || (s1 == s2 && m1 == m2)

instance Ord TimeDelta where
  compare (TimeDelta s1 m1) (TimeDelta s2 m2) =
    if m1 == 0 && m2 == 0
      then EQ
      else case s1 of
        SignPos ->
          case s2 of
            SignPos -> compare m1 m2
            SignNeg -> GT
        SignNeg ->
          case s2 of
            SignPos -> LT
            SignNeg -> compare m2 m1

instance Bounded TimeDelta where
  minBound = TimeDelta SignNeg maxBound
  maxBound = TimeDelta SignPos maxBound

instance Semigroup TimeDelta where
  td1@(TimeDelta s1 m1) <> td2@(TimeDelta s2 m2) =
    if
      | m1 == 0 -> td2
      | m2 == 0 -> td1
      | s1 == s2 -> mkTimeDelta s1 (m1 + m2)
      | otherwise ->
          if m1 >= m2
            then mkTimeDelta s1 (m1 - m2)
            else mkTimeDelta s2 (m2 - m1)

instance Monoid TimeDelta where
  mempty = TimeDelta SignPos 0

instance Num TimeDelta where
  (+) = (<>)
  (*) = error "TimeDelta multiplication has no meaning"
  abs (TimeDelta _ m) = TimeDelta SignPos m
  signum (TimeDelta s m) =
    if m == 0
      then 0
      else case s of
        SignPos -> 1
        SignNeg -> -1
  fromInteger i =
    if i >= 0
      then TimeDelta SignPos (fromInteger i)
      else TimeDelta SignNeg (fromInteger (negate i))
  negate td@(TimeDelta s m) =
    if m == 0 && s == SignPos
      then td
      else case s of
        SignPos -> TimeDelta SignNeg m
        SignNeg -> TimeDelta SignPos m

-- private
mkTimeDelta :: Sign -> Word64 -> TimeDelta
mkTimeDelta s m =
  if m == 0
    then TimeDelta SignPos m
    else TimeDelta s m

-- | Return a 'TimeDelta' corresponding the the given number of fractional seconds.
-- (For example, 1.5 represents one and a half seconds.)
timeDeltaFromFracSecs :: (Real a) => a -> TimeDelta
timeDeltaFromFracSecs d =
  if d >= 0
    then TimeDelta SignPos (round (1000000000 * toRational d))
    else TimeDelta SignNeg (round (1000000000 * toRational (negate d)))

-- | Return a 'TimeDelta' corresponding the the given number of nanoseconds.
-- (For example, 1000000000 represends one second.)
timeDeltaFromNanos :: (Integral a) => a -> TimeDelta
timeDeltaFromNanos = fromIntegral

-- private
timeDeltaFromDiff :: Word64 -> Word64 -> TimeDelta
timeDeltaFromDiff end start =
  if end >= start
    then TimeDelta SignPos (end - start)
    else TimeDelta SignNeg (start - end)

-- private
timeDeltaAdd :: Word64 -> TimeDelta -> Word64
timeDeltaAdd t (TimeDelta s m) =
  case s of
    SignPos -> t + m
    SignNeg -> t - m

timeDeltaToFracSecs :: (Fractional a) => TimeDelta -> a
timeDeltaToFracSecs (TimeDelta s m) =
  let a = fromIntegral m / 1000000000
  in  case s of
        SignPos -> a
        SignNeg -> negate a

timeDeltaToNanos :: TimeDelta -> (Sign, Word64)
timeDeltaToNanos (TimeDelta s m) = (s, m)

threadDelayDelta :: TimeDelta -> IO ()
threadDelayDelta (TimeDelta s m) =
  case s of
    SignPos | m > 0 -> threadDelay (fromIntegral (div m 1000))
    _ -> pure ()

-- | Show a 'TimeDelta' as a fractional second with the given number
-- of decimal places for debugging
showTimeDelta :: Int -> TimeDelta -> String
showTimeDelta places td = showFFloat @Double (Just places) (timeDeltaToFracSecs td) ""

-- | 'MonoTime', 'PosixTime', and 'NtpTime' act similarly
class (Ord t) => TimeLike t where
  -- | `diffTime end start` computes `end - start`
  diffTime :: t -> t -> TimeDelta

  -- | `addTime start (diffTime end start) == end`
  addTime :: t -> TimeDelta -> t

  -- | Get the current time in the desired type
  currentTime :: IO t

awaitDelta :: (TimeLike t) => t -> TimeDelta -> IO t
awaitDelta m t = do
  let target = addTime m t
  cur <- currentTime
  let td = diffTime target cur
  target <$ threadDelayDelta td

newtype PosixTime = PosixTime {unPosixTime :: Word64}
  deriving stock (Show)
  deriving newtype (Eq, Ord, Bounded, Num)

-- | Show 'PosixTime' as a UTC ISO-8601 String for debugging
showPosixTime :: PosixTime -> String
showPosixTime (PosixTime ns) =
  iso8601Show (posixSecondsToUTCTime (fromRational (fromIntegral ns % 1000000000)))

-- private
e9W :: Word64
e9W = 1000000000

-- private
picoToNanoWord :: Pico -> Word64
picoToNanoWord (MkFixed i) = fromInteger (div i 1000)

instance TimeLike PosixTime where
  diffTime (PosixTime t2) (PosixTime t1) = timeDeltaFromDiff t2 t1
  addTime (PosixTime t) td = PosixTime (timeDeltaAdd t td)
  currentTime = fmap (PosixTime . picoToNanoWord . nominalDiffTimeToSeconds) getPOSIXTime

-- | Monotonic time in nanoseconds since some unspecified epoch (see 'getMonotonicTimeNs')
newtype MonoTime = MonoTime {unMonoTime :: Word64}
  deriving stock (Show)
  deriving newtype (Eq, Ord, Bounded, Num)

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) = timeDeltaFromDiff t2 t1
  addTime (MonoTime t) td = MonoTime (timeDeltaAdd t td)
  currentTime = fmap MonoTime getMonotonicTimeNSec

newtype NtpTime = NtpTime {unNtpTime :: Word64}
  deriving stock (Show)
  deriving newtype (Eq, Ord, Bounded, Num)

-- private
nanoWordToSplit :: Word64 -> (Word32, Word32)
nanoWordToSplit j =
  let whole = div j e9W
      part = j - e9W * whole
  in  (fromIntegral whole, fromIntegral part)

-- private
nanoWordFromSplit :: Word32 -> Word32 -> Word64
nanoWordFromSplit whole part = e9W * fromIntegral whole + fromIntegral part

-- private
ntpFromSplit :: Word32 -> Word32 -> NtpTime
ntpFromSplit whole part = NtpTime (shiftL (fromIntegral whole) 32 .|. fromIntegral part)

-- private
ntpToSplit :: NtpTime -> (Word32, Word32)
ntpToSplit (NtpTime k) = (fromIntegral (shiftR k 32), fromIntegral k)

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

-- | Convert 'NtpTime' to 'PosixTime'
posixToNtp :: PosixTime -> NtpTime
posixToNtp (PosixTime j) =
  let (whole, part) = nanoWordToSplit j
      whole' = whole + ntpEpochDiffSeconds
  in  ntpFromSplit whole' part

-- | Convert 'NtpTime' to 'PosixTime'
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

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