One9.Ms (omultiset v0.4.0-rc.0)

Operations on simple non-struct multiplicity maps (aka "multisets", t/1 and t_lax/1).

Most operations have 3 modes:

  • One9.Ms._func(%{...}, ...): "Just works", suitable for most use-cases. Returns correct results regardless of whether arguments are t/1 or t_lax/1, but guarantees to return t/1 whenever all arguments are t/1.

  • One9.Ms._func(%{...}, ..., :strict): "Fast path" when invariants can be controlled, such as inside a struct. Guarantees to return t/1, but invokes UB when any arguments are not t/1.

  • One9.Ms._func(%{...}, ..., :lax): Guarantees to preserve all keys from the inputs, even at the cost of having 0-valued entries in the result. (Testing TODO, but I believe these methods will also be faster than the "just works" methods.)

Summary

Functions

A more efficient alternative to One9.Ms.to_list(ms) |> Enum.at(index).

Determine the multiplicity of an element in a multiset.

Create a well-formed multiset from any (finite) Enumerable of values.

Delete (up to) the given number of copies of an element from a multiset.

Return the first multiset, less the elements in the second.

Return the first multiset, less the elements in the second, which must be a subset.

Determine whether the multiset is empty.

Determine whether two multisets are equal.

Construct a well-formed multiset from any enumerable of multiplicities (t0/0).

Return the intersection of two multisets.

Determine whether a value is present at all in a multiset.

Add additional copies (by default, 1 copy) of the element into the multiset.

Determine the cardinality of a multiset.

Determine whether the first multiset is a (non-strict) subset of the second.

Convert a multiset into a List of unique elements.

Return the cardinality of a multiset's support.

Convert a multiset into a complete List of elements (including repeats).

Return the union of two multisets.

Types

t0()

@type t0() :: t0(term())

t0(value)

@type t0(value) :: Enumerable.t({value, non_neg_integer()})

t()

@type t() :: t(term())

t(value)

@type t(value) :: %{optional(value) => pos_integer()}

t_lax()

@type t_lax() :: t_lax(term())

t_lax(value)

@type t_lax(value) :: %{optional(value) => non_neg_integer()}

Functions

at(ms, index)

@spec at(t(e) | t_lax(e) | t0(e), non_neg_integer()) :: e when e: term()

A more efficient alternative to One9.Ms.to_list(ms) |> Enum.at(index).

See also to_list/1.

count_element(ms, element)

@spec count_element(t() | t_lax(), term()) :: non_neg_integer()

Determine the multiplicity of an element in a multiset.

Examples

iex> %{"cat" => 10, "dog" => 10}
...> |> One9.Ms.count_element("dog")
10

iex> %{"cat" => 10, "dog" => 10}
...> |> One9.Ms.count_element("unicorn")
0

See also size/1.

counts(elements \\ [])

@spec counts([e]) :: t(e) when e: term()
@spec counts(Enumerable.t(e)) :: t(e) when e: term()

Create a well-formed multiset from any (finite) Enumerable of values.

Examples

iex> One9.Ms.counts()
%{}

iex> One9.Ms.counts([1, 2, 1, 1, 1])
%{1 => 4, 2 => 1} # four (4) copies of the number 1, and one (1) copy of the number 2.

iex> One9.Ms.counts(%{1 => 4}) # WATCH OUT: This is probably not what you want!
%{{1, 4} => 1} # one (1) copy of the tuple {1, 4}.

See also from_counts/1.

delete(ms, element)

@spec delete(t(e), term()) :: t(e) when e: term()
@spec delete(t_lax(e), term()) :: t_lax(e) when e: term()

Delete (up to) the given number of copies of an element from a multiset.

Examples

iex> %{"cat" => 10, "dog" => 10} |> delete("cat", 1)
%{"dog" => 10, "cat" => 9}

iex> %{"cat" => 10, "dog" => 10} |> delete("cat", 10)
%{"dog" => 10}

iex> %{"cat" => 10, "dog" => 10} |> delete("cat", 999)
%{"dog" => 10}

