packages feed

wireform-proto-0.2.0.0: src/Proto/Google/Protobuf/Duration/Util.hs

{-# LANGUAGE ScopedTypeVariables #-}

{- | Utility functions for @google.protobuf.Duration@.

Provides conversions to\/from 'NominalDiffTime', construction from
milliseconds\/microseconds\/nanoseconds, arithmetic, an 'Ord' instance,
and validation — mirroring utilities found in Go (@durationpb@),
Java (@com.google.protobuf.util.Durations@), and Rust (@prost-types@).
-}
module Proto.Google.Protobuf.Duration.Util (
  -- * Conversions
  durationFromNominalDiffTime,
  durationToNominalDiffTime,

  -- * Construction
  durationFromSeconds,
  durationFromMillis,
  durationFromMicros,
  durationFromNanos,

  -- * Extraction
  durationToSeconds,
  durationToMillis,
  durationToMicros,
  durationToNanos,

  -- * Arithmetic
  addDurations,
  negateDuration,
  absDuration,

  -- * Validation
  isValidDuration,
  normalizeDuration,

  -- * Comparison
  compareDuration,
) where

import Data.Int (Int32, Int64)
import Data.Time.Clock (NominalDiffTime)
import Proto.Google.Protobuf.Duration (Duration (..), defaultDuration)


nanosPerSecond :: Int64
nanosPerSecond = 1000000000


{- | Convert a 'NominalDiffTime' to a 'Duration'.

Fractional seconds are preserved at nanosecond granularity.
-}
durationFromNominalDiffTime :: NominalDiffTime -> Duration
durationFromNominalDiffTime ndt =
  let totalNanos = round (ndt * fromIntegral nanosPerSecond) :: Int64
      (s, n) = totalNanos `quotRem` nanosPerSecond
  in defaultDuration
       { durationSeconds = s
       , durationNanos = fromIntegral n
       }


-- | Convert a 'Duration' to a 'NominalDiffTime'.
durationToNominalDiffTime :: Duration -> NominalDiffTime
durationToNominalDiffTime dur =
  let s = fromIntegral (durationSeconds dur) :: NominalDiffTime
      n = fromIntegral (durationNanos dur) / fromIntegral nanosPerSecond :: NominalDiffTime
  in s + n


-- | Construct a 'Duration' from whole seconds.
durationFromSeconds :: Int64 -> Duration
durationFromSeconds s = defaultDuration {durationSeconds = s}


-- | Construct a 'Duration' from milliseconds.
durationFromMillis :: Int64 -> Duration
durationFromMillis ms =
  let (s, rem') = ms `quotRem` 1000
  in defaultDuration
       { durationSeconds = s
       , durationNanos = fromIntegral (rem' * 1000000)
       }


-- | Construct a 'Duration' from microseconds.
durationFromMicros :: Int64 -> Duration
durationFromMicros us =
  let (s, rem') = us `quotRem` 1000000
  in defaultDuration
       { durationSeconds = s
       , durationNanos = fromIntegral (rem' * 1000)
       }


-- | Construct a 'Duration' from nanoseconds.
durationFromNanos :: Int64 -> Duration
durationFromNanos ns =
  let (s, n) = ns `quotRem` nanosPerSecond
  in defaultDuration
       { durationSeconds = s
       , durationNanos = fromIntegral n
       }


-- | Extract total seconds (truncating nanos).
durationToSeconds :: Duration -> Int64
durationToSeconds = durationSeconds


-- | Convert to total milliseconds (truncating sub-millisecond part).
durationToMillis :: Duration -> Int64
durationToMillis dur =
  durationSeconds dur * 1000 + fromIntegral (durationNanos dur) `quot` 1000000


-- | Convert to total microseconds (truncating sub-microsecond part).
durationToMicros :: Duration -> Int64
durationToMicros dur =
  durationSeconds dur * 1000000 + fromIntegral (durationNanos dur) `quot` 1000


-- | Convert to total nanoseconds.
durationToNanos :: Duration -> Int64
durationToNanos dur =
  durationSeconds dur * nanosPerSecond + fromIntegral (durationNanos dur)


-- | Add two 'Duration' values.
addDurations :: Duration -> Duration -> Duration
addDurations a b =
  let totalNanos = durationToNanos a + durationToNanos b
      (s, n) = totalNanos `quotRem` nanosPerSecond
  in defaultDuration
       { durationSeconds = s
       , durationNanos = fromIntegral n
       }


-- | Negate a 'Duration'.
negateDuration :: Duration -> Duration
negateDuration dur =
  defaultDuration
    { durationSeconds = negate (durationSeconds dur)
    , durationNanos = negate (durationNanos dur)
    }


-- | Absolute value of a 'Duration'.
absDuration :: Duration -> Duration
absDuration dur
  | durationSeconds dur < 0 || (durationSeconds dur == 0 && durationNanos dur < 0) =
      negateDuration dur
  | otherwise = dur


{- | Normalize a Duration so that seconds and nanos have the same sign
and nanos is in @(-999999999, 999999999)@.

The proto spec requires that for valid Durations, nanos have the same
sign as seconds (or be zero) and @|nanos| < 10^9@.
-}
normalizeDuration :: Duration -> Duration
normalizeDuration dur =
  let totalNanos = durationToNanos dur
      (s, n) = totalNanos `quotRem` nanosPerSecond
  in defaultDuration
       { durationSeconds = s
       , durationNanos = fromIntegral n
       }


{- | A 'Duration' is valid when:

* seconds is in @[-315576000000, 315576000000]@ (roughly +/- 10000 years)
* nanos is in @[-999999999, 999999999]@
* seconds and nanos have the same sign (or either is zero)
-}
isValidDuration :: Duration -> Bool
isValidDuration dur =
  abs (durationSeconds dur) <= 315576000000
    && abs (fromIntegral (durationNanos dur) :: Int64) <= 999999999
    && signsAgree (durationSeconds dur) (fromIntegral (durationNanos dur))
  where
    signsAgree s n
      | s == 0 || n == 0 = True
      | otherwise = (s > 0) == (n > 0)


-- | Compare two 'Duration' values by total nanoseconds.
compareDuration :: Duration -> Duration -> Ordering
compareDuration a b = compare (durationToNanos a) (durationToNanos b)


instance Ord Duration where
  compare = compareDuration