Bounds v0.1.14 Bounds View Source

Bounds is a formalization of the {pos, len} tuple that is used in Erlang to slice binaries.

A Bounds value is similar to a Range value, with a few differences:

  • A Bounds value can be zero-length. (A Range value must represent at least one element.)

  • A Bounds value is always ascending. (A Range value may be descending.)

  • A Bounds value always has non-negative values. (A Range value may represent negative values.)

Point Bounds

The relaxation of the nonzero-size rule from Range, means that Bounds values may represent single points, equivalent to {pos, 0} tuples. Such Bounds values are said to be "point bounds" or "point-bounded." A Bounds value that is not point-bounded is said to be a "range bound" or "range-bounded", as there exists an equivalent Range value to it.

Many functions in Bounds return point bounds when used near the Bounds value's end points. For example, difference/2 subtracts an open interval from a closed interval; if used with the same argument on both sides, the mathematical result will be [a, b] - (a, b) = {(a, a), (b, b)} — a list of two point bounds representing the left-over endpoints from the closed interval.

This is an implementation choice made to allow the user of Bounds to not worry about whether their Bounds values represent open or closed intervals. Rather than explicitly defining Bounds values as representing open, half-closed, or closed intervals at all times, Bounds values instead represent generic intervals, which the algorithms in Bounds interpret as open, half-closed, or closed, either because that is the only useful interpretation for the given operation, or because a particular interpretation yields a value containing the most information—information from which the results of operations on the other types of intervals can be deduced as corollaries.

To return to the difference/2 example, if you want to compute the difference between two closed intervals represented as Bounds values, you can pass the results of difference/2 to the function ranges/1 to find only the range-bounded (i.e. not point-bounded) elements, or use the predicates point?/1 or range?/1 to filter for these values yourself.

Implementation

Bounds values are normalized as an interval [lower, upper] for convenience of calculation. All functions in the Bounds module expect a %Bounds{} to have been created by a call to a constructor function (e.g. new/2, from_poslen/1, from_range/1.) If you construct a Bounds value yourself, the following guard must hold:

%Bounds{lower: lower, upper: upper} when
  is_integer(lower) and is_integer(upper) and lower >= 0 and upper >= lower

Enumeration

Like Range, Bounds implements Enumerable; and thus, like Range values, Bounds values can be understood as a compact representation of an equivalent list value.

Unlike Range, Bounds has several decorators which implement Enumerable differently, to represent the different, equivalent abstractions that a Bounds value can be understood as.

A Bounds value, passed directly to Enum functions, will enumerate as a collection of all integer points in the closed interval [lower, upper]. If your algorithm wants "point" Bounds values (values where lower == upper) to be included as values in the enumeration, then this is the approach you want.

A common use-case is enumerating all half-closed intervals of some length n (e.g. [lower, lower + n)) which are contained by a given Bounds value. For example, if the Bounds value represents the bounds of a binary, then you might want the bounds of each byte of the binary: [0, 1), [1, 2), etc. The functions chunk_every/2, split_stepwise/2, and partitioned/3 will help with this.

Link to this section Summary

Functions

Returns a Bounds.Stepwise decorator value, which implements Enumerable.

Creates a new Bounds value by limiting the lower and upper bounds of value_bounds to be no more than the lower and upper bounds of clamp_bounds.

Determines whether the Bounds values a and b (both interpreted as half-closed intervals) can be joined to form a single larger interval, with no discontinuities or overlap.

Removes the open interval sub_bounds from the closed interval bounds and returns whatever is left over.

Determines whether the Bounds values a and b have no points in common when both are interpreted as half-closed intervals.

Returns the end points (i.e. the points on either end) of the closed interval represented by the Bounds value.

Constructs a Bounds value representing the bounds of the given binary bin, i.e. the interval

Creates a point-bounded Bounds value equivalent to the integer offset point.

Converts a {pos, len} tuple to an equivalent Bounds value.

Converts a Range to an equivalent Bounds value.

Filters the passed Map or Enumerable value for only the Bounds values which are point-bounded, additionally casting them to integer offsets.

Merges an enumerable of contiguous intervals (order ignored) defined by bounds_enum into a single interval containing the superset of the points of all the intervals provided.

Merges the contiguous intervals a and b into a single interval containing the superset of the points of both intervals.

Casts a compatible value to a Bounds value.

Constructs a new Bounds value from a position and a length.

Determines whether the Bounds values a and b, have any point in common when both are interpreted as half-closed intervals.

Returns a Bounds.Partitioned decorator value, which implements Enumerable.

Determines whether a Bounds value is zero-length.

Filters the passed Map or Enumerable value for only the Bounds values which are point-bounded.

Determines whether a Bounds value is range-bounded — i.e. whether it has a nonzero size/1.

Filters the passed Map or Enumerable value for only range-bounded Bounds values.

Returns the size of the Bounds value when interpreted as a half-closed interval [lower, upper).

Takes a sub-interval of bounds as a new Bounds value.

Returns two sub-intervals representing the result of "breaking" bounds at a given position.

Splits a Bounds value into three parts, returned as a map with the following keys

Determines whether the endpoints of the Bounds value part are entirely contained within the endpoints of the Bounds value whole, when both are interpreted as closed intervals. Has the additional constraint that the bounds must not be equal.

Determines whether the endpoints of the Bounds value part are entirely contained within the endpoints of the Bounds value whole, when both are interpreted as closed intervals.

Given a point-bounded Bounds value, returns the integer offset equivalent to it.

Converts a Bounds value to an equivalent {pos, len} tuple.

Given a range-bounded Bounds value, returns the Range value equivalent to it.

Returns a new Bounds value representing the result of translating (sliding) the endpoints of bounds up or down by the integer displacement disp.

Link to this section Functions

Link to this function

chunk_every(bounds, step, opts \\ []) View Source

Returns a Bounds.Stepwise decorator value, which implements Enumerable.

The values enumerated from a Bounds.Stepwise decorator are themselves Bounds values, representing a set of contiguous intervals, each of size step_size. The enumeration always begins with the interval [0, step_size) (if it exists.)

Any bounded interval smaller than step_size is not considered a part of the enumeration.

This enumeration strategy is useful when you have a sequence of unit-sized chunks (like the bytes in a binary), in which a representation for one element is encoded as step_size contiguous chunks. The enumerated values will then represent the bounds of the representations of all potentially-valid elements.

Examples

Get the bounds of each single byte of a binary:

iex> Bounds.from_binary("foo") |> Bounds.chunk_every(1) |> Enum.to_list()
[%Bounds{lower: 0, upper: 1}, %Bounds{lower: 1, upper: 2}, %Bounds{lower: 2, upper: 3}]

Get the bounds of a sequence of 32-bit values in a binary:

iex> Bounds.from_binary("0123456789") |> Bounds.chunk_every(4, partials: :discard) |> Enum.to_list()
[%Bounds{lower: 0, upper: 4}, %Bounds{lower: 4, upper: 8}]
Link to this function

clamp(value_bounds, clamp_bounds) View Source

Creates a new Bounds value by limiting the lower and upper bounds of value_bounds to be no more than the lower and upper bounds of clamp_bounds.

If value_bounds fully contains clamp_bounds, the result of the clamp will be clamp_bounds exactly.

Determines whether the Bounds values a and b (both interpreted as half-closed intervals) can be joined to form a single larger interval, with no discontinuities or overlap.

See also: overlap?/2, disjoint?/2, subset?/2

Examples

Disjoint intervals fail:

iex> Bounds.contiguous?(Bounds.new(0, 4), Bounds.new(5, 5))
false

Overlapping intervals fail:

iex> Bounds.contiguous?(Bounds.new(0, 6), Bounds.new(5, 5))
false

Contiguous intervals succeed:

iex> Bounds.contiguous?(Bounds.new(0, 5), Bounds.new(5, 5))
true
Link to this function

difference(bounds, sub_bounds) View Source

Removes the open interval sub_bounds from the closed interval bounds and returns whatever is left over.

If sub_bounds has one or both endpoints in common with bounds, point (zero-length) bounds will be left over at the shared endpoint.

Examples

Subtracting from the middle produces two interval-bounds:

iex> Bounds.difference(%Bounds{lower: 0, upper: 10}, %Bounds{lower: 3, upper: 6})
[%Bounds{lower: 0, upper: 3}, %Bounds{lower: 6, upper: 10}]

Subtracting from one end produces one point-bound and one interval-bound:

iex> Bounds.difference(%Bounds{lower: 0, upper: 10}, %Bounds{lower: 0, upper: 2})
[%Bounds{lower: 0, upper: 0}, %Bounds{lower: 2, upper: 10}]

Subtracting everything produces two point-bounds:

iex> Bounds.difference(%Bounds{lower: 0, upper: 10}, %Bounds{lower: 0, upper: 10})
[%Bounds{lower: 0, upper: 0}, %Bounds{lower: 10, upper: 10}]

Subtracting from a point always produces the same point:

iex> Bounds.difference(%Bounds{lower: 5, upper: 5}, %Bounds{lower: 0, upper: 10})
[%Bounds{lower: 5, upper: 5}]

Determines whether the Bounds values a and b have no points in common when both are interpreted as half-closed intervals.

See also: overlap?/2

Examples

Disjoint values succeed:

iex> Bounds.disjoint?(Bounds.from_range(1..5), Bounds.from_range(4..10))
false

iex> Bounds.disjoint?(Bounds.from_range(1..5), Bounds.from_range(5..10))
false

Everything else fails:

iex> Bounds.disjoint?(Bounds.from_range(1..5), Bounds.from_range(6..10))
true

iex> Bounds.disjoint?(Bounds.from_range(1..10), Bounds.from_range(1..10))
false

iex> Bounds.disjoint?(Bounds.from_range(1..10), Bounds.from_range(3..6))
false

Returns the end points (i.e. the points on either end) of the closed interval represented by the Bounds value.

If the Bounds value represents a point, there will be one end point returned (the same point.) Otherwise, there will be two points returned.

Constructs a Bounds value representing the bounds of the given binary bin, i.e. the interval:

[0, byte_size(bin))

Creates a point-bounded Bounds value equivalent to the integer offset point.

Converts a {pos, len} tuple to an equivalent Bounds value.

Converts a Range to an equivalent Bounds value.

Filters the passed Map or Enumerable value for only the Bounds values which are point-bounded, additionally casting them to integer offsets.

Merges an enumerable of contiguous intervals (order ignored) defined by bounds_enum into a single interval containing the superset of the points of all the intervals provided.

Raises a Bounds.DisjointError exception if there would be any discontinuities in the resulting interval.

See also: join/2, contiguous?/2

Merges the contiguous intervals a and b into a single interval containing the superset of the points of both intervals.

Raises a Bounds.DisjointError exception if a and b are not contiguous intervals.

See also: contiguous?/2

Casts a compatible value to a Bounds value.

Constructs a new Bounds value from a position and a length.

Determines whether the Bounds values a and b, have any point in common when both are interpreted as half-closed intervals.

See also: disjoint?/2

Examples

Disjoint values fail:

iex> Bounds.overlap?(Bounds.from_range(1..5), Bounds.from_range(4..10))
true

iex> Bounds.overlap?(Bounds.from_range(1..5), Bounds.from_range(5..10))
true

Everything else succeeds:

iex> Bounds.overlap?(Bounds.from_range(1..5), Bounds.from_range(6..10))
false

iex> Bounds.overlap?(Bounds.from_range(1..10), Bounds.from_range(1..10))
true

iex> Bounds.overlap?(Bounds.from_range(1..10), Bounds.from_range(3..6))
true
Link to this function

partitioned(bounds, step, offset) View Source

Returns a Bounds.Partitioned decorator value, which implements Enumerable.

The values enumerated from a Bounds.Partitioned decorator will be Bounds values representing a set of contiguous intervals. Most of these will be steps of size step. The first full interval (if it exists) will be [offset, offset + step).

Unlike with intervals/2, the values enumerated from a Bounds.Partitioned value will include intervals smaller than step—namely:

  • if offset is nonzero, an initial interval [0, offset) will appear at the beginning of the enumeration (if it exists.)

  • if, after removing the initial interval, step does not evenly divide the bounds, then a final interval [step * n, step * n + remainder) will appear at the end of the enumeration (if it exists.)

This enumeration strategy is useful when you are using Bounds to compactly represent a set of values, and you wish to "bin" these values into contiguous bins of size step.

Determines whether a Bounds value is zero-length.

Filters the passed Map or Enumerable value for only the Bounds values which are point-bounded.

Determines whether a Bounds value is range-bounded — i.e. whether it has a nonzero size/1.

Filters the passed Map or Enumerable value for only range-bounded Bounds values.

Returns the size of the Bounds value when interpreted as a half-closed interval [lower, upper).

Link to this function

slice(bounds, slicing_bounds) View Source

Takes a sub-interval of bounds as a new Bounds value.

slicing_bounds does not represent an absolute interval to intersect with bounds, but rather is taken as a relative offset and length, as in Enum.slice/3.

This operation can be understood as having equivalent semantics to that of Enum.slice/3. The offset and length will be "clamped" so that the result will be fully contained by bounds.

Link to this function

split_at(bounds, point_or_idx_or_offset) View Source

Returns two sub-intervals representing the result of "breaking" bounds at a given position.

