From cbe0364846c40390fb96de2889385ce82b56b56b Mon Sep 17 00:00:00 2001 From: Adam Lindberg Date: Thu, 2 Jul 2020 21:56:53 +0200 Subject: [PATCH] Add deep_update_with/4 --- CHANGELOG.md | 14 ++++++++++-- src/mapz.app.src | 2 +- src/mapz.erl | 56 +++++++++++++++++++++++++++++---------------- test/mapz_tests.erl | 29 +++++++++++++++++++---- 4 files changed, 74 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b95d80..e72e1a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [2.1.0] - 2020-07-02 + +### Added + +- New [`deep_update_with/4`][deep_update_with-4] function + +[deep_update_with-4]: https://hexdocs.pm/mapz/mapz.html#deep_update_with-4 + ## [2.0.0] - 2019-11-27 ### Added @@ -41,7 +50,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial release -[unreleased]: https://github.com/eproxus/mapz/compare/v2.0.0...HEAD +[unreleased]: https://github.com/eproxus/mapz/compare/v2.1.0...HEAD +[2.1.0]: https://github.com/eproxus/mapz/compare/v2.0.0...v2.1.0 [2.0.0]: https://github.com/eproxus/mapz/compare/v1.0.0...v2.0.0 [1.0.0]: https://github.com/eproxus/mapz/compare/v0.3.0...v1.0.0 [0.3.0]: https://github.com/eproxus/mapz/releases/tag/v0.3.0 diff --git a/src/mapz.app.src b/src/mapz.app.src index a50982b..3775f33 100644 --- a/src/mapz.app.src +++ b/src/mapz.app.src @@ -1,6 +1,6 @@ {application, mapz, [ {description, "Extensions to the Erlang maps module"}, - {vsn, "2.0.0"}, + {vsn, "2.1.0"}, {applications, [kernel, stdlib]}, {licenses, ["MIT"]}, diff --git a/src/mapz.erl b/src/mapz.erl index 441590f..ebec636 100644 --- a/src/mapz.erl +++ b/src/mapz.erl @@ -7,6 +7,7 @@ -export([deep_put/3]). -export([deep_update/3]). -export([deep_update_with/3]). +-export([deep_update_with/4]). -export([deep_remove/2]). -export([deep_merge/1]). -export([deep_merge/2]). @@ -88,15 +89,9 @@ deep_get(Path, Map, Default) -> -spec deep_put(path(), term(), map()) -> map(). deep_put(Path, Value, Map1) -> check(Path, Map1), - update(Map1, Path, - fun(_Existing) -> Value end, - fun - (P, _Rest, {ok, _Existing}) -> - error({badvalue, P}); - (_P, Rest, error) -> - lists:foldr(fun(Key, Acc) -> #{Key => Acc} end, Value, Rest) - end - ). + update(Map1, Path, fun(_Existing) -> Value end, fun(P, Rest, V) -> + badvalue_and_create(P, Rest, V, Value) + end). % @doc If `Path' exists in `Map1', the old associated value is replaced by value % `Value'. The function returns a new map `Map2' containing the new associated @@ -113,13 +108,7 @@ deep_put(Path, Value, Map1) -> -spec deep_update(path(), term(), map()) -> map(). deep_update(Path, Value, Map1) -> check(Path, Map1), - update(Map1, Path, - fun(_Existing) -> Value end, - fun - (P, _Rest, {ok, _Existing}) -> error({badvalue, P}); - (P, _Rest, error) -> error({badkey, P}) - end - ). + update(Map1, Path, fun(_Existing) -> Value end, fun badvalue_and_badkey/3). % @doc Update a value in a `Map1' associated with `Path' by calling `Fun' on the % old value to get a new value. @@ -135,14 +124,32 @@ deep_update(Path, Value, Map1) -> % -spec deep_update_with(path(), fun((term()) -> term()), map()) -> map(). deep_update_with(Path, Fun, Map1) -> + deep_update_with_1(Path, Fun, Map1, fun badvalue_and_badkey/3). + +% @doc Update a value in a `Map1' associated with `Path' by calling `Fun' on the +% old value to get a new value. If `Path' is not present in `Map1' then `Init' +% will be associated with `Path'. +% +% The call can raise the following exceptions: +% +-spec deep_update_with(path(), fun((term()) -> term()), any(), map()) -> map(). +deep_update_with(Path, Fun, Init, Map1) -> + deep_update_with_1(Path, Fun, Map1, fun(P, Rest, Value) -> + badvalue_and_create(P, Rest, Value, Init) + end). + +deep_update_with_1(Path, Fun, Map1, Default) -> check(Path, Map1), check_fun(Fun, 1), update(Map1, Path, fun(Value) -> Fun(Value) end, - fun - (P, _Rest, {ok, _Existing}) -> error({badvalue, P}); - (P, _Rest, error) -> error({badkey, P}) - end + Default ). % @doc Removes the last existing key of `Path', and its associated value from @@ -287,3 +294,12 @@ remove(Map, [First, Second|Path]) when is_map(Map) -> error -> Map end. + +create(Path, Value) -> + lists:foldr(fun(Key, Acc) -> #{Key => Acc} end, Value, Path). + +badvalue_and_badkey(P, _Rest, {ok, _Existing}) -> error({badvalue, P}); +badvalue_and_badkey(P, _Rest, error) -> error({badkey, P}). + +badvalue_and_create(P, _Rest, {ok, _Existing}, _Init) -> error({badvalue, P}); +badvalue_and_create(_P, Rest, error, Init) -> create(Rest, Init). diff --git a/test/mapz_tests.erl b/test/mapz_tests.erl index a8479e9..25eaf3d 100644 --- a/test/mapz_tests.erl +++ b/test/mapz_tests.erl @@ -23,6 +23,7 @@ deep_put/3, deep_update/3, deep_update_with/3, + deep_update_with/4, deep_remove/2, deep_merge/1, deep_merge/2, @@ -84,14 +85,34 @@ deep_update_with_test_() -> ?_assertExit(badarg, deep_update_with([a], fun() -> foo end, ?STRUCT)) ] ++ error_(fun(P, M) -> deep_update_with(P, Incr, M) end)}. -error_(Function) -> +deep_update_with_init_test_() -> + Incr = fun(V) -> V + 1 end, {inparallel, [ + ?_assertEqual(#{a => 2}, deep_update_with([a], Incr, 0, #{a => 1})), + ?_assertEqual( + deep_put([a, a, a], 2, ?STRUCT), + deep_update_with([a, a, a], Incr, 0, ?STRUCT) + ), + ?_assertEqual( + deep_put([a, a, x, y], 0, ?STRUCT), + deep_update_with([a, a, x, y], Incr, 0, ?STRUCT) + ), + ?_assertExit(badarg, deep_update_with([a], x, 0, ?STRUCT)), + ?_assertExit(badarg, deep_update_with([a], fun() -> foo end, 0, ?STRUCT)) + ] ++ error_init_(fun(P, M) -> deep_update_with(P, Incr, 0, M) end)}. + +error_(Function) -> + [ + ?_assertError({badkey, [b]}, Function([b], #{a => 1})), + ?_assertError({badkey, [b, x]}, Function([b, x], ?STRUCT)) + ] ++ error_init_(Function). + +error_init_(Function) -> + [ ?_assertError({badmap, foobar}, Function([a], foobar)), ?_assertError({badpath, foobar}, Function(foobar, #{a => 1})), - ?_assertError({badkey, [b]}, Function([b], #{a => 1})), - ?_assertError({badkey, [b, x]}, Function([b, x], ?STRUCT)), ?_assertError({badvalue, [d]}, Function([d, x], ?STRUCT)) - ]}. + ]. deep_remove_test_() -> {inparallel, [