Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,13 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
## [v0.2.2](https://github.com/diffo-dev/outstanding/compare/v0.2.1...v0.2.2) (2025-05-19)

### Features
* deriving callback for Structs
* deriving callback for Structs

## [v0.2.3](https://github.com/diffo-dev/outstanding/compare/v0.2.2...v0.2.3) (2025-08-08)

### Features
* enhanced map map implementation to allow structs to resolve

## Fixes
* fixed regex test failing with Elixir 1.18.4

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ Out of the box we have outstanding protocol implementations for the following ty
| Keyword | [a: :a] | handled by List | (Keyword) List | non_empty_keyword |
| List | [:a] | | List | any_list, empty_list, non_empty_list |
| MapSet | MapSet.new([:a]) | uses difference | MapSet | any_map_set, empty_map_set, non_empty_map_set |
| Map | {a: :b, c: :d} | strict | Map | any_map, empty_map, non_empty_map |
| Map | {a: :b, c: :d} | strict | Map, any Struct | any_map, empty_map, non_empty_map |
| NaiveDateTime | ~N[2025-02-25 11:59:00] | | NaiveDateTime | any_naive_date_time, future_time, current_time, past_time |
| Range | 1 | | Range, Integer | any_range |
| Regex | ~r/foo/ | actual is argument | String.Chars implementations | - |
| Time | ~T[11:59:00.000] | | Time | any_time, current_time, future_time, past_time |
| Tuple | {a: :b} | handled by Any | Tuple | any_tuple |

Maps call outstanding on each element is expected, but allow extra elements in actual.
Maps call outstanding on each element is expected, but allow extra elements in actual. Maps can also be resolved by Structs.

Keywords are Lists of Tuples but are handled like Maps.

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.2
0.2.3
40 changes: 38 additions & 2 deletions lib/outstand.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1320,8 +1320,10 @@ defmodule Outstand do
cond do
actual == nil ->
:less_than

true ->
now = DateTime.utc_now()

case DateTime.compare(DateTime.shift(now, actual), DateTime.shift(now, expected)) do
:lt -> nil
_ -> :less_than
Expand All @@ -1347,8 +1349,10 @@ defmodule Outstand do
cond do
actual == nil ->
:greater_than

true ->
now = DateTime.utc_now()

case DateTime.compare(DateTime.shift(now, actual), DateTime.shift(now, expected)) do
:gt -> nil
_ -> :greater_than
Expand Down Expand Up @@ -1391,7 +1395,7 @@ defmodule Outstand do
:bounded_by

true ->
:nil
nil
end
end
end
Expand Down Expand Up @@ -1428,7 +1432,7 @@ defmodule Outstand do

cond do
DateTime.before?(shifted, min) or DateTime.after?(shifted, max) ->
:nil
nil

true ->
:unbounded_by
Expand Down Expand Up @@ -1534,6 +1538,38 @@ defmodule Outstand do
nil
end

@doc """
Calculates outstanding on two maps

## Examples

```
iex> Outstand.outstanding_map(%{}, %{})
nil
iex> Outstand.outstanding_map(%{a: 1}, %{b: 2})
%{a: 1}
iex> Outstand.outstanding_map(%{a: 1}, %{a: 2})
%{a: 1}
iex> Outstand.outstanding_map(%{a: 1}, %{a: 1, b: 2})
nil
```
"""
def outstanding_map(expected, actual) when is_map(expected) and is_map(actual) do
expected
|> Enum.reduce(
%{},
fn {key, expected_value}, acc ->
if (not Map.has_key?(actual, key) and key == :no_value) or
Outstanding.outstanding(expected_value, actual[key]) != nil do
Map.put(acc, key, Outstanding.outstanding(expected_value, actual[key]))
else
acc
end
end
)
|> Outstand.suppress()
end

@doc """
Types the argument, similar to Typable

Expand Down
3 changes: 2 additions & 1 deletion lib/outstanding.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ defprotocol Outstanding do
@spec outstanding?(t, any()) :: boolean()
def outstanding?(expected, actual)

@impl true
defmacro __deriving__(module, options) do
quote do
defimpl Outstanding, for: unquote(module) do
import Outstand, only: [map_to_struct: 2, outstanding?: 1]

def outstanding(expected, actual) do
case {expected, actual} do
{nil, nil} ->
Expand All @@ -45,6 +45,7 @@ defprotocol Outstanding do
|> Outstand.map_to_struct(name)
end
end

def outstanding?(expected, actual) do
Outstand.outstanding?(outstanding(expected, actual))
end
Expand Down
1 change: 1 addition & 0 deletions lib/outstanding/duration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defoutstanding expected :: Duration, actual :: Any do
expected
else
now = DateTime.utc_now()

if DateTime.shift(now, expected) == DateTime.shift(now, actual) do
nil
else
Expand Down
15 changes: 6 additions & 9 deletions lib/outstanding/map.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
use Outstand

defoutstanding expected :: Map, actual :: Any do
case Outstand.type_of(actual) do
Map ->
Map.keys(expected)
|> Enum.filter(fn key ->
(not Map.has_key?(actual, key) and key == :no_value) or
Outstanding.outstanding(expected[key], actual[key]) != nil
end)
|> Enum.into(%{}, &{&1, Outstanding.outstanding(expected[&1], actual[&1])})
|> Outstand.suppress()
case actual do
%_{} ->
Outstand.outstanding_map(expected, Map.from_struct(actual))

%{} ->
Outstand.outstanding_map(expected, actual)

_ ->
expected
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Outstanding.MixProject do
use Mix.Project

@name :outstanding
@version "0.2.2"
@version "0.2.3"
@description "Elixir protocol calculating outstanding from expected and actual"
@github_url "https://github.com/diffo-dev/outstanding"

Expand Down
2 changes: 1 addition & 1 deletion outstanding.livemd
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Outstanding Elixir Protocol

```elixir
Mix.install([{:outstanding, "~> 0.2.2"}], consolidate_protocols: false)
Mix.install([{:outstanding, "~> 0.2.3"}], consolidate_protocols: false)
```

## Overview
Expand Down
2 changes: 2 additions & 0 deletions test/function_arity_2_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ defmodule Outstanding.ExpectedFunctionArity2Test do
gen_result_outstanding_test("less_than value result, equal", {&Outstand.less_than/2, %Duration{hour: 1}}, %Duration{hour: 1}, :less_than)

gen_something_outstanding_test("greater_than value outstanding", {&Outstand.greater_than/2, %Duration{hour: 2}}, %Duration{hour: 1})

gen_something_outstanding_test("greater_than value outstanding, equal", {&Outstand.greater_than/2, %Duration{hour: 2}}, %Duration{hour: 2})

gen_nothing_outstanding_test("greater_than value realized", {&Outstand.greater_than/2, %Duration{hour: 2}}, %Duration{minute: 121})

gen_result_outstanding_test(
Expand Down
45 changes: 45 additions & 0 deletions test/map_and_struct_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule Outstanding.MapAndStructTest do
use ExUnit.Case
use Outstand

defstruct [:x, :y, :z]

gen_something_outstanding_test("key outstanding", %{x: :a, y: :b}, %__MODULE__{x: :a, z: :b})
gen_something_outstanding_test("value outstanding", %{x: :a, y: :b}, %__MODULE__{x: :b, y: :b})

gen_something_outstanding_test("key explicit nil value outstanding, no value", %{x: :a, y: &Outstand.explicit_nil/1}, %__MODULE__{
x: :a,
z: :b
})

gen_something_outstanding_test("key explicit nil value outstanding, not nil", %{x: :a, y: &Outstand.explicit_nil/1}, %__MODULE__{
x: :a,
y: :c,
z: :b
})

gen_something_outstanding_test("value falsy outstanding", %{x: false, y: :b}, %__MODULE__{x: :b, y: :b})
gen_something_outstanding_test("no value outstanding", %{x: :no_value, y: :b}, %__MODULE__{x: :b, y: :b})
gen_nothing_outstanding_test("realized", %{x: :a, y: :b}, %__MODULE__{x: :a, y: :b})
gen_nothing_outstanding_test("realized with extra item", %{x: :a, y: :b}, %__MODULE__{x: :a, y: :b, z: :b})
gen_nothing_outstanding_test("no value realized, nil value", %{x: :no_value, y: :b}, %__MODULE__{x: nil, y: :b})
gen_nothing_outstanding_test("no value realized, no key", %{x: :no_value, y: :b}, %__MODULE__{y: :b})
gen_result_outstanding_test("key result", %{x: :a, y: :b}, %__MODULE__{x: :a, z: :b}, %{y: :b})
gen_result_outstanding_test("value result", %{x: :a, y: :b}, %__MODULE__{x: :b, y: :b}, %{x: :a})

gen_result_outstanding_test("key explicit nil result, no value", %{x: :a, y: &Outstand.explicit_nil/1}, %__MODULE__{x: :a, z: :b}, %{
y: :explicit_nil
})

gen_result_outstanding_test(
"key explicit nil result, not nil",
%{x: :a, y: &Outstand.explicit_nil/1},
%__MODULE__{x: :a, y: :c, z: :b},
%{
y: :explicit_nil
}
)

gen_result_outstanding_test("value falsy result", %{x: false, y: :b}, %__MODULE__{x: :b, y: :b}, %{x: false})
gen_result_outstanding_test("no value result", %{x: :no_value, y: :b}, %__MODULE__{x: :b, y: :b}, %{x: :no_value})
end
3 changes: 1 addition & 2 deletions test/regex_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ defmodule Outstanding.RegexTest do
gen_nothing_outstanding_test("realized", ~r/foo/, "foo")
gen_nothing_outstanding_test("realized, match within string", ~r/foo/, "barfoobar")
gen_nothing_outstanding_test("realized, match within String.Chars implementation", ~r/foo/, :barfoobar)
#doesn't work as ~r/foo/ gets compiled separately so doesn't match
#gen_result_outstanding_test("value result", ~r/foo/, "bar", ~r/foo/)

test "value result" do
foo_regex = ~r/foo/
outstanding = foo_regex --- "bar"
Expand Down