Current section
Files
Jump to
Current section
Files
lib/bounds.ex
defmodule Bounds do
@moduledoc ~S"""
`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.)
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 `stepwise/2`,
`split_stepwise/2`, and `partitioned/3` will help with this.
"""
defstruct [
lower: 0,
upper: 0
]
def new(pos, len), do: from_poslen({pos, len})
def new({pos, len}), do: from_poslen({pos, len})
def new(%Range{} = r), do: from_range(r)
def from_poslen(poslen)
def from_poslen({pos, len}) when is_integer(pos) and is_integer(len) and pos >= 0 and len >= 0, do:
%__MODULE__{lower: pos, upper: pos + len}
def to_poslen(%__MODULE__{lower: lower, upper: upper}), do:
{lower, upper - lower}
def at_point(point) when is_integer(point) and point >= 0, do:
%__MODULE__{lower: point, upper: point}
def point?(%__MODULE__{lower: point, upper: point}), do: true
def point?(%__MODULE__{lower: lower, upper: upper}) when lower < upper, do: false
def to_point(%__MODULE__{lower: point, upper: point}), do: point
def to_point(%__MODULE__{lower: lower, upper: upper} = bounds) when lower < upper, do:
raise ArgumentError, "cannot convert #{inspect bounds} to point"
def points(m) when is_map(m), do:
Map.new(Enum.filter(m, fn {_, bounds} -> point?(bounds) end))
def points(enum), do:
Enum.filter(enum, &point?/1)
def from_range(first..last) when is_integer(first) and is_integer(last) and first >= 0 and last >= first, do:
%__MODULE__{lower: first, upper: last + 1}
def range?(%__MODULE__{lower: point, upper: point}), do: false
def range?(%__MODULE__{lower: lower, upper: upper}) when lower < upper, do: true
def to_range(%__MODULE__{lower: point, upper: point} = bounds), do:
raise ArgumentError, "cannot convert #{inspect bounds} to Range"
def to_range(%__MODULE__{lower: lower, upper: upper}) when lower < upper, do:
{lower, upper - 1}
def ranges(m) when is_map(m), do:
Map.new(Enum.filter(m, fn {_, bounds} -> range?(bounds) end))
def ranges(enum), do:
Enum.filter(enum, &range?/1)
def size(%__MODULE__{lower: lower, upper: upper}), do: upper - lower
@doc ~S"""
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.stepwise(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("abcdEFGH") |> Bounds.stepwise(4) |> Enum.to_list()
[%Bounds{lower: 0, upper: 4}, %Bounds{lower: 4, upper: 8}]
"""
def stepwise(%__MODULE__{} = bounds, step_size \\ 1) when is_integer(step_size) and step_size >= 1 do
%Bounds.Stepwise{bounds: bounds, step: step_size}
end
@doc ~S"""
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 `union/2` of the `stepwise/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.
"""
def split_stepwise(%__MODULE__{lower: lower, upper: upper} = bounds, 1), do: %{
partial_before: %Bounds{lower: lower, upper: lower},
whole: bounds,
partial_after: %Bounds{lower: upper, upper: upper}
}
def split_stepwise(%__MODULE__{lower: lower, upper: upper}, step) when is_integer(step) and step >= 2 do
whole_lower = case rem(lower, step) do
0 -> lower
n when n > 0 -> lower - n + step
end
whole_upper = upper - rem(upper, step)
%{
partial_before: %Bounds{lower: lower, upper: whole_lower},
whole: %Bounds{lower: whole_lower, upper: whole_upper},
partial_after: %Bounds{lower: whole_upper, upper: upper},
}
end
# @doc ~S"""
# 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`.
# ## Examples
# iex> Bounds.from_binary("foo") |> Bounds.intervals(1) |> Enum.to_list()
# [%Bounds{lower: 0, upper: 1}, %Bounds{lower: 1, upper: 2}, %Bounds{lower: 2, upper: 3}]
# iex> Bounds.from_binary("abcdEFGH") |> Bounds.intervals(4) |> Enum.to_list()
# [%Bounds{lower: 0, upper: 4}, %Bounds{lower: 4, upper: 8}]
# """
def partitioned(%__MODULE__{} = _bounds, step, offset) when is_integer(step) and step >= 1 and is_integer(offset) and offset < step do
throw :not_implemented
# full_part = stepwise(translate(bounds, offset))
# %Bounds.Partitioned{bounds: bounds, step: step}
end
def endpoints(%__MODULE__{lower: point, upper: point} = bounds), do:
[bounds]
def endpoints(%__MODULE__{lower: lower, upper: upper}) when lower < upper, do:
[%__MODULE__{lower: lower, upper: lower}, %__MODULE__{lower: upper, upper: upper}]
def translate(%__MODULE__{lower: lower, upper: upper}, offset) when is_integer(offset) do
%__MODULE__{lower: lower + offset, upper: upper + offset}
end
def slice(%__MODULE__{lower: lower, upper: upper}, %__MODULE__{lower: a, upper: b}) do
new_lower = :erlang.min(lower + a, upper)
new_upper = :erlang.min(lower + b, upper)
%__MODULE__{lower: new_lower, upper: new_upper}
end
def slice(%__MODULE__{} = bounds, other), do:
slice(bounds, Bounds.new(other))
def clamp(value_bounds, clamp_bounds)
def clamp(%__MODULE__{lower: lower, upper: upper}, %__MODULE__{lower: clamp_lower, upper: clamp_upper}) do
new_lower = :erlang.min(:erlang.max(lower, clamp_lower), clamp_upper)
new_upper = :erlang.min(:erlang.max(upper, clamp_lower), clamp_upper)
%__MODULE__{lower: new_lower, upper: new_upper}
end
def difference(%__MODULE__{lower: lower, upper: upper} = bounds, %__MODULE__{} = sub_bounds) do
%__MODULE__{lower: a, upper: b} = clamp(sub_bounds, bounds)
uniq_pair(%__MODULE__{lower: lower, upper: a}, %__MODULE__{lower: b, upper: upper})
end
def split(%__MODULE__{lower: lower, upper: upper} = bounds, %__MODULE__{lower: point, upper: point}) when point >= lower and point <= upper, do:
split(bounds, point - lower)
def split(%__MODULE__{lower: lower, upper: upper} = bounds, offset) when is_integer(offset) and offset < 0 and (lower - upper) <= offset, do:
split(bounds, upper + offset)
def split(%__MODULE__{lower: lower, upper: upper}, at_idx) when is_integer(at_idx) and at_idx >= 0 and at_idx <= (upper - lower), do:
uniq_pair(%__MODULE__{lower: lower, upper: lower + at_idx}, %__MODULE__{lower: lower + at_idx, upper: upper})
# def partition(@empty = b, _at_idxs), do: b
# def partition(%__MODULE__{record: {_, _}} = b0, at_idxs) do
# Enum.reduce(at_idxs, [b0], fn at_idx, [last_bound | bounds_acc] ->
# case split(last_bound, at_idx) do
# {^last_bound} -> [last_bound | bounds_acc]
# {split_before, split_after} -> [split_after | [split_before | bounds_acc]]
# end
# end)
# |> Enum.reverse()
# end
def concat(%Bounds{} = bounds_a, %Bounds{} = bounds_b), do:
concat_all(order_pair(bounds_a, bounds_b))
def concat(bounds_enum), do:
concat_all(Enum.sort(bounds_enum))
defp concat_all(sorted_bounds_enum) do
Enum.reduce(sorted_bounds_enum, fn
%__MODULE__{lower: common, upper: upper}, %__MODULE__{lower: lower, upper: common} ->
%Bounds{lower: lower, upper: upper}
bounds_new, bounds_acc ->
raise Bounds.DisjointError, {bounds_acc, bounds_new}
end)
end
@compile inline: [uniq_pair: 2, order_pair: 2]
defp uniq_pair(a, a), do: [a]
defp uniq_pair(a, b), do: [a, b]
defp order_pair(a, b) when a > b, do: [b, a]
defp order_pair(a, b), do: [a, b]
end
defimpl Inspect, for: Bounds do
import Inspect.Algebra
def inspect(%Bounds{lower: point, upper: point}, opts) do
concat([
color("@", :tuple, opts),
color(to_string(point), :number, opts)
# color(")", :tuple, opts)
])
end
def inspect(%Bounds{lower: lower, upper: upper}, opts) when lower < upper do
concat([
# color("(", :tuple, opts),
color(to_string(lower), :number, opts),
color("...", :tuple, opts),
color(to_string(upper), :number, opts)
# color(")", :tuple, opts)
])
end
end
defimpl Enumerable, for: Bounds do
def count(%Bounds{lower: lower, upper: upper}), do:
{:ok, (upper - lower) + 1}
def member?(%Bounds{lower: lower, upper: upper}, %Bounds{lower: point, upper: point}) when point >= lower and point <= upper, do: {:ok, true}
def member?(%Bounds{}, _), do: {:ok, false}
def reduce(%Bounds{lower: lower, upper: upper}, acc, fun), do:
reduce(lower, (upper - lower) + 1, acc, fun)
defp reduce(_n, _take, {:halt, acc}, _fun), do:
{:halted, acc}
defp reduce(n, take, {:suspend, acc}, fun), do:
{:suspended, acc, &reduce(n, take, &1, fun)}
defp reduce(_, 0, {:cont, acc}, _fun), do:
{:done, acc}
defp reduce(n, take, {:cont, acc}, fun) when take > 0, do:
reduce(n + 1, take - 1, fun.(%Bounds{lower: n, upper: n}, acc), fun)
def slice(%Bounds{lower: lower, upper: upper}) do
count = (upper - lower) + 1
slicer = fn offset, len ->
new_lower = lower + offset
new_upper = :erlang.min(new_lower + len, upper)
slicer_accum(new_lower, new_upper, [])
end
{:ok, count, slicer}
end
defp slicer_accum(lower, upper, acc) when lower > upper, do: acc
defp slicer_accum(lower, upper, acc) when lower <= upper do
val = %Bounds{lower: upper, upper: upper}
slicer_accum(lower, upper - 1, [val | acc])
end
end
# defimpl Collectable, for: AbstractEVM.Bounds do
# alias AbstractEVM.Bounds
# def into(%AbstractEVM.Bounds{} = bounds_orig) do
# collector_fun = fn
# bounds_acc, {:cont, %AbstractEVM.Bounds{} = bounds_new} ->
# Bounds.union(bounds_acc, bounds_new)
# _bounds_acc, {:cont, other} ->
# raise ArgumentError, "cannot cast #{inspect(other)} to AbstractEVM.Bounds"
# bounds_acc, :done ->
# bounds_acc
# _bounds_acc, :halt ->
# :ok
# end
# {bounds_orig, collector_fun}
# end
# end