Packages

An spherical geometry library for Elixir

Current section

Files

Jump to
spherical lib spherical r1 interval.ex
Raw

lib/spherical/r1/interval.ex

defmodule Spherical.R1.Interval do
@moduledoc ~S"""
Represents a closed interval on ℝ¹.
"""
defstruct lo: 1.0, hi: 0.0 # Return an empty interval by default
alias __MODULE__
import Kernel, except: [length: 1]
@type t :: %Interval{lo: number, hi: number}
# API
@doc "Returns an empty interval."
def new do
%__MODULE__{lo: 1.0, hi: 0.0}
end
@doc "Returns an interval representing a single `point`."
def new(point) when is_number(point) do
%__MODULE__{lo: point, hi: point}
end
@doc "Returns an interval between `a` and `b`."
def new(a, b) when is_number(a) and is_number(b) do
%__MODULE__{lo: a, hi: b}
end
# epsilon represents a reasonable level of noise between two values
# that can be considered to be equal.
@epsilon 1.0e-14
# API
@doc "Checks whether the `interval` is empty."
def is_empty?(%Interval{lo: lo, hi: hi}), do: lo > hi
def is_empty?(_interval), do: false
@doc "Checks if the intervals contains the same points."
def is_equal?(%Interval{lo: lo, hi: hi}, %Interval{lo: lo, hi: hi}), do: true
def is_equal?(%Interval{}=first, %Interval{}=second) do
is_empty?(first) && is_empty?(second)
end
def is_equal?(_first, _second), do: false
@doc "Returns the midpoint of the `interval`."
def center(%Interval{lo: lo, hi: hi}), do: 0.5 * (lo + hi)
@doc """
Returns the length of the `interval`.
The length of an empty interval is negative.
"""
def length(%Interval{lo: lo, hi: hi}), do: hi - lo
@doc "Checks if the interval contains `point`."
def contains?(%Interval{}=first, %Interval{}=second) do
case is_empty? second do
true -> true
false -> first.lo <= second.lo && second.hi <= first.hi
end
end
def contains?(%Interval{lo: lo, hi: hi}, point) when is_number(point) do
lo <= point && point <= hi
end
@doc "Checks if the `interval` strictly contains `point`."
def interior_contains?(%Interval{lo: lo, hi: hi}, point) when is_number(point) do
lo < point && point < hi
end
def interior_contains?(%Interval{}=first, %Interval{}=second) do
case is_empty? second do
true -> true
false -> first.lo < second.lo && second.hi < first.hi
end
end
@doc "Check if `first` contains any points in common with `second`."
def intersects?(%Interval{}=first, %Interval{}=second) do
if first.lo <= second.lo do
second.lo <= first.hi && second.lo <= second.hi
else
first.lo <= second.hi && first.lo <= first.hi
end
end
@doc """
Check if the interior of the `first` contains any points in common
with `second`, including the latter's boundary.
"""
def interior_intersects?(%Interval{}=first, %Interval{}=second) do
second.lo < first.hi
&& first.lo < second.hi
&& first.lo < first.hi
&& second.lo <= first.hi
end
@doc """
Returns the interval containing **all** points common to `first` and
`second`.
"""
def intersection(%Interval{}=first, %Interval{}=second) do
Interval.new(max(first.lo, second.lo),
min(first.hi, second.hi))
end
@doc "Returns a copy of `interval` containing the given `point`."
def add_point(%Interval{}=interval, point) when is_number(point) do
cond do
is_empty? interval -> Interval.new(point, point)
point < interval.lo -> Interval.new(point, interval.hi)
point > interval.hi -> Interval.new(interval.lo, point)
true -> interval
end
end
@doc """
Returns the closest point in the `interval` to the given `point`.
The interval must be non-empty.
"""
def clamp_point(%Interval{}=interval, point) when is_number(point) do
max(interval.lo, min(interval.hi, point))
end
@doc """
Returns an `interval` that has been expanded on each side by
`margin`.
If `margin` is negative, then the function shrinks the `interval` on
each side by margin instead. The resulting interval may be empty.
Any expansion of an empty interval remains empty.
"""
def expanded(%Interval{lo: lo, hi: hi}=interval, margin) when is_number(margin) do
case is_empty? interval do
true -> interval
false -> Interval.new(lo - margin, hi + margin)
end
end
@doc """
Returns the smallest interval that contains the `first` and `second`
intervals.
"""
def union(%Interval{}=first, %Interval{}=second) do
cond do
is_empty? first -> second
is_empty? second -> first
true ->
Interval.new(min(first.lo, second.lo),
max(first.hi, second.hi))
end
end
@doc """
Reports whether the `first` interval can be transformed into the
`second` interval by moving each endpoint a small distance.
The empty interval is considered to be positioned arbitrarily on the
real line, so any interval with a small enough length will match the
empty interval.
"""
def approx_equal(%Interval{}=first, %Interval{}=second) do
cond do
is_empty? first -> length(second) <= 2 * @epsilon
is_empty? second -> length(first) <= 2 * @epsilon
true ->
abs(second.lo - first.lo) <= @epsilon &&
abs(second.hi - first.hi) <= @epsilon
end
end
end