iex> %{"cat" => 10, "dog" => 10} |> delete("cat", 999, :lax)
%{"cat" => 0, "dog" => 10}

iex> %{"cat" => 10, "dog" => 10} |> delete("unicorn", 1)
%{"cat" => 10, "dog" => 10}

iex> %{"cat" => 10, "dog" => 10} |> delete("unicorn", 1, :lax)
%{"cat" => 10, "dog" => 10}

See also difference/2.

delete(ms, element, count)

@spec delete(t(e), term(), :strict) :: t(e) when e: term()
@spec delete(t_lax(e), term(), :lax) :: t_lax(e) when e: term()
@spec delete(t(e), term(), non_neg_integer() | :all) :: t(e) when e: term()
@spec delete(t_lax(e), term(), non_neg_integer() | :all) :: t_lax(e) when e: term()

delete(ms, element, count, strict)

@spec delete(t(e), term(), non_neg_integer() | :all, :strict) :: t(e) when e: term()
@spec delete(t_lax(e), term(), non_neg_integer() | :all, :lax) :: t_lax(e)
when e: term()

difference(ms1, ms2, strict \\ :strict)

@spec difference(t(e), t() | t_lax(), :strict) :: t(e) when e: term()
@spec difference(t_lax(e), t_lax(), :lax) :: t_lax(e) when e: term()

Return the first multiset, less the elements in the second.

Calculations are done soft aka "clamping".

Examples

iex> %{"cat" => 10, "dog" => 10}
...> |> difference(%{"dog" => 3})
%{"cat" => 10, "dog" => 7}

iex> %{"cat" => 10, "dog" => 10}
...> |> difference(%{"dog" => 3, "cat" => 10})
%{"dog" => 7}

iex> %{"cat" => 10, "dog" => 10}
...> |> difference(%{"dog" => 3, "cat" => 999})
%{"dog" => 7}

iex> %{"cat" => 10, "dog" => 10}
...> |> difference(%{"dog" => 3, "cat" => 10}, :lax)
%{"cat" => 0, "dog" => 7}

iex> %{"cat" => 10, "dog" => 10}
...> |> difference(%{"dog" => 3, "cat" => 999}, :lax)
%{"cat" => 0, "dog" => 7}

iex> %{"cat" => 10, "dog" => 10}
...> |> difference(%{"dog" => 3, "unicorn" => 1})
%{"cat" => 10, "dog" => 7}

iex> %{"cat" => 10, "dog" => 10}
...> |> difference(%{"dog" => 3, "unicorn" => 1}, :lax)
%{"cat" => 10, "dog" => 7}

See also delete/3, difference!/2.

difference!(ms1, ms2)

@spec difference!(t(e), t(e) | t_lax(e)) :: t(e) when e: term()
@spec difference!(t_lax(e), t_lax(e)) :: t_lax(e) when e: term()

Return the first multiset, less the elements in the second, which must be a subset.

Raises KeyError if the second multiset is not a subset of the first.

You should generally not use this function; instead, prefer testing subset?/2 and then using difference/2, which (I believe; TODO: test) is slightly faster.

Examples

iex> %{"cat" => 10, "dog" => 10}
...> |> difference!(%{"dog" => 3})
%{"cat" => 10, "dog" => 7}

iex> %{"cat" => 10, "dog" => 10}
...> |> difference!(%{"dog" => 3, "cat" => 10})
%{"dog" => 7}

iex> %{"cat" => 10, "dog" => 10}
...> |> difference!(%{"dog" => 3, "cat" => 999})
** (KeyError) key {"cat", 999} not found in: %{"cat" => 10, "dog" => 10}

iex> %{"cat" => 10, "dog" => 10}
...> |> difference!(%{"dog" => 3, "cat" => 10}, :lax)
%{"dog" => 7, "cat" => 0}

iex> %{"cat" => 10, "dog" => 10}
...> |> difference!(%{"dog" => 3, "cat" => 999}, :lax)
** (KeyError) key {"cat", 999} not found in: %{"cat" => 10, "dog" => 10}

iex> %{"cat" => 10, "dog" => 10}
...> |> difference!(%{"dog" => 3, "unicorn" => 1})
** (KeyError) key {"unicorn", 1} not found in: %{"cat" => 10, "dog" => 10}

iex> %{"cat" => 10, "dog" => 10}
...> |> difference!(%{"dog" => 3, "unicorn" => 0}, :lax)
%{"dog" => 7, "cat" => 10}

iex> %{"cat" => 10, "dog" => 10} # WATCH OUT: lax mode doesn't change the semantics!
...> |> difference!(%{"dog" => 3, "unicorn" => 1}, :lax)
** (KeyError) key {"unicorn", 1} not found in: %{"cat" => 10, "dog" => 10}

See also difference/2.

difference!(ms1, ms2, atom)

@spec difference!(t(e), t(e) | t_lax(e), :strict) :: t(e) when e: term()
@spec difference!(t_lax(e), t_lax(e), :lax) :: t_lax(e) when e: term()

empty?(ms)

@spec empty?(t() | t_lax()) :: boolean()

Determine whether the multiset is empty.

Examples

iex> One9.Ms.empty?(%{})
true

iex> One9.Ms.empty?(%{"pony" => 1})
false

iex> One9.Ms.empty?(%{"unicorn" => 0})
true

See also size/1, well_formed?/1.

empty?(ms, atom)

@spec empty?(t(), :strict) :: boolean()

equals?(ms1, ms2)

@spec equals?(t() | t_lax(), t() | t_lax()) :: boolean()

Determine whether two multisets are equal.

Examples

iex> One9.Ms.equals?(
...>   %{"cat" => 10, "dog" => 10},
...>   %{"dog" => 10, "cat" => 10}
...> )
true

iex> One9.Ms.equals?(
...>   %{"cat" => 10, "dog" => 10},
...>   %{"cat" => 10, "dog" => 10, "pony" => 1}
...> )
false

iex> One9.Ms.equals?(
...>   %{"cat" => 10, "dog" => 10},
...>   %{"cat" => 10, "dog" => 10, "unicorn" => 0}
...> )
true

equals?(ms1, ms2, atom)

@spec equals?(t(), t(), :strict) :: boolean()

from_counts(counts \\ %{})

@spec from_counts(counts :: t0(e)) :: t(e) when e: term()

Construct a well-formed multiset from any enumerable of multiplicities (t0/0).

Duplicate entries are combined additively.

Examples

iex> One9.Ms.from_counts(%{"cat" => 10, "dog" => 10, "unicorn" => 0})
%{"cat" => 10, "dog" => 10}

iex> One9.Ms.from_counts(true: 99, nil: 0, true: 1, false: 1)
%{true: 100, false: 1}

See also counts/1.

intersection(ms1, ms2)

@spec intersection(t_lax(e | e1), t_lax(e | e2)) :: t(e)
when e: term(), e1: term(), e2: term()

Return the intersection of two multisets.

Either/both operands may be non-well-formed. Result will always be well-formed.

iex> One9.Ms.intersection(%{a: 1, b: 2, c: 3}, %{b: 1, c: 2})
%{b: 1, c: 2}

iex> One9.Ms.intersection(%{a: 1, b: 999, z: 999}, %{a: 3, b: 2, c: 1})
%{a: 1, b: 2}

member?(ms, element)

@spec member?(t() | t_lax(), term()) :: boolean()

Determine whether a value is present at all in a multiset.

Examples

iex> %{"cat" => 10, "dog" => 10, "unicorn" => 0}
...> |> One9.Ms.member?("cat")
true

iex> %{"cat" => 10, "dog" => 10, "unicorn" => 0}
...> |> One9.Ms.member?("unicorn")
false

See also count_element/2.

member?(ms, element, atom)

@spec member?(t(), term(), :strict) :: boolean()

put(ms, element)

