View Source Cmp (Cmp v0.1.2)
Semantic comparison and sorting for Elixir.
why-cmp
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
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
2. Weakly typed
Built-in comparators accept any set of operands:
iex> 2 < "1"
true
iex> 0 < true
true
iex> false < nil
true
Cmp
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
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
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
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.Comparable
protocol - Robust and well-tested (both unit and property-based)
limitations
Limitations
Link to this section 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
.
Link to this section 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
Examples
iex> Cmp.compare(2, 1)
:gt
iex> Cmp.compare(1, 1.0)
:eq
It 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
Examples
iex> Cmp.eq?(2, 1)
false
iex> Cmp.eq?(1, 1.0)
true
It 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
Examples
iex> Cmp.gt?(2, 1)
true
iex> Cmp.gt?(1, 2)
false
iex> Cmp.gt?(1, 1.0)
false
It 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: nil
Unlike >/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
Examples
iex> Cmp.gte?(2, 1)
true
iex> Cmp.gte?(1, 2)
false
iex> Cmp.gte?(1, 1.0)
true
It 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: nil
Unlike >=/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
Examples
iex> Cmp.lt?(1, 2)
true
iex> Cmp.lt?(2, 1)
false
iex> Cmp.lt?(1, 1.0)
false
It 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: nil
Unlike </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
Examples
iex> Cmp.lte?(1, 2)
true
iex> Cmp.lte?(2, 1)
false
iex> Cmp.lte?(1, 1.0)
true
It 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: nil
Unlike <=/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
Examples
iex> Cmp.max([1, 3, 2])
3
Respects 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: nil
Raises 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
Examples
iex> Cmp.max(1, 2)
2
iex> Cmp.max(1, 1.0)
1
It 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: nil
Unlike 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
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: nil
Raises 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
Examples
iex> Cmp.min([1, 3, 2])
1
Respects 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: nil
Raises 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
Examples
iex> Cmp.min(2, 1)
1
iex> Cmp.min(1, 1.0)
1
It 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: nil
Unlike 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
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: nil
Raises 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
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
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"