The second argument can take a few forms, each with a different result:

  • If the second argument is an integer ≥ 0, it will be used to calculate a point to break at, relative to bounds.lower;
  • If the second argument is an integer < 0, it will be used to calculate a point to break at, relative to bounds.upper;
  • If the second argument is a Bounds value and Bounds.point?(bounds) is true, it will be used as an absolute point to break at.

Examples

Non-negative relative offsets:

iex> Bounds.split_at(%Bounds{lower: 5, upper: 10}, 1)
[%Bounds{lower: 5, upper: 6}, %Bounds{lower: 6, upper: 10}]

Negative relative offsets:

iex> Bounds.split_at(%Bounds{lower: 5, upper: 10}, -1)
[%Bounds{lower: 5, upper: 9}, %Bounds{lower: 9, upper: 10}]

Absolute break positions:

iex> Bounds.split_at(%Bounds{lower: 5, upper: 10}, %Bounds{lower: 7, upper: 7})
[%Bounds{lower: 5, upper: 7}, %Bounds{lower: 7, upper: 10}]

If the interval is broken at one of its endpoints, one of the resulting sub-intervals will be a point bound:

iex> Bounds.split_at(%Bounds{lower: 0, upper: 10}, 0)
[%Bounds{lower: 0, upper: 0}, %Bounds{lower: 0, upper: 10}]

If a relative offset is past the end of the original bounds (in either direction), the original bounds will be returned:

iex> Bounds.split_at(%Bounds{lower: 0, upper: 10}, 20)
[%Bounds{lower: 0, upper: 10}]

iex> Bounds.split_at(%Bounds{lower: 0, upper: 10}, -20)
[%Bounds{lower: 0, upper: 10}]
Link to this function

split_stepwise(bounds, step) View Source

Splits a Bounds value into three parts, returned as a map with the following keys:

  • :whole: the bounds of the contiguous set of values whose bounds are both 1. within the original bounds, and 2. divisible by step_size. This is equivalent to the concat/2enation of the chunk_every/2 enumeration of the given bounds at the given step_size.
  • :partial_before: the interval extending from the beginning of the original bounds, to the beginning of whole.
  • :partial_after: the interval extending from the end of whole, to the end of the original bounds.

Any/all of the values in this map may turn out to be zero-sized "point" Bounds.

Link to this function

strict_subset?(whole, part) View Source

Determines whether the endpoints of the Bounds value part are entirely contained within the endpoints of the Bounds value whole, when both are interpreted as closed intervals. Has the additional constraint that the bounds must not be equal.

See also: subset?/2

Examples

Disjoint values fail:

iex> Bounds.strict_subset?(Bounds.from_range(1..5), Bounds.from_range(4..10))
false

iex> Bounds.strict_subset?(Bounds.from_range(1..5), Bounds.from_range(5..10))
false

Merely overlapping values fail:

iex> Bounds.strict_subset?(Bounds.from_range(1..5), Bounds.from_range(6..10))
false

Strict subsets succeed:

iex> Bounds.strict_subset?(Bounds.from_range(1..10), Bounds.from_range(3..6))
true

But non-strict subsets fail:

iex> Bounds.strict_subset?(Bounds.from_range(1..10), Bounds.from_range(1..10))
false

Determines whether the endpoints of the Bounds value part are entirely contained within the endpoints of the Bounds value whole, when both are interpreted as closed intervals.

See also: strict_subset?/2

Examples

Disjoint values fail:

iex> Bounds.subset?(Bounds.from_range(1..5), Bounds.from_range(4..10))
false

iex> Bounds.subset?(Bounds.from_range(1..5), Bounds.from_range(5..10))
false

Merely overlapping values fail:

iex> Bounds.subset?(Bounds.from_range(1..5), Bounds.from_range(6..10))
false

Strict subsets succeed:

iex> Bounds.subset?(Bounds.from_range(1..10), Bounds.from_range(3..6))
true

As do non-strict subsets:

iex> Bounds.subset?(Bounds.from_range(1..10), Bounds.from_range(1..10))
true

Given a point-bounded Bounds value, returns the integer offset equivalent to it.

Raises an exception if the given Bounds value is not point-bounded.

Converts a Bounds value to an equivalent {pos, len} tuple.

Given a range-bounded Bounds value, returns the Range value equivalent to it.

Raises an exception if the given Bounds value is not range-bounded.

Returns a new Bounds value representing the result of translating (sliding) the endpoints of bounds up or down by the integer displacement disp.