View Source Cmp (Cmp v0.1.3)
Semantic comparison and sorting for Elixir.
Why Cmp?
The built-in comparison operators as well as functions like Enum.sort/2 or Enum.max/1
are based on Erlang's term ordering and suffer two issues, which require attention and
might lead to unexpected behaviors or bugs:
1. Structural comparisons
Built-ins use structural comparison over semantic comparison:
~D[2020-03-02] > ~D[2019-06-06]
false
iex> Enum.sort([~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]])
[~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]]Semantic comparison is available but not straightforward:
iex> Date.compare(~D[2019-01-01], ~D[2020-03-02])
:lt
iex> Enum.sort([~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]], Date)
[~D[2019-01-01], ~D[2019-06-06], ~D[2020-03-02]]Cmp does the right thing out of the box:
iex> Cmp.gt?(~D[2020-03-02], ~D[2019-06-06])
true
iex> Cmp.sort([~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]])
[~D[2019-01-01], ~D[2019-06-06], ~D[2020-03-02]]2. Weakly typed
Built-in comparators accept any set of operands:
iex> 2 < "1"
true
iex> 0 < true
true
iex> false < nil
trueCmp will only compare compatible elements or raise a Cmp.TypeError:
iex> Cmp.lte?(1, 1.0)
true
iex> Cmp.lte?(2, "1")
** (Cmp.TypeError) Failed to compare incompatible types - left: 2, right: "1"What's in the box
- Boolean comparisons:
eq?/2,lt?/2,gt?/2,lte?/2,gte?/2 - Equivalents of
Kernel.min/2/Kernel.max/2:Cmp.min/2,Cmp.max/2 - Equivalents of
Enum.min/1/Enum.max/1/Enum.sort/2:Cmp.min/1,Cmp.max/1,Cmp.sort/2 compare/2
Supported types
The Cmp.Comparable protocol is implemented for the following types:
It isn't implemented for atoms by design, since atoms are not semantically an ordered type.
It supports tuples of the same size and types:
iex> Cmp.max({12, ~D[2019-06-06]}, {12, ~D[2020-03-02]})
{12, ~D[2020-03-02]}
iex> Cmp.max({12, "Foo"}, {15, nil})
** (Cmp.TypeError) Failed to compare incompatible types - left: "Foo", right: nil
iex> Cmp.max({12, "Foo"}, {15})
** (Cmp.TypeError) Failed to compare incompatible types - left: {12, "Foo"}, right: {15}Decimal support can prevent nasty bugs too:
iex> max(Decimal.new(2), Decimal.from_float(1.0))
#Decimal<1.0>
iex> Cmp.max(Decimal.new(2), Decimal.from_float(1.0))
#Decimal<2>See the Cmp.Comparable documentation to implement the protocol for other existing
or new structs.
Design goals
- Fast and well-optimized - the overhead should be quite small over built-in equivalents.
See the
benchmarks/folder for more details. - No need to require macros, plain functions
- Easily extensible through the
Cmp.Comparableprotocol - Robust and well-tested (both unit and property-based)
Supporting comparisons between non-homogeneous types such as mixed Decimal and
built-in numbers for instance is a non-goal. This limitation is a necessary
trade-off in order to ensure the points above. Use the Decimal library
directly if you need this.
Limitations
Summary
Functions
Returns :gt if left is semantically greater than right, lt if left
is less than right, and :eq if they are equal.
Safe equivalent to ==/2, which only works if both types are compatible and
uses semantic comparison.
Safe equivalent to >/2, which only works if both types are compatible and
uses semantic comparison.
Safe equivalent to >=/2, which only works if both types are compatible and
uses semantic comparison.
Safe equivalent to </2, which only works if both types are compatible and
uses semantic comparison.
Safe equivalent to <=/2, which only works if both types are compatible and
uses semantic comparison.
Safe equivalent of Enum.max/2, returning the minimum of a non-empty
enumerable of comparables.
Safe equivalent to max/2, which only works if both types are compatible and
uses semantic comparison.
Safe equivalent of Enum.max_by/3, returning the element of a non-empty
enumerable for which fun gives the maximum comparable value.
Safe equivalent of Enum.min/2, returning the maximum of a non-empty
enumerable of comparables.
Safe equivalent to min/2, which only works if both types are compatible and
uses semantic comparison.
Safe equivalent of Enum.min_by/3, returning the element of a non-empty
enumerable for which fun gives the minimum comparable value.
Safe equivalent of Enum.sort/2.
Safe equivalent of Enum.sort_by/2.
Functions
@spec compare(Cmp.Comparable.t(), Cmp.Comparable.t()) :: :eq | :lt | :gt
Returns :gt if left is semantically greater than right, lt if left
is less than right, and :eq if they are equal.
Examples
iex> Cmp.compare(2, 1)
:gt
iex> Cmp.compare(1, 1.0)
:eqIt will raise a Cmp.TypeError if trying to compare incompatible types
iex> Cmp.compare(1, "")
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: ""
@spec eq?(Cmp.Comparable.t(), Cmp.Comparable.t()) :: boolean()
Safe equivalent to ==/2, which only works if both types are compatible and
uses semantic comparison.
Examples
iex> Cmp.eq?(2, 1)
false
iex> Cmp.eq?(1, 1.0)
trueIt will raise a Cmp.TypeError if trying to compare incompatible types
iex> Cmp.eq?(1, "")
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: ""
@spec gt?(Cmp.Comparable.t(), Cmp.Comparable.t()) :: boolean()
Safe equivalent to >/2, which only works if both types are compatible and
uses semantic comparison.
Examples
iex> Cmp.gt?(2, 1)
true
iex> Cmp.gt?(1, 2)
false
iex> Cmp.gt?(1, 1.0)
falseIt will raise a Cmp.TypeError if trying to compare incompatible types
iex> Cmp.gt?(1, nil)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nilUnlike >/2, it will perform a semantic comparison for structs and not a
structural comparison:
iex> Cmp.gt?(~D[2020-03-02], ~D[2019-06-06])
true
~D[2020-03-02] > ~D[2019-06-06]
false
@spec gte?(Cmp.Comparable.t(), Cmp.Comparable.t()) :: boolean()
Safe equivalent to >=/2, which only works if both types are compatible and
uses semantic comparison.
Examples
iex> Cmp.gte?(2, 1)
true
iex> Cmp.gte?(1, 2)
false
iex> Cmp.gte?(1, 1.0)
trueIt will raise a Cmp.TypeError if trying to compare incompatible types
iex> Cmp.gte?(1, nil)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nilUnlike >=/2, it will perform a semantic comparison for structs and not a
structural comparison:
iex> Cmp.gte?(~D[2020-03-02], ~D[2019-06-06])
true
~D[2020-03-02] >= ~D[2019-06-06]
false
@spec lt?(Cmp.Comparable.t(), Cmp.Comparable.t()) :: boolean()
Safe equivalent to </2, which only works if both types are compatible and
uses semantic comparison.
Examples
iex> Cmp.lt?(1, 2)
true
iex> Cmp.lt?(2, 1)
false
iex> Cmp.lt?(1, 1.0)
falseIt will raise a Cmp.TypeError if trying to compare incompatible types
iex> Cmp.lt?(1, nil)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nilUnlike </2, it will perform a semantic comparison for structs and not a
structural comparison:
iex> Cmp.lt?(~D[2019-06-06], ~D[2020-03-02])
true
~D[2019-06-06] < ~D[2020-03-02]
false
@spec lte?(Cmp.Comparable.t(), Cmp.Comparable.t()) :: boolean()
Safe equivalent to <=/2, which only works if both types are compatible and
uses semantic comparison.
Examples
iex> Cmp.lte?(1, 2)
true
iex> Cmp.lte?(2, 1)
false
iex> Cmp.lte?(1, 1.0)
trueIt will raise a Cmp.TypeError if trying to compare incompatible types
iex> Cmp.lte?(1, nil)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nilUnlike <=/2, it will perform a semantic comparison for structs and not a
structural comparison:
iex> Cmp.lte?(~D[2019-06-06], ~D[2020-03-02])
true
~D[2019-06-06] <= ~D[2020-03-02]
false
@spec max(Enumerable.t(elem)) :: elem when elem: Cmp.Comparable.t()
Safe equivalent of Enum.max/2, returning the minimum of a non-empty
enumerable of comparables.
Examples
iex> Cmp.max([1, 3, 2])
3Respects semantic comparison:
iex> Cmp.max([~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]])
~D[2020-03-02]Raises a Cmp.TypeError on non-uniform enumerables:
iex> Cmp.max([1, nil, 2])
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nilRaises an Enum.EmptyError on empty enumerables:
iex> Cmp.max([])
** (Enum.EmptyError) empty error
@spec max(value, value) :: value when value: Cmp.Comparable.t()
Safe equivalent to max/2, which only works if both types are compatible and
uses semantic comparison.
Examples
iex> Cmp.max(1, 2)
2
iex> Cmp.max(1, 1.0)
1It will raise a Cmp.TypeError if trying to compare incompatible types
iex> Cmp.max(1, nil)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nilUnlike max/2, it will perform a semantic comparison for structs and not a
structural comparison:
iex> Cmp.max(~D[2020-03-02], ~D[2019-06-06])
~D[2020-03-02]
max(~D[2020-03-02], ~D[2019-06-06])
~D[2019-06-06]
@spec max_by(Enumerable.t(elem), (elem -> Cmp.Comparable.t())) :: elem when elem: term()
Safe equivalent of Enum.max_by/3, returning the element of a non-empty
enumerable for which fun gives the maximum comparable value.
Examples
iex> Cmp.max_by([%{x: 1}, %{x: 3}, %{x: 2}], & &1.x)
%{x: 3}Respects semantic comparison:
iex> Cmp.max_by([%{date: ~D[2020-03-02]}, %{date: ~D[2019-06-06]}], & &1.date)
%{date: ~D[2020-03-02]}Raises a Cmp.TypeError on non-uniform enumerables:
iex> Cmp.max_by([%{x: 1}, %{x: nil}, %{x: 2}], & &1.x)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nilRaises an Enum.EmptyError on empty enumerables:
iex> Cmp.max_by([], & &1.x)
** (Enum.EmptyError) empty error
@spec min(Enumerable.t(elem)) :: elem when elem: Cmp.Comparable.t()
Safe equivalent of Enum.min/2, returning the maximum of a non-empty
enumerable of comparables.
Examples
iex> Cmp.min([1, 3, 2])
1Respects semantic comparison:
iex> Cmp.min([~D[2020-03-02], ~D[2019-06-06]])
~D[2019-06-06]Raises a Cmp.TypeError on non-uniform enumerables:
iex> Cmp.min([1, nil, 2])
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nilRaises an Enum.EmptyError on empty enumerables:
iex> Cmp.min([])
** (Enum.EmptyError) empty error
@spec min(value, value) :: value when value: Cmp.Comparable.t()
Safe equivalent to min/2, which only works if both types are compatible and
uses semantic comparison.
Examples
iex> Cmp.min(2, 1)
1
iex> Cmp.min(1, 1.0)
1It will raise a Cmp.TypeError if trying to compare incompatible types
iex> Cmp.min(1, nil)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nilUnlike min/2, it will perform a semantic comparison for structs and not a
structural comparison:
iex> Cmp.min(~D[2020-03-02], ~D[2019-06-06])
~D[2019-06-06]
min(~D[2020-03-02], ~D[2019-06-06])
~D[2020-03-02]
@spec min_by(Enumerable.t(elem), (elem -> Cmp.Comparable.t())) :: elem when elem: term()
Safe equivalent of Enum.min_by/3, returning the element of a non-empty
enumerable for which fun gives the minimum comparable value.
Examples
iex> Cmp.min_by([%{x: 1}, %{x: 3}, %{x: 2}], & &1.x)
%{x: 1}Respects semantic comparison:
iex> Cmp.min_by([%{date: ~D[2020-03-02]}, %{date: ~D[2019-06-06]}], & &1.date)
%{date: ~D[2019-06-06]}Raises a Cmp.TypeError on non-uniform enumerables:
iex> Cmp.min_by([%{x: 1}, %{x: nil}, %{x: 2}], & &1.x)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nilRaises an Enum.EmptyError on empty enumerables:
iex> Cmp.min_by([], & &1.x)
** (Enum.EmptyError) empty error
@spec sort(Enumerable.t(elem), :asc | :desc) :: Enumerable.t(elem) when elem: Cmp.Comparable.t()
Safe equivalent of Enum.sort/2.
Examples
iex> Cmp.sort([3, 1, 2])
[1, 2, 3]
iex> Cmp.sort([3, 1, 2], :desc)
[3, 2, 1]Respects semantic comparison:
iex> Cmp.sort([~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]])
[~D[2019-01-01], ~D[2019-06-06], ~D[2020-03-02]]Raises a Cmp.TypeError on non-uniform enumerables:
iex> Cmp.sort([~D[2019-01-01], nil, ~D[2020-03-02]])
** (Cmp.TypeError) Failed to compare incompatible types - left: ~D[2019-01-01], right: nil
@spec sort_by(Enumerable.t(elem), (elem -> Cmp.Comparable.t()), :asc | :desc) :: Enumerable.t(elem) when elem: term()
Safe equivalent of Enum.sort_by/2.
Examples
iex> Cmp.sort_by([%{x: 3}, %{x: 1}, %{x: 2}], & &1.x)
[%{x: 1}, %{x: 2}, %{x: 3}]
iex> Cmp.sort_by([%{x: 3}, %{x: 1}, %{x: 2}], & &1.x, :desc)
[%{x: 3}, %{x: 2}, %{x: 1}]Respects semantic comparison:
iex> Cmp.sort_by([%{date: ~D[2020-03-02]}, %{date: ~D[2019-06-06]}], & &1.date)
[%{date: ~D[2019-06-06]}, %{date: ~D[2020-03-02]}]Raises a Cmp.TypeError on non-uniform enumerables:
iex> Cmp.sort_by([%{x: 3}, %{x: "1"}, %{x: 2}], & &1.x)
** (Cmp.TypeError) Failed to compare incompatible types - left: 3, right: "1"