packages feed

moonlight-core-0.1.0.0: src-numeric/Moonlight/Core/Canon.hs

-- | Checked constructors for canonical floating point ('mkFiniteDouble' and friends) and the hash-stable quantization they share.
module Moonlight.Core.Canon
  ( canonicalize
  , isCanonical
  , quantizeForHash
  , mkFiniteDouble
  , mkFiniteWith
  , mkPositiveInt
  , mkPositiveIntWith
  , mkPositiveFiniteDouble
  , mkPositiveFiniteWith
  , mkNonNegativeFiniteDouble
  , mkNonNegativeFiniteWith
  ) where

import Data.Int (Int64)
import Data.Word (Word32)
import Moonlight.Core.Error (MoonlightError (..), MoonlightErrorContext (..), NonFiniteInput (..))
import Moonlight.Internal.FloatMath (isNegativeZero, normalizeNegativeZero)
import Prelude
  ( Bool(..), Bounded(..), Double, Either(..), Int, Num(..), Ord(..)
  , Integer, String, fromIntegral, id, isInfinite, isNaN, not, otherwise
  , round, toInteger, (&&), (^), (>>=)
  )

canonicalize :: Double -> Either MoonlightError Double
canonicalize x
  | isNaN x = Left (NonFiniteValue CanonicalizeContext NaNInput)
  | isInfinite x = Left (NonFiniteValue CanonicalizeContext InfiniteInput)
  | otherwise = Right (normalizeNegativeZero x)

isCanonical :: Double -> Bool
isCanonical x = not (isNaN x) && not (isInfinite x) && not (isNegativeZero x)

quantizeForHash :: Word32 -> Double -> Either MoonlightError Int64
quantizeForHash precision value
  | precision > 9     = Left (QuantizePrecisionTooLarge precision)
  | isNaN value       = Left (NonFiniteValue QuantizeContext NaNInput)
  | isInfinite value  = Left (NonFiniteValue QuantizeContext InfiniteInput)
  | otherwise =
      let scale = (10 :: Double) ^ (fromIntegral precision :: Int)
          scaled = value * scale
      in if scaled >= fromIntegral (maxBound :: Int64)
           then Right maxBound
           else
             if scaled <= fromIntegral (minBound :: Int64)
               then Right minBound
               else Right (saturate (round scaled :: Integer))
  where
    saturate :: Integer -> Int64
    saturate q
      | q > toInteger (maxBound :: Int64) = maxBound
      | q < toInteger (minBound :: Int64) = minBound
      | otherwise = fromIntegral q

mkFiniteDouble :: String -> Double -> Either MoonlightError Double
mkFiniteDouble domainLabel value
  | isNaN value = Left (NonFiniteValue (DomainContext domainLabel) NaNInput)
  | isInfinite value = Left (NonFiniteValue (DomainContext domainLabel) InfiniteInput)
  | otherwise = Right (normalizeNegativeZero value)

mkFiniteWith :: (Double -> errorValue) -> (Double -> value) -> Double -> Either errorValue value
mkFiniteWith errorValue wrapValue value =
  case canonicalize value of
    Left _ -> Left (errorValue value)
    Right canonicalValue -> Right (wrapValue canonicalValue)

mkPositiveInt :: String -> Int -> Either MoonlightError Int
mkPositiveInt domainLabel =
  mkPositiveIntWith
    (\_ -> NonPositiveValue (DomainContext domainLabel))
    id

mkPositiveIntWith :: (Int -> errorValue) -> (Int -> value) -> Int -> Either errorValue value
mkPositiveIntWith errorValue wrapValue value
  | value <= 0 = Left (errorValue value)
  | otherwise = Right (wrapValue value)

mkPositiveFiniteDouble :: String -> Double -> Either MoonlightError Double
mkPositiveFiniteDouble domainLabel value =
  mkFiniteDouble domainLabel value >>= \finiteValue ->
    if finiteValue <= 0
      then Left (NonPositiveValue (DomainContext domainLabel))
      else Right finiteValue

mkPositiveFiniteWith :: (Double -> errorValue) -> (Double -> value) -> Double -> Either errorValue value
mkPositiveFiniteWith errorValue wrapValue value =
  case mkFiniteWith errorValue id value of
    Left validationError -> Left validationError
    Right finiteValue ->
      if finiteValue <= 0
        then Left (errorValue value)
        else Right (wrapValue finiteValue)

mkNonNegativeFiniteDouble :: String -> Double -> Either MoonlightError Double
mkNonNegativeFiniteDouble domainLabel value =
  mkFiniteDouble domainLabel value >>= \finiteValue ->
    if finiteValue < 0
      then Left (NegativeValue (DomainContext domainLabel))
      else Right finiteValue

mkNonNegativeFiniteWith :: (Double -> errorValue) -> (Double -> value) -> Double -> Either errorValue value
mkNonNegativeFiniteWith errorValue wrapValue value =
  case mkFiniteWith errorValue id value of
    Left validationError -> Left validationError
    Right finiteValue ->
      if finiteValue < 0
        then Left (errorValue value)
        else Right (wrapValue finiteValue)