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

hedgehog.Range.scala Maven / Gradle / Ivy

The newest version!
package hedgehog

import hedgehog.core.NumericPlus
import hedgehog.predef.{DecimalPlus, IntegralPlus}

/**
 * Tests are parameterized by the size of the randomly-generated data, the
 * meaning of which depends on the particular generator used.
 */
sealed abstract case class Size private (value: Int) {

  /** Represents the size as a percentage (0 - 1) which is useful for range calculations */
  def percentage: Double =
    value.toDouble / Size.max

  def incBy(v: Size): Size =
    Size(value + v.value)

  /**
   * Scale a size using the golden ratio.
   */
  def golden: Size =
    Size((value * 0.61803398875).toInt)
}

object Size {
  def apply(value: Int): Size = {
    val remainder = value % max
    new Size(if (remainder <= 0) remainder + max else remainder) {}
  }

  def max: Int =
    100
}

/**
 * A range describes the bounds of a number to generate, which may or may not
 * be dependent on a 'Size'.
 *
 * @param origin
 *   Get the origin of a range. This might be the mid-point or the lower bound,
 *   depending on what the range represents.
 *
 *   The 'bounds' of a range are scaled around this value when using the
 *   'linear' family of combinators.
 *
 *   When using a 'Range' to generate numbers, the shrinking function will
 *   shrink towards the origin.
 *
 * @param bounds
 *   Get the extents of a range, for a given size.
 */
case class Range[A](origin: A, bounds: Size => (A, A)) {

  /** Get the lower bound of a range for the given size. */
  def lowerBound(size: Size)(implicit O: Ordering[A]): A = {
    val (x, y) = bounds(size)
    O.min(x, y)
  }

  /** Get the upper bound of a range for the given size. */
  def upperBound(size: Size)(implicit O: Ordering[A]): A = {
    val (x, y) = bounds(size)
    O.max(x, y)
  }

  def map[B](f: A => B): Range[B] =
    Range(f(origin), s => {
      val (x, y) = bounds(s)
      (f(x), f(y))
    })
}

object Range {

  /**
   * Construct a range which represents a constant single value.
   *
   * {{{
   * scala> Range.singleton(5).bounds(x)
   * (5,5)
   *
   * scala> Range.singleton(5).origin
   * 5
   * }}}
   */
  def singleton[A](x: A): Range[A] =
    Range(x, _ => (x, x))

  /**
   * Construct a range which is unaffected by the size parameter.
   *
   * A range from `0` to `10`, with the origin at `0`:
   *
   * {{{
   * scala> Range.constant(0, 10).bounds(x)
   * (0,10)
   *
   * scala> Range.constant(0, 10).origin
   * 0
   * }}}
   */
  def constant[A](x: A, y: A): Range[A] =
    constantFrom(x, x, y)

  /**
   * Construct a range which is unaffected by the size parameter with a origin
   * point which may differ from the bounds.
   *
   * A range from `-10` to `10`, with the origin at `0`:
   *
   * {{{
   * scala> Range.constantFrom(0, -10, 10).bounds(x)
   * (-10,10)
   *
   * scala> Range.constantFrom(0, -10, 10).origin
   * 0
   * }}}
   *
   * A range from `1970` to `2100`, with the origin at `2000`:
   *
   * {{{
   * scala> Range.constantFrom(2000, 1970, 2100).bounds(x)
   * (1970,2100)
   *
   * scala> Range.constantFrom(2000, 1970, 2100).origin
   * 2000
   * }}}
   */
  def constantFrom[A](z: A, x: A, y: A): Range[A] =
    Range(z, _ => (x, y))

  /**
   * Construct a range which scales the second bound relative to the size
   * parameter.
   *
   * {{{
   * scala> Range.linear(0, 10).bounds(Size(1))
   * (0,0)
   *
   * scala> Range.linear(0, 10).bounds(Size(50))
   * (0,5)
   *
   * scala> Range.linear(0, 10).bounds(Size(100))
   * (0,10)
   * }}}
   */
  def linear[A : Integral : IntegralPlus : NumericPlus](x: A, y: A): Range[A] =
    linearFrom(x, x, y)

  /**
   * Construct a range which scales the second bound relative to the size
   * parameter.
   *
   * {{{
   * scala> Range.linearFrom(0, -10, 10).bounds(Size(1))
   * (0,0)
   *
   * scala> Range.linearFrom(0, -10, 20).bounds(Size(50))
   * (-5,10)
   *
   * scala> Range.linearFrom(0, -10, 20).bounds(Size(100))
   * (-10,20)
   * }}}
   */
  def linearFrom[A](z: A, x: A, y: A)(implicit I: Integral[A], J: IntegralPlus[A], R: NumericPlus[A]): Range[A] =
    // Check for overflow and if we do then start using BigInt
    if (I.lt(I.minus(y, x), I.zero) && I.gt(y, I.zero)) {
      linearFrom_(J.toBigInt(z), J.toBigInt(x), J.toBigInt(y))
        .map(J.fromBigInt)
    } else {
      linearFrom_(z, x, y)
    }

  def linearFrom_[A : Integral : NumericPlus](z: A, x: A, y: A): Range[A] =
    Range(z, sz => (
        clamp(x, y, scaleLinear(sz, z, x))
      , clamp(x, y, scaleLinear(sz, z, y))
      )
    )

  /**
   * Construct a range which scales the second bound relative to the size
   * parameter.
   *
   * This works the same as 'linear', but for fractional values.
   */
  def linearFrac[A : Fractional : DecimalPlus : NumericPlus](x: A, y: A): Range[A] =
    linearFracFrom(x, x, y)

  /**
   * Construct a range which scales the bounds relative to the size parameter.
   *
   * This works the same as [[linearFrom]], but for fractional values.
   */
  def linearFracFrom[A](z: A, x: A, y: A)(implicit I: Fractional[A], J: DecimalPlus[A], R: NumericPlus[A]): Range[A] =
    // Check for gross imprecision and lift to `BigDecimal` to ensure we don't produce a bad range
    if (I.toDouble(I.minus(y, x)).isInfinity) {
      linearFracFrom_(J.toBigDecimal(z), J.toBigDecimal(x), J.toBigDecimal(y))
        .map(J.fromBigDecimal)
    } else {
      linearFracFrom_(z, x, y)
    }

  def linearFracFrom_[A : Fractional : NumericPlus](z: A, x: A, y: A): Range[A] =
    Range(z, sz => (
        clamp(x, y, scaleLinearFrac(sz, z, x))
      , clamp(x, y, scaleLinearFrac(sz, z, y))
      )
    )

  /**
   * Truncate a value so it stays within some range.
   *
   * {{{
   * scala> clamp(5, 10, 15)
   * 10
   *
   * scala> clamp(5, 10, 0)
   * 5
   * }}}
   */
  def clamp[A](x: A, y: A, n: A)(implicit O: Ordering[A]): A =
    if (O.gt(x, y))
      O.min(x, O.max(y, n))
    else
      O.min(y, O.max(x, n))

  /** Scale an integral linearly with the size parameter. */
  def scaleLinear[A](sz: Size, z: A, n: A)(implicit I: Integral[A], J: NumericPlus[A]): A =
    I.plus(z, J.timesDouble(I.minus(n, z), sz.percentage))

  /** Scale a fractional number linearly with the size parameter. */
  def scaleLinearFrac[A](sz: Size, z: A, n: A)(implicit F: Fractional[A], J: NumericPlus[A]): A =
    F.plus(z, J.timesDouble(F.minus(n, z), sz.percentage))

  /** Check that list contains at least a certain number of elements. */
  def atLeast[A](n: Int, l: List[A]): Boolean =
    if (n == 0)
      true
    else
      l.drop(n - 1).nonEmpty
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy