All Downloads are FREE. Search and download functionalities are using the official Maven repository.

lucuma.core.math.SignalToNoise.scala Maven / Gradle / Ivy

There is a newer version: 0.108.0
Show newest version
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package lucuma.core.math

import cats.Order
import cats.syntax.all.*
import eu.timepit.refined.types.numeric.NonNegBigDecimal
import io.circe.Decoder
import io.circe.Encoder
import io.circe.JsonNumber
import lucuma.core.optics.Format
import lucuma.core.validation.ValidSplitEpiNec
import monocle.Prism

import scala.util.control.Exception.catching

// Signal-to-noise stored as milli-signal to noise Long capped at 9,999,999,999
// so that it fits in a database numeric(10, 3).

opaque type SignalToNoise = Long

object SignalToNoise {

  /**
   * Maximum supported signal-to-noise value: 9,999,999.999.
   */
  val Max: SignalToNoise =
    9_999_999_999L

  /**
   * Minimum supported signal-to-noise value: 0.000.
   */
  val Min: SignalToNoise =
    0L

  extension (s2n: SignalToNoise) {

    /**
     * Converts this SignalToNoise value to a `BigDecimal`, which is guaranteed
     * to be positive.
     */
    def toBigDecimal: BigDecimal =
      BigDecimal(s2n, 3)

    /**
     * Converts this SignalToNoise value to a `NonNegBigDecimal`.
     */
    def toNonNegBigDecimal: NonNegBigDecimal =
      NonNegBigDecimal.unsafeFrom(toBigDecimal)

  }

  private def fromMilliLong(msn: Long): Option[SignalToNoise] =
    Option.when(msn >= Min && msn <= Max)(msn)

  private def fromMilliDecimal(msn: BigDecimal): Option[SignalToNoise] =
    Option.when(msn.isValidLong)(msn.longValue).flatMap(fromMilliLong)

  private def errorMessage(sn: BigDecimal): String =
    s"Signal-to-noise is limited to [${Min.toBigDecimal}, ${Max.toBigDecimal}), got $sn"

  private def error(sn: BigDecimal): Nothing =
    sys.error(errorMessage(sn))

  /**
   * Creates a `SignalToNoise` value assuming that the given BigDecimal is in
   * range [Min, Max] and does not have a finer scale than milli-sn.
   *
   * @group Optics
   */
  val FromBigDecimalExact: Prism[BigDecimal, SignalToNoise] =
    Prism[BigDecimal, SignalToNoise](bd => fromMilliDecimal(bd * 1000))(_.toBigDecimal)

  /**
   * Creates a `SignalToNoise` value assuming from a NonNegBigDecimal that is in
   * range [Min, Max] and does not have a finer scale than milli-sn.
   *
   * @group Optics
   */
  val FromNonNegBigDecimalExact: Prism[NonNegBigDecimal, SignalToNoise] =
    Prism[NonNegBigDecimal, SignalToNoise](bd =>
      SignalToNoise.FromBigDecimalExact.getOption(bd.value)
    )(_.toNonNegBigDecimal)

  /**
   * Creates a `SignalToNoise` value assuming that the given BigDecimal is in
   * range [Min, Max].  Rounds finer scale values to milli-sn.
   *
   * @group Optics
   */
  val FromBigDecimalRounding: ValidSplitEpiNec[String, BigDecimal, SignalToNoise] =
    ValidSplitEpiNec(
      bd =>
        fromMilliDecimal((bd * 1000)
          .setScale(0, BigDecimal.RoundingMode.HALF_UP))
          .toRightNec("Invalid SignalToNoise value $bd"),
      _.toBigDecimal,
    )

  /**
   * Creates a `SignalToNoise` value assuming that the given NonNegBigDecimal is in
   * range [Min, Max].  Rounds finer scale values to milli-sn.
   *
   * @group Optics
   */
  val FromNonNegBigDecimalRounding: ValidSplitEpiNec[String, NonNegBigDecimal, SignalToNoise] =
    ValidSplitEpiNec(
      bd =>
        fromMilliDecimal((bd.value * 1000)
          .setScale(0, BigDecimal.RoundingMode.HALF_UP))
          .toRightNec("Invalid SignalToNoise value $bd"),
      _.toNonNegBigDecimal)
  /**
   * Formats to the canonical String representation for SignalToNoise.
   *
   * @group Optics
   */
  val FromString: Format[String, SignalToNoise] = {
    def parse(s: String): Option[SignalToNoise] =
      (catching(classOf[NumberFormatException]) opt BigDecimal.exact(s)).flatMap(FromBigDecimalExact.getOption)

    def show(sn: SignalToNoise): String =
      sn.toBigDecimal.underlying.toPlainString

    Format(parse, show)
  }

  /**
   * Creates a `SignalToNoise` value from the given integer assuming it is
   * positive non-zero and less than 10,000,000.
   */
  def fromInt(sn: Int):  Option[SignalToNoise] =
    fromMilliLong(sn * 1000L)

  /**
   * Constructs a SignalToNoise from a BigDecimal, assuming it is in range and representable in 3 decimal places.
   * Otherwise throws an exception.
   */
  def unsafeFromBigDecimalExact(sn: BigDecimal): SignalToNoise =
    FromBigDecimalExact.getOption(sn).getOrElse(error(sn))

  given Order[SignalToNoise] =
    Order.fromLessThan(_ < _)

  given Decoder[SignalToNoise] =
    Decoder.decodeJsonNumber.emap { jNum =>
      for {
        bd <- jNum.toBigDecimal.toRight(s"Signal-to-noise values must be parsable as a decimal, not: $jNum")
        sn <- FromBigDecimalExact.getOption(bd).toRight(errorMessage(bd))
      } yield sn
    }

  given Encoder[SignalToNoise] =
    Encoder.encodeJsonNumber.contramap[SignalToNoise] { sn =>
      JsonNumber.fromDecimalStringUnsafe(FromString.reverseGet(sn))
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy