Scales.Bounds (scales v0.1.0)

Copy Markdown View Source

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

bound()

@type bound() :: number() | :infinity | :neg_infinity

An end of a range: a number, :infinity or :neg_infinity.

bounds()

@type bounds() :: {bound() | nil, bound() | nil}

A range values outside of are found by: its ends, either nil for none on its side.

range()

@type range() :: {bound(), bound()}

A range, its low end and its high end, each a number or an infinity.

Functions

censor(values, arg)

@spec censor(list(), bounds()) :: list()

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]

censor_any(values, arg)

@spec censor_any(list(), bounds()) :: list()

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]

discard(values, arg)

@spec discard(list(), bounds()) :: list()

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]

expand_range(range, options \\ [])

@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 is 0.

  • :add - The constant each end moves by besides, a number for both or {low_end, high_end}. The default value is 0.

  • :zero_width - The width a zero-wide range is widened as though it had. The default value is 1.

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}

keep(values, arg)

@spec keep(list(), bounds()) :: list()

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]

rescale(values, from, to)

@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]

rescale_max(values, arg1, arg2)

@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]

rescale_mid(values, from, to, midpoint)

@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]

rescale_none(values)

@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]

squish(values, arg)

@spec squish(list(), bounds()) :: list()

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]

squish_any(values, arg)

@spec squish_any(list(), bounds()) :: list()

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]

squish_infinite(values, arg)

@spec squish_infinite(list(), bounds()) :: list()

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]

zero_range?(arg)

@spec zero_range?(range()) :: boolean()

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