@spec put(t(e1), e2) :: t(e1 | e2) when e1: term(), e2: term()
@spec put(t_lax(e1), e2) :: t_lax(e1 | e2) when e1: term(), e2: term()

Add additional copies (by default, 1 copy) of the element into the multiset.

Examples

iex> %{"cat" => 10, "dog" => 10}
...> |> One9.Ms.put("pony")
%{"cat" => 10, "dog" => 10, "pony" => 1}

iex> %{"cat" => 10, "dog" => 10}
...> |> One9.Ms.put("unicorn", 0)
%{"cat" => 10, "dog" => 10}

iex> %{"cat" => 10, "dog" => 10}
...> |> One9.Ms.put("unicorn", 0, :lax)
%{"cat" => 10, "dog" => 10, "unicorn" => 0}

put(ms, element, count)

@spec put(t(e1), e2, pos_integer()) :: t(e1 | e2) when e1: term(), e2: term()
@spec put(t(e1), term(), 0) :: t(e1) when e1: term()
@spec put(t_lax(e1), e2, non_neg_integer()) :: t_lax(e1 | e2)
when e1: term(), e2: term()

put(ms, element, count, strict)

@spec put(t(e1), e2, pos_integer(), :strict) :: t(e1 | e2) when e1: term(), e2: term()
@spec put(t(e1), term(), 0, :strict) :: t(e1) when e1: term()
@spec put(t_lax(e1), e2, non_neg_integer(), :lax) :: t_lax(e1 | e2)
when e1: term(), e2: term()

size(ms)

@spec size(t() | t_lax()) :: non_neg_integer()

Determine the cardinality of a multiset.

Examples

iex> %{"cat" => 10, "dog" => 10} |> One9.Ms.size()
20

See also support_size/1, count_element/2 and empty?/1.

subset?(ms1, ms2)

@spec subset?(t() | t_lax(), t() | t_lax()) :: boolean()

Determine whether the first multiset is a (non-strict) subset of the second.

Examples

iex> One9.Ms.subset?(
...>   %{"cat" => 10, "dog" => 9},
...>   %{"cat" => 10, "dog" => 10}
...> )
true

iex> One9.Ms.subset?(
...>   %{"cat" => 10, "dog" => 10},
...>   %{"cat" => 10, "dog" => 10}
...> )
true

iex> One9.Ms.subset?(
...>   %{"cat" => 10, "unicorn" => 0},
...>   %{"cat" => 10, "dog" => 10}
...> )
true

iex> One9.Ms.subset?(
...>   %{"cat" => 10, "pony" => 1},
...>   %{"cat" => 10, "dog" => 10}
...> )
false

subset?(ms1, ms2, atom)

@spec subset?(t(), t(), :strict) :: boolean()

sum(ms1, ms2)

@spec sum(t(e1), t(e2)) :: t(e1 | e2) when e1: term(), e2: term()
@spec sum(t_lax(e1), t_lax(e2)) :: t_lax(e1 | e2) when e1: term(), e2: term()

support(ms)

@spec support(t(e)) :: [e] when e: term()
@spec support(t_lax(e)) :: [e] when e: term()

Convert a multiset into a List of unique elements.

Examples

iex> One9.Ms.counts([1, 1, 2, 42, 42, 42, 42, 42])
...> |> One9.Ms.support()
...> |> Enum.sort()
[1, 2, 42]

See also to_list/1, support_size/1, size/1.

support(ms, atom)

@spec support(t(e), :strict) :: [e] when e: term()
@spec support(t_lax(e), :lax) :: [e] when e: term()

support_size(ms)

@spec support_size(t() | t_lax()) :: non_neg_integer()

Return the cardinality of a multiset's support.

Examples

iex> %{"dog" => 10, "cat" => 10}
...> |> One9.Ms.support_size()
2

iex> %{"dog" => 10, "cat" => 10, "pony" => 1}
...> |> One9.Ms.support_size()
3

iex> %{"dog" => 10, "cat" => 10, "unicorn" => 0}
...> |> One9.Ms.support_size()
2

See also support/1, size/1.

support_size(ms, atom)

@spec support_size(t(), :strict) :: non_neg_integer()

symmetric_difference(ms1, ms2)

@spec symmetric_difference(t(e1), t(e2)) :: t(e1 | e2) when e1: term(), e2: term()
@spec symmetric_difference(t_lax(e1), t_lax(e2)) :: t_lax(e1 | e2)
when e1: term(), e2: term()

Examples

iex> One9.Ms.symmetric_difference(
...>   %{"cat" => 10, "dog" => 10},
...>   %{"cat" => 10, "dog" => 10}
...> )
%{}

iex> One9.Ms.symmetric_difference(
...>   %{"cat" => 10, "dog" => 4},
...>   %{"cat" => 3, "dog" => 10}
...> )
%{"cat" => 7, "dog" => 6}

iex> One9.Ms.symmetric_difference(
...>   %{"cat" => 10, "dog" => 10, "pony" => 1},
...>   %{"cat" => 10, "dog" => 15}
...> )
%{"dog" => 5, "pony" => 1}

iex> One9.Ms.symmetric_difference(
...>   %{"cat" => 10, "dog" => 10, "unicorn" => 0},
...>   %{"cat" => 10, "dog" => 10}, :lax
...> )
%{"cat" => 0, "dog" => 0, "unicorn" => 0}

symmetric_difference(ms1, ms2, strict)

@spec symmetric_difference(t(e1), t(e2), :strict) :: t(e1 | e2)
when e1: term(), e2: term()
@spec symmetric_difference(t_lax(e1), t_lax(e2), :lax) :: t_lax(e1 | e2)
when e1: term(), e2: term()

take(ms, element, count)

@spec take(t(e1), e2, non_neg_integer()) :: {t(e1), [e2]} when e1: term(), e2: term()
@spec take(t_lax(e1), e2, non_neg_integer()) :: {t_lax(e1), [e2]}
when e1: term(), e2: term()

Examples

iex> %{"cat" => 10, "dog" => 10}
...> |> One9.Ms.take("cat", 2)
{%{"cat" => 8, "dog" => 10}, ["cat", "cat"]}

iex> %{"cat" => 5, "dog" => 5}
...> |> One9.Ms.take("cat", 10000000000)
{%{"dog" => 5}, ["cat", "cat", "cat", "cat", "cat"]}

iex> %{"cat" => 5, "dog" => 5}
...> |> One9.Ms.take("cat", :all)
{%{"dog" => 5}, ["cat", "cat", "cat", "cat", "cat"]}

iex> %{"cat" => 5, "dog" => 5}
...> |> One9.Ms.take("cat", 10000000000, :lax)
{%{"cat" => 0, "dog" => 5}, ["cat", "cat", "cat", "cat", "cat"]}

take(ms, element, count, strict)

@spec take(t(e1), e2, non_neg_integer(), :strict) :: {t(e1), [e2]}
when e1: term(), e2: term()
@spec take(t_lax(e1), e2, non_neg_integer(), :lax) :: {t_lax(e1), [e2]}
when e1: term(), e2: term()

to_list(ms)

@spec to_list(t0(e)) :: [e] when e: term()

Convert a multiset into a complete List of elements (including repeats).

Examples

iex> One9.Ms.to_list(%{a: 1, b: 2})
...> |> Enum.sort()
[:a, :b, :b]

iex> One9.Ms.to_list([a: 1, b: 2, a: 3])
...> |> Enum.sort()
[:a, :a, :a, :a, :b, :b]

union(ms1, ms2)

@spec union(t(e1), t(e2)) :: t(e1 | e2) when e1: term(), e2: term()
@spec union(t_lax(e1), t_lax(e2)) :: t_lax(e1 | e2) when e1: term(), e2: term()

Return the union of two multisets.

Either/both operands may be non-well-formed. Result will be well-formed whenever both operands are well-formed.

iex> One9.Ms.union(%{a: 1, b: 2}, %{b: 1, c: 3})
%{a: 1, b: 2, c: 3}