DistributionSupport.java

package org.loudouncodes.randkit.api;

/**
 * Describes the mathematical support (domain) of a probability distribution: numeric lower/upper
 * bounds, whether each bound is closed (inclusive) or open (exclusive), and whether values are
 * treated as continuous reals or discrete integers.
 *
 * <p>Typical examples:
 *
 * <ul>
 *   <li>Normal: {@code (-∞, +∞)} continuous
 *   <li>Exponential: {@code [0, +∞)} continuous
 *   <li>Poisson: {@code {0,1,2,...}} discrete
 *   <li>Beta: {@code (0,1)} continuous (open interval)
 * </ul>
 */
public final class DistributionSupport {

  /** Indicates whether the distribution's values are continuous reals or discrete integers. */
  public enum Kind {
    /** Continuous, real-valued support. */
    CONTINUOUS,
    /** Discrete, integer-valued support. */
    DISCRETE
  }

  private final double lower;
  private final double upper;
  private final boolean lowerClosed;
  private final boolean upperClosed;
  private final Kind kind;

  /**
   * Creates a new support descriptor.
   *
   * @param lower the numeric lower bound, or {@link Double#NEGATIVE_INFINITY} for unbounded below
   * @param lowerClosed {@code true} if the lower bound is included (closed); ignored if unbounded
   *     below
   * @param upper the numeric upper bound, or {@link Double#POSITIVE_INFINITY} for unbounded above
   * @param upperClosed {@code true} if the upper bound is included (closed); ignored if unbounded
   *     above
   * @param kind whether values are continuous reals or discrete integers
   * @throws IllegalArgumentException if finite bounds are not strictly ordered ({@code lower <
   *     upper})
   */
  private DistributionSupport(
      double lower, boolean lowerClosed, double upper, boolean upperClosed, Kind kind) {
    if (!Double.isFinite(lower) && lower != Double.NEGATIVE_INFINITY) {
      throw new IllegalArgumentException("lower must be finite or -Infinity");
    }
    if (!Double.isFinite(upper) && upper != Double.POSITIVE_INFINITY) {
      throw new IllegalArgumentException("upper must be finite or +Infinity");
    }
    if (Double.isFinite(lower) && Double.isFinite(upper) && !(lower < upper)) {
      throw new IllegalArgumentException("lower must be strictly less than upper");
    }
    this.lower = lower;
    this.upper = upper;
    this.lowerClosed = lowerClosed;
    this.upperClosed = upperClosed;
    this.kind = kind;
  }

  /**
   * Returns the numeric lower bound.
   *
   * @return the lower bound, or {@link Double#NEGATIVE_INFINITY} if unbounded below
   */
  public double lower() {
    return lower;
  }

  /**
   * Returns the numeric upper bound.
   *
   * @return the upper bound, or {@link Double#POSITIVE_INFINITY} if unbounded above
   */
  public double upper() {
    return upper;
  }

  /**
   * Indicates whether the lower bound is included in the support (closed). Ignored when the support
   * is unbounded below.
   *
   * @return {@code true} if the lower bound is closed; {@code false} otherwise
   */
  public boolean isLowerClosed() {
    return lowerClosed;
  }

  /**
   * Indicates whether the upper bound is included in the support (closed). Ignored when the support
   * is unbounded above.
   *
   * @return {@code true} if the upper bound is closed; {@code false} otherwise
   */
  public boolean isUpperClosed() {
    return upperClosed;
  }

  /**
   * Returns whether the support is unbounded below.
   *
   * @return {@code true} if the lower bound is {@link Double#NEGATIVE_INFINITY}; otherwise {@code
   *     false}
   */
  public boolean isUnboundedBelow() {
    return lower == Double.NEGATIVE_INFINITY;
  }

  /**
   * Returns whether the support is unbounded above.
   *
   * @return {@code true} if the upper bound is {@link Double#POSITIVE_INFINITY}; otherwise {@code
   *     false}
   */
  public boolean isUnboundedAbove() {
    return upper == Double.POSITIVE_INFINITY;
  }

  /**
   * Returns the value kind (continuous or discrete).
   *
   * @return the support kind
   */
  public Kind kind() {
    return kind;
  }

  /**
   * Tests whether a numeric value lies within this support's interval, respecting open/closed
   * endpoints. For discrete supports, this method checks only the numeric interval; it does not
   * require {@code x} to be an integer.
   *
   * @param x the value to test
   * @return {@code true} if {@code x} lies in the support; {@code false} otherwise
   */
  public boolean contains(double x) {
    boolean aboveLower = isUnboundedBelow() || (lowerClosed ? x >= lower : x > lower);
    boolean belowUpper = isUnboundedAbove() || (upperClosed ? x <= upper : x < upper);
    return aboveLower && belowUpper;
  }

  /**
   * Creates a continuous (real-valued) support with the given bounds.
   *
   * @param lower the lower bound, or {@link Double#NEGATIVE_INFINITY} for unbounded below
   * @param lowerClosed {@code true} if the lower bound is closed (included)
   * @param upper the upper bound, or {@link Double#POSITIVE_INFINITY} for unbounded above
   * @param upperClosed {@code true} if the upper bound is closed (included)
   * @return a continuous {@code DistributionSupport}
   */
  public static DistributionSupport continuous(
      double lower, boolean lowerClosed, double upper, boolean upperClosed) {
    return new DistributionSupport(lower, lowerClosed, upper, upperClosed, Kind.CONTINUOUS);
  }

  /**
   * Creates a discrete (integer-valued) support with the given bounds.
   *
   * @param lower the lower bound, or {@link Double#NEGATIVE_INFINITY} for unbounded below
   * @param lowerClosed {@code true} if the lower bound is closed (included)
   * @param upper the upper bound, or {@link Double#POSITIVE_INFINITY} for unbounded above
   * @param upperClosed {@code true} if the upper bound is closed (included)
   * @return a discrete {@code DistributionSupport}
   */
  public static DistributionSupport discrete(
      double lower, boolean lowerClosed, double upper, boolean upperClosed) {
    return new DistributionSupport(lower, lowerClosed, upper, upperClosed, Kind.DISCRETE);
  }

  /** {@inheritDoc} */
  @Override
  public String toString() {
    String l = isUnboundedBelow() ? "(-Inf" : (lowerClosed ? "[" + lower : "(" + lower);
    String r = isUnboundedAbove() ? "+Inf)" : (upperClosed ? upper + "]" : upper + ")");
    return "Support" + l + ", " + r + " " + kind + ")";
  }

  /** {@inheritDoc} */
  @Override
  public int hashCode() {
    int h = 17;
    long a = Double.doubleToLongBits(lower);
    long b = Double.doubleToLongBits(upper);
    h = 31 * h + (int) (a ^ (a >>> 32));
    h = 31 * h + (int) (b ^ (b >>> 32));
    h = 31 * h + (lowerClosed ? 1 : 0);
    h = 31 * h + (upperClosed ? 1 : 0);
    h = 31 * h + kind.hashCode();
    return h;
  }

  /** {@inheritDoc} */
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof DistributionSupport)) return false;
    DistributionSupport d = (DistributionSupport) o;
    return Double.doubleToLongBits(lower) == Double.doubleToLongBits(d.lower)
        && Double.doubleToLongBits(upper) == Double.doubleToLongBits(d.upper)
        && lowerClosed == d.lowerClosed
        && upperClosed == d.upperClosed
        && kind == d.kind;
  }

  /* ---------- Common presets ---------- */

  /** Real line {@code (-∞, +∞)}, continuous. */
  public static final DistributionSupport REAL_LINE =
      continuous(Double.NEGATIVE_INFINITY, false, Double.POSITIVE_INFINITY, false);

  /** Non-negative reals {@code [0, +∞)}, continuous. */
  public static final DistributionSupport NON_NEGATIVE_REALS =
      continuous(0.0, true, Double.POSITIVE_INFINITY, false);

  /** Unit interval {@code (0, 1)}, continuous (open). */
  public static final DistributionSupport UNIT_INTERVAL_OPEN = continuous(0.0, false, 1.0, false);

  /** Unit interval {@code [0, 1]}, continuous (closed). */
  public static final DistributionSupport UNIT_INTERVAL_CLOSED = continuous(0.0, true, 1.0, true);

  /** Non-negative integers {@code {0,1,2,...}}, discrete. */
  public static final DistributionSupport NON_NEGATIVE_INTEGERS =
      discrete(0.0, true, Double.POSITIVE_INFINITY, false);
}