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

lucuma.core.validation.InputValidSplitEpi.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.validation

import cats.data.NonEmptyChain
import cats.syntax.all.*
import eu.timepit.refined.api.Refined
import eu.timepit.refined.api.Validate as RefinedValidate
import eu.timepit.refined.collection.NonEmpty
import eu.timepit.refined.numeric.Positive
import eu.timepit.refined.types.numeric.PosBigDecimal
import eu.timepit.refined.types.numeric.PosInt
import eu.timepit.refined.types.string.NonEmptyString
import lucuma.core.optics.*
import lucuma.core.syntax.string.*
import lucuma.refined.*
import monocle.Iso
import monocle.Prism

import java.math.RoundingMode
import java.text.DecimalFormat
import scala.util.Try

/**
 * Convenience version of `ValidSplitEpi` when the error type is `NonEmptyChain[NonEmptyString]` and
 * `T` is `String`.
 */
object InputValidSplitEpi {

  /**
   * Build a `InputValidSplitEpi` that's always valid and doesn't normalize or format
   */
  val id: InputValidSplitEpi[String] = ValidSplitEpi.id

  /**
   * Build a `InputValidSplitEpi` from `getValid` and `reverseGet` functions
   */
  def apply[A](
    getValid:   String => EitherErrors[A],
    reverseGet: A => String
  ): InputValidSplitEpi[A] =
    ValidSplitEpi(getValid, reverseGet)

  /**
   * Build a `InputValidSplitEpi` from a `Format`
   */
  def fromFormat[A](
    format:       Format[String, A],
    errorMessage: NonEmptyString = "Invalid format".refined
  ): InputValidSplitEpi[A] =
    ValidSplitEpi(
      format.getOption.andThen(_.toRight(errorMessage).toEitherErrors),
      format.reverseGet
    )

  /**
   * Build a `InputValidSplitEpi` from a `Prism`
   */
  def fromPrism[A](
    prism:        Prism[String, A],
    errorMessage: NonEmptyString = "Invalid value".refined
  ): InputValidSplitEpi[A] =
    fromFormat(Format.fromPrism(prism), errorMessage)

  /**
   * Build a `InputValidSplitEpi` from an `Iso`
   */
  def fromIso[A](iso: Iso[String, A]): InputValidSplitEpi[A] =
    ValidSplitEpi(
      (iso.get).andThen(_.asRight),
      iso.reverseGet
    )

  /**
   * Build a `InputValidSplitEpi` for `String Refined P`
   */
  def refinedString[P](implicit
    v: RefinedValidate[String, P]
  ): InputValidSplitEpi[String Refined P] =
    id.refined[P](_ => NonEmptyChain("Invalid value".refined))

  /**
   * `InputValidSplitEpi` for `NonEmptyString`
   */
  val nonEmptyString: InputValidSplitEpi[NonEmptyString] =
    refinedString[NonEmpty].withErrorMessage(_ => "Must be defined".refined)

  /**
   * `InputValidSplitEpi` for `Int`
   */
  val int: InputValidSplitEpi[Int] =
    InputValidSplitEpi(
      s => fixIntString(s).toIntOption.toRight(NonEmptyChain("Must be an integer".refined)),
      _.toString
    )

  /**
   * Build a `InputValidSplitEpi` for `Int Refined P`
   */
  def refinedInt[P](implicit v: RefinedValidate[Int, P]): InputValidSplitEpi[Int Refined P] =
    int.refined[P](_ => NonEmptyChain("Invalid format".refined))

  /**
   * `InputValidSplitEpi` for `PosInt`
   */
  val posInt: InputValidSplitEpi[PosInt] =
    refinedInt[Positive]

  /**
   * `InputValidSplitEpi` for `BigDecimal`.
   *
   * Does not, and cannot, format to a particular number of decimal places. For that you need a
   * `TruncatedBigDecimal`.
   */
  val bigDecimal: InputValidSplitEpi[BigDecimal] =
    InputValidSplitEpi(
      s => fixDecimalString(s).parseBigDecimalOption.toRight(NonEmptyChain("Must be a number".refined)),
      _.toString.toLowerCase.replace("+", "") // Strip + sign from exponent.
    )

  /**
   * Build a `InputValidSplitEpi` for `BigDecimal Refined P`
   */
  def refinedBigDecimal[P](implicit
    v: RefinedValidate[BigDecimal, P]
  ): InputValidSplitEpi[BigDecimal Refined P] =
    bigDecimal.refined[P](_ => NonEmptyChain("Invalid format".refined))

  /**
   * `InputValidSplitEpi` for `PosBigDecimal`
   */
  val posBigDecimal: InputValidSplitEpi[PosBigDecimal] =
    refinedBigDecimal[Positive]

  /**
   * `InputValidSplitEpi` for `BigDecimal`, formatting with only one integer digit.
   */
  val bigDecimalWithScientificNotation: InputValidSplitEpi[BigDecimal] =
    InputValidSplitEpi(
      bigDecimal.getValid,
      n => Try(scientificNotationFormat(n)).toOption.orEmpty
    ) // We shouldn't need to catch errors here, but https://github.com/scala-js/scala-js/issues/4655

  /**
   * Build a `InputValidSplitEpi` for `BigDecimal Refined P`, formatting with only one integer
   * digit.
   */
  def refinedBigDecimalWithScientificNotation[P](implicit
    v: RefinedValidate[BigDecimal, P]
  ): InputValidSplitEpi[BigDecimal Refined P] =
    bigDecimalWithScientificNotation.refined[P](_ => NonEmptyChain("Invalid format".refined))

  /**
   * `InputValidSplitEpi` for `PosBigDecimal`, formatting with only one integer digit.
   */
  val posBigDecimalWithScientificNotation: InputValidSplitEpi[PosBigDecimal] =
    refinedBigDecimalWithScientificNotation[Positive]

  private def fixIntString(str: String): String = str match {
    case ""  => "0"
    case "-" => "-0"
    case _   => str
  }

  private def fixDecimalString(str: String): String = str match {
    case ""  => "0"
    case "-" => "-0"
    case "." => "0."
    case _   => str
  }

  private def scientificNotationFormat(
    x:        BigDecimal,
    decimals: Option[DigitCount] = none
  ): String = {
    val formatter = new DecimalFormat("0.0E0")
    formatter.setRoundingMode(RoundingMode.HALF_UP)
    formatter.setMaximumFractionDigits(decimals.map(_.value).getOrElse(x.precision - 1))
    formatter.format(x.bigDecimal).stripSuffix("E0").toLowerCase
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy