Values against the bounds of a range: values moved from one range to another (rescale/3,
rescale_mid/4, rescale_max/3, or not at all, rescale_none/1), whether a range is zero
wide (zero_range?/1), a range widened at its ends (expand_range/2), and what is done with
values outside a range, R's oob_* family: censored (censor/2, censor_any/2), squished
onto its ends (squish/2, squish_any/2, squish_infinite/2), dropped (discard/2) or kept
(keep/2).
Each takes values as R holds its doubles (Scales.double/0) and gives R's answer to the last
digit, in R's order of operations, with IEEE arithmetic's infinities and NaN where R gives them:
over a zero-wide range, an infinite one, or an infinite value.
A range's end is a number or an infinity; for the oob_* family it may also be nil, which
bounds nothing on its side.
Summary
Types
An end of a range: a number, :infinity or :neg_infinity.
A range values outside of are found by: its ends, either nil for none on its side.
A range, its low end and its high end, each a number or an infinity.
Functions
Each value, nil where it is a finite number outside {low, high}, R's oob_censor().
Each value, nil where it is a number or an infinity outside {low, high}, R's
oob_censor_any(): as censor/2, the infinities censored too.
The values inside {low, high}, those outside dropped, R's oob_discard(), so fewer may come
back than were given.
The range widened at each end by a multiple of its width plus a constant, R's
expand_range(): the low end moved down by width * mult + add, the high end up, each end
by its own where {low_end, high_end} gives two. A zero-wide range (zero_range?/1) takes
zero_width as its width, so a single value is still widened by a multiple. A range high to
low is widened outward as R's is, its width negative, and a negative multiple or constant
narrows it, its ends crossing past the middle.
Every value as it is, R's oob_keep(), for a caller that handles values outside a range by a
function and wants none handled.
Moves each value linearly from the range from to the range to, R's rescale().
Each value over the high end of from, times the high end of to, R's rescale_max(), so 0
stays at 0; the low ends are not read.
Moves each value from the range from to the range to about midpoint, R's
rescale_mid(): midpoint goes to the middle of to, and the end of from further from
midpoint to the end of to on its side, so the nearer end falls short of its own.
Each value as it is, R's rescale_none(), for a caller that rescales by a function and wants
none.
Each value, moved onto the nearer end of {low, high} where it is a finite number outside it,
R's oob_squish().
Each value, moved onto the nearer end of {low, high} where it is a number or an infinity
outside it, R's oob_squish_any(): as squish/2, the infinities moved too.
Each infinity replaced by an end of {low, high}, R's oob_squish_infinite(): :neg_infinity
by low first, then :infinity by high, as R replaces them, so a low of :infinity is
replaced in turn. A finite number is kept, inside the range or not, and an end of nil puts
nil in an infinity's place, as R puts NA.
Whether a range is zero wide, R's zero_range(): its ends are equal, or so close, relative to
the smaller, that their difference is rounding error, under 1000 times the machine epsilon. A
range with one infinite end is not, and one with both the same infinity is.
Types
@type bound() :: number() | :infinity | :neg_infinity
An end of a range: a number, :infinity or :neg_infinity.
A range values outside of are found by: its ends, either nil for none on its side.
A range, its low end and its high end, each a number or an infinity.
Functions
Each value, nil where it is a finite number outside {low, high}, R's oob_censor().
Only finite numbers are censored: :infinity, :neg_infinity, :nan and values that are not
numbers are kept, and nil stays nil. An end of nil censors nothing on its side.
Examples
iex> Scales.Bounds.censor([1, 5, 10, nil, :infinity, :nan], {2, 8})
[nil, 5, nil, nil, :infinity, :nan]
iex> Scales.Bounds.censor([1, 5, 10], {nil, 8})
[1, 5, nil]
Each value, nil where it is a number or an infinity outside {low, high}, R's
oob_censor_any(): as censor/2, the infinities censored too.
Examples
iex> Scales.Bounds.censor_any([1, 5, :infinity, :neg_infinity, :nan, nil], {2, 8})
[nil, 5, nil, nil, :nan, nil]
The values inside {low, high}, those outside dropped, R's oob_discard(), so fewer may come
back than were given.
A number or an infinity is kept when inside the range, its ends included; nil is kept, NaN
is kept as nil, as R gives NA for both, and values that are not numbers are kept. An end of
nil bounds nothing on its side, where R gives NA for every value the other end keeps.
Examples
iex> Scales.Bounds.discard([1, 5, 8, 10, :infinity, nil, :nan], {2, 8})
[5, 8, nil, nil]
iex> Scales.Bounds.discard([1, 5, 10, :infinity], {2, nil})
[5, 10, :infinity]
@spec expand_range(range(), keyword()) :: {Scales.double(), Scales.double()}
The range widened at each end by a multiple of its width plus a constant, R's
expand_range(): the low end moved down by width * mult + add, the high end up, each end
by its own where {low_end, high_end} gives two. A zero-wide range (zero_range?/1) takes
zero_width as its width, so a single value is still widened by a multiple. A range high to
low is widened outward as R's is, its width negative, and a negative multiple or constant
narrows it, its ends crossing past the middle.
An end may be an infinity, with IEEE arithmetic's answer, as R's gives: an infinite width
times 0 is NaN, so a range reaching an infinity widened by no multiple gives :nan at both
ends, as R does. An end of nil or NaN is refused, as R fails on one; so are an infinite
multiple, constant or width, which R takes.
Options
:mult- The multiple of the width each end moves by, a number for both or{low_end, high_end}. The default value is0.:add- The constant each end moves by besides, a number for both or{low_end, high_end}. The default value is0.:zero_width- The width a zero-wide range is widened as though it had. The default value is1.
Examples
iex> Scales.Bounds.expand_range({1, 3}, mult: 0.05)
{0.9, 3.1}
iex> Scales.Bounds.expand_range({1, 3}, mult: {0.05, 0.1}, add: {0, 2})
{0.9, 5.2}
iex> Scales.Bounds.expand_range({5, 5}, mult: 0.05, zero_width: 10)
{4.5, 5.5}
iex> Scales.Bounds.expand_range({1, :infinity}, mult: 0.05)
{:neg_infinity, :infinity}
Every value as it is, R's oob_keep(), for a caller that handles values outside a range by a
function and wants none handled.
Examples
iex> Scales.Bounds.keep([1, 5, 10, :infinity], {2, 8})
[1, 5, 10, :infinity]
@spec rescale([Scales.double()], range(), range()) :: [Scales.double()]
Moves each value linearly from the range from to the range to, R's rescale().
Where either range is zero wide (zero_range?/1), every value is put at the middle of to,
but a missing one or NaN, which is nil, as R gives NA.
Examples
iex> Scales.Bounds.rescale([2, 4, nil, 10], {2, 10}, {0, 1})
[0.0, 0.25, nil, 1.0]
iex> Scales.Bounds.rescale([2, 6], {2, 10}, {10, -10})
[10.0, 0.0]
iex> Scales.Bounds.rescale([5, 5], {5, 5}, {0.1, 1.0})
[0.55, 0.55]
iex> Scales.Bounds.rescale([:infinity, 1, :nan], {0, :infinity}, {0, 1})
[:nan, 0.0, :nan]
@spec rescale_max([Scales.double()], range(), range()) :: [Scales.double()]
Each value over the high end of from, times the high end of to, R's rescale_max(), so 0
stays at 0; the low ends are not read.
Over a high end of 0, a value other than 0 is an infinity, :infinity or :neg_infinity, a
negative zero's sign counting, and 0 is NaN, :nan, as is any value whose infinity is
multiplied by a high end of to of 0.
Examples
iex> Scales.Bounds.rescale_max([1, 2, nil], {0, 4}, {0, 6})
[1.5, 3.0, nil]
iex> Scales.Bounds.rescale_max([0, 1, -1], {-2, 0}, {0, 1})
[:nan, :infinity, :neg_infinity]
@spec rescale_mid([Scales.double()], range(), range(), bound()) :: [Scales.double()]
Moves each value from the range from to the range to about midpoint, R's
rescale_mid(): midpoint goes to the middle of to, and the end of from further from
midpoint to the end of to on its side, so the nearer end falls short of its own.
Where either range is zero wide (zero_range?/1), every value is put at the middle of to,
but a missing one or NaN, which is nil, as R gives NA.
Examples
iex> Scales.Bounds.rescale_mid([-1, 0, 3, nil], {-1, 3}, {0, 1}, 1)
[0.0, 0.25, 1.0, nil]
iex> Scales.Bounds.rescale_mid([-1, 0, 3], {-1, 3}, {10, 20}, 1)
[10.0, 12.5, 20.0]
@spec rescale_none([Scales.double()]) :: [Scales.double()]
Each value as it is, R's rescale_none(), for a caller that rescales by a function and wants
none.
Examples
iex> Scales.Bounds.rescale_none([-1, 0.5, :infinity, nil])
[-1, 0.5, :infinity, nil]
Each value, moved onto the nearer end of {low, high} where it is a finite number outside it,
R's oob_squish().
Only finite numbers are squished: :infinity, :neg_infinity, :nan and values that are not
numbers are kept, and nil stays nil. An end of nil squishes nothing on its side. A value
below low is moved up to it first and one then above high down to it, as R moves them, so a
range given high to low puts every number at high.
Examples
iex> Scales.Bounds.squish([1, 5, 10, nil, :infinity, :nan], {2, 8})
[2, 5, 8, nil, :infinity, :nan]
iex> Scales.Bounds.squish([1, 5, 10], {nil, 8})
[1, 5, 8]
iex> Scales.Bounds.squish([1, 5, 10], {8, 2})
[2, 2, 2]
Each value, moved onto the nearer end of {low, high} where it is a number or an infinity
outside it, R's oob_squish_any(): as squish/2, the infinities moved too.
Examples
iex> Scales.Bounds.squish_any([1, 5, :infinity, :neg_infinity, :nan], {2, 8})
[2, 5, 8, 2, :nan]
Each infinity replaced by an end of {low, high}, R's oob_squish_infinite(): :neg_infinity
by low first, then :infinity by high, as R replaces them, so a low of :infinity is
replaced in turn. A finite number is kept, inside the range or not, and an end of nil puts
nil in an infinity's place, as R puts NA.
Examples
iex> Scales.Bounds.squish_infinite([:neg_infinity, 5, 10, :infinity, nil], {2, 8})
[2, 5, 10, 8, nil]
iex> Scales.Bounds.squish_infinite([:neg_infinity, :infinity], {:infinity, :neg_infinity})
[:neg_infinity, :neg_infinity]
Whether a range is zero wide, R's zero_range(): its ends are equal, or so close, relative to
the smaller, that their difference is rounding error, under 1000 times the machine epsilon. A
range with one infinite end is not, and one with both the same infinity is.
Examples
iex> Scales.Bounds.zero_range?({3, 3})
true
iex> Scales.Bounds.zero_range?({1.0, 1.0000000000002218})
true
iex> Scales.Bounds.zero_range?({1.0, 1.000000000000222})
false
iex> Scales.Bounds.zero_range?({0, :infinity})
false