Skip to content

Commit

Permalink
Add revert_diff/3
Browse files Browse the repository at this point in the history
  • Loading branch information
Laszlo Toth authored and Laszlo Toth committed Jun 9, 2020
1 parent fac7e8f commit 3c63c7e
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 34 deletions.
103 changes: 82 additions & 21 deletions src/maps_utils.erl
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
diff/3,
update/4,
apply_diff/2,
apply_diff/3
apply_diff/3,
revert_diff/2
]).

%% exported for eunit only
-export([get_val/2,
sort_remove_operators/1]).
sort_remove_operators/1,
revert_op/1]).

-type op() :: move | rename | remove | add.
-type path_el() :: {Key :: term(), list | map}.
Expand Down Expand Up @@ -122,15 +124,19 @@ diff(From, To, Path, Log) when is_map(From), is_map(To) ->
end
end, {Log, NewPairs}, From),
lists:foldl(fun({K, V}, L) ->
[#{op => add, path => path([{K, map} | Path]), value => V}|L]
[#{op => add, path => path([{K, map} | Path]), value => V} | L]
end, Log2, NewPairs2);
diff(From, To, Path, Log) when is_list(From), is_list(To) ->
list_diff(From, To, Path, Log, 0);
diff(From, To, _Path, Log) when From =:= To -> Log;
diff(_From, To, Path, Log) ->
[#{op => replace, path => path(Path), value => To}|Log].
diff(From, To, _Path, Log) when From =:= To ->
Log;
diff(From, To, Path, Log) ->
[#{op => replace, path => path(Path), value => To, orig_value => From}|Log].


%% TODO handle move operator in revert
%% TODO check move operator in apply_diff

-spec diff(From, To, UpdateFun) -> Res when
From :: map(),
To :: map(),
Expand All @@ -140,6 +146,14 @@ diff(_From, To, Path, Log) ->
diff(From, To, Fun) ->
lists:reverse(diff(From, To, [], [], Fun)).

-spec diff(From, To, Path, Log, UpdateFun) -> Res when
From :: map(),
To :: map(),
Path :: path(),
Log :: list(),
UpdateFun :: fun((From :: map(), To :: map(),
Path :: path(), Log :: list()) -> map()),
Res :: list(diff_operator()).
diff(From, To, Path, Log, Fun) when is_map(From), is_map(To) ->
FromKeys = maps:keys(From),
NewPairs = maps:to_list(maps:without(FromKeys, To)),
Expand All @@ -165,8 +179,8 @@ diff(From, To, Path, Log, Fun) when is_integer(From), is_integer(To), To < From
Fun(From, To, path(Path), Log);
diff(From, To, Path, Log, Fun) when is_integer(From), is_integer(To), To > From ->
Fun(From, To, path(Path), Log);
diff(_From, To, Path, Log, _Fun) ->
[#{op => replace, path => path(Path), value => To}|Log].
diff(From, To, Path, Log, _Fun) ->
[#{op => replace, path => path(Path), value => To, orig_value => From} | Log].

-spec update(Key, Map, New, UpdateFun) -> Res when
Key :: term(),
Expand Down Expand Up @@ -200,6 +214,13 @@ apply_diff(Data, Diff0, Fun) ->
apply_op(Op, Acc, Fun)
end, Data, Diff).

-spec revert_diff(Data, Diff) -> Res when
Data :: term(),
Diff :: list(diff_operator()),
Res :: term().
revert_diff(Data, Diff) ->
RevertedDiff = reverted_diff(Diff),
apply_diff(Data, RevertedDiff).

%%==============================================================================
%% Exported for eunit
Expand Down Expand Up @@ -229,6 +250,38 @@ sort_remove_operators(Diff) ->
%% Internal functions
%%==============================================================================

-spec reverted_diff(Diff) -> Res when
Diff :: list(diff_operator()),
Res :: list(diff_operator()).
reverted_diff(Diff) ->
[revert_op(Op) || Op <- Diff].

-spec revert_op(Op) -> Res when
Op :: diff_operator(),
Res :: diff_operator().
revert_op(#{op := add} = Op) ->
Op#{op => remove};
revert_op(#{op := remove, orig_value := Orig, path := Path} = Op) ->
ConvertedPath = convert_revert_path(Path, []),
Op#{op => add, value => Orig, path => ConvertedPath};
revert_op(#{op := replace, orig_value := Orig, value := Value} = Op) ->
Op#{orig_value => Value, value => Orig};
revert_op(#{op := move, from := From, path := Path} = Op) ->
Op#{from => Path, path => From};
revert_op(Op) ->
Op.

-spec convert_revert_path(Path, Acc) -> Res when
Path :: path(),
Acc :: path(),
Res :: path().
convert_revert_path([{_, list}], Acc) ->
lists:reverse([{'-', list} | Acc]);
convert_revert_path([El], Acc) ->
lists:reverse([El | Acc]);
convert_revert_path([El | T], Acc) ->
convert_revert_path(T, [El | Acc]).

%%
%% Sort only filtered elements of the list. FilterFun is used to select
%% some matching elements. Those selected elements will be sorted. Order
Expand Down Expand Up @@ -336,12 +389,12 @@ get_new_val(Diff, _Data) ->
apply_op(_Data, _Op, [], NewVal) ->
NewVal;
apply_op(Data, remove, [{Idx, list}], _NewVal) when is_list(Data) ->
rm_list_el(Data, Idx);
rm_list_el(remove, Data, Idx);
apply_op(Data, remove, [{Key, map}], _NewVal) when is_map(Data) ->
maps:remove(Key, Data);
apply_op(Data, Op, [{Idx, list} | T], NewVal) when is_list(Data) ->
ListEl = nth_el(Data, Idx, NewVal),
RemainingEls = rm_list_el(Data, Idx),
RemainingEls = rm_list_el(Op, Data, Idx),
NewEl = apply_op(ListEl, Op, T, NewVal),
add_list_el(RemainingEls, Idx, NewEl);
apply_op(Data, Op, [{Key, map} | T], NewVal) when is_map(Data) ->
Expand Down Expand Up @@ -371,13 +424,17 @@ add_list_el(List, Ind0, El) ->
LastEls = string:substr(List, Ind),
FirstEls ++ [El] ++ LastEls.

-spec rm_list_el(List, Idx) -> Res when
-spec rm_list_el(Op, List, Idx) -> Res when
Op :: atom(),
List :: list(),
Idx :: index(),
Res :: list().
rm_list_el(List, '-') ->
rm_list_el(remove, List, '-') ->
%% remove last element of the list
string:substr(List, 1, length(List) -1);
rm_list_el(_Op, List, '-') ->
List;
rm_list_el(List, Ind0) ->
rm_list_el(_Op, List, Ind0) ->
Ind = Ind0 + 1,
[E ||
{E, I} <- lists:zip(List, lists:seq(1, length(List))), I =/= Ind].
Expand Down Expand Up @@ -414,8 +471,9 @@ def_value([], NewVal) ->
maybe_moved(K, FromV, Pairs, Path, L) ->
maybe_moved_(K, FromV, Pairs, Path, L, []).

maybe_moved_(K, _V, [], Path, Log, Acc) ->
{[#{op => remove, path => path([{K, map} | Path])}|Log], Acc};
maybe_moved_(K, V, [], Path, Log, Acc) ->
{[#{op => remove, path => path([{K, map} | Path]), orig_value => V} | Log],
Acc};
maybe_moved_(K, V, [{NewK, V}|Rest], Path, Log, Acc) ->
{[#{op => move, path => path([{NewK, map} | Path]),
from => path([{K, map} | Path])} | Log],
Expand All @@ -431,9 +489,11 @@ maybe_moved_(K, V, [Other|Rest], Path, Log, Acc) ->
Counter :: index(),
Res :: list(diff_operator()).
list_diff([From|RestF], [To|RestT], Path, Log, Cnt) ->
list_diff(RestF, RestT, Path, diff(From, To, [{Cnt, list}|Path], Log), Cnt+1);
list_diff([_|Rest], [], Path, Log, Cnt) ->
NewLog = [#{op => remove, path => path([{Cnt, list}|Path])}|Log],
list_diff(RestF, RestT, Path, diff(From, To, [{Cnt, list} | Path], Log),
Cnt+1);
list_diff([V | Rest], [], Path, Log, Cnt) ->
NewLog = [#{op => remove, path => path([{Cnt, list} | Path]),
orig_value => V} | Log],
list_diff(Rest, [], Path, NewLog, Cnt+1);
list_diff([], Rest, Path, Log, _Cnt) ->
lists:foldl(fun(V, L) ->
Expand All @@ -443,12 +503,13 @@ list_diff([], Rest, Path, Log, _Cnt) ->
list_diff([From|RestF], [To|RestT], Path, Log, Cnt, Fun) ->
list_diff(RestF, RestT, Path, diff(From, To, [{Cnt, list} | Path], Log, Fun),
Cnt + 1, Fun);
list_diff([_|Rest], [], Path, Log, Cnt, Fun) ->
NewLog = [#{op => remove, path => path([{Cnt, list} | Path])} | Log],
list_diff([V | Rest], [], Path, Log, Cnt, Fun) ->
NewLog = [#{op => remove, path => path([{Cnt, list} | Path]),
orig_value => V} | Log],
list_diff(Rest, [], Path, NewLog, Cnt + 1, Fun);
list_diff([], Rest, Path, Log, _Cnt, _Fun) ->
lists:foldl(fun(V, L) ->
[#{op => add, path => path([{'-', list} | Path]), value => V}|L]
[#{op => add, path => path([{'-', list} | Path]), value => V} | L]
end, Log, Rest).

-spec path(Path) -> Res when
Expand Down
49 changes: 36 additions & 13 deletions test/maps_utils_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ filtermap_test_() ->
diff_test_() ->
[?_test(?assertEqual([], maps_utils:diff(#{a => 1}, #{a => 1}))),
?_test(?assertEqual(
[#{op => replace, path => [{a, map}], value => 2}],
[#{op => replace, orig_value => 1, path => [{a, map}], value => 2}],
maps_utils:diff(#{a => 1}, #{a => 2}))),
?_test(?assertEqual(
[#{op => move, path => [{b, map}], from => [{a, map}]}],
Expand All @@ -60,17 +60,19 @@ diff_test_() ->
[#{op => add, path => [{a, map}], value => 1}],
maps_utils:diff(#{}, #{a => 1}))),
?_test(?assertEqual(
[#{op => remove, path => [{a, map}]}],
[#{op => remove, orig_value => 1, path => [{a, map}]}],
maps_utils:diff(#{a => 1}, #{}))),
?_test(?assertEqual(
[#{op => remove, path => [{a, map}]},
[#{op => remove, orig_value => 1, path => [{a, map}]},
#{op => add, path => [{c, map}], value => 3},
#{op => add, path => [{b, map}], value => 2}],
maps_utils:diff(#{a => 1}, #{b => 2, c => 3}))),
?_test(?assertEqual(
[#{op => move, path => [{e, map}], from => [{a, map}]},
#{op => replace, path => [{b, map}, {0, list}], value => 2},
#{op => replace, path => [{b, map}, {1, list}, {c, map}], value => 4},
#{op => replace, orig_value => 1,
path => [{b, map}, {0, list}], value => 2},
#{op => replace, orig_value => 3,
path => [{b, map}, {1, list}, {c, map}], value => 4},
#{op => add, path => [{b, map}, {'-', list}], value => 7},
#{op => add, path => [{k, map}], value => #{l => 1}}
],
Expand All @@ -80,9 +82,11 @@ diff_test_() ->
?_test(?assertEqual(
[
#{op => move, path => [{e, map}], from => [{a, map}]},
#{op => replace, path => [{b, map}, {0, list}], value => 2},
#{op => replace, path => [{b, map}, {1, list}, {c, map}], value => 4},
#{op => remove, path => [{b, map}, {2, list}]},
#{op => replace, orig_value => 1,
path => [{b, map}, {0, list}], value => 2},
#{op => replace, orig_value => 3,
path => [{b, map}, {1, list}, {c, map}], value => 4},
#{op => remove, orig_value => 7, path => [{b, map}, {2, list}]},
#{op => add, path => [{k, map}], value => #{l => 1}}
],
maps_utils:diff(#{a => 1, b => [1, #{c => 3}, 7], d => 4},
Expand Down Expand Up @@ -160,12 +164,12 @@ diff_nums_test_() ->
maps_utils:diff(#{}, #{a => 1}, Fun))),
?_test(?assertEqual(
[
#{ op => remove, path => [{a, map}]}
#{ op => remove, orig_value => 1, path => [{a, map}]}
],
maps_utils:diff(#{a => 1}, #{}, Fun))),
?_test(?assertEqual(
[
#{op => remove, path => [{a, map}]},
#{op => remove, orig_value => 1, path => [{a, map}]},
#{op => add, path => [{c, map}], value => 3},
#{op => add, path => [{b, map}], value => 2}
],
Expand All @@ -180,13 +184,12 @@ diff_nums_test_() ->
#{e => 1, b => [2, #{c => 4}, 7], d => 4,
k => #{l => 1}},
Fun))),

?_test(?assertEqual(
[#{fun_result => 2},
#{op => replace, path => [{b, map}], value => y},
#{op => replace, orig_value => x, path => [{b, map}], value => y},
#{fun_result => 3},
#{fun_result => <<"3">>},
#{op => remove, path => [{e, map}, {1, list}]}],
#{op => remove, orig_value => 2, path => [{e, map}, {1, list}]}],
maps_utils:diff(#{a => 1, b => x, c => 4, d => <<"4">>,
e => [1, 2]},
#{a => 2, b => y, c => 3, d => <<"3">>,
Expand Down Expand Up @@ -216,6 +219,21 @@ apply_fun_test() ->
#{op => incr, path => [{key1, map}], value => 3}],
ReverseCounterFun)).

revert_diff_test() ->
?assertEqual(#{op => incr}, maps_utils:revert_op(#{op => incr})),
chk_revert(#{k1 => 1, k2 => #{k21 => 21, k22 => 22}},
#{k1 => 1, k2 => #{k21 => 21, k22 => 23}}),
chk_revert(#{k1 => 1, k2 => 2},
#{k1 => 1, k3 => 2}),
chk_revert(#{k1 => #{k11 => 11, k12 => 12}},
#{k1 => #{k11 => 11, k13 => 12}}),
chk_revert([#{k1 => 1, k2 => 2}, #{k3 => 3, k4 => 4}],
[#{k1 => 1, k2 => 3}, #{k4 => 2}]),
chk_revert(#{k1 => [1, 2, 3]},
#{k1 => [1, 2, 3, 4]}),
chk_revert(#{k1 => [1, 2, 3]},
#{k1 => [1, 2]}).

%%==============================================================================
%% Performance tests
%%==============================================================================
Expand Down Expand Up @@ -365,6 +383,11 @@ chk_apply(D1, D2) ->
Current = maps_utils:apply_diff(D1, Diff),
?assertEqual(D2, Current).

chk_revert(D1, D2) ->
Diff = maps_utils:diff(D1, D2),
Current = maps_utils:revert_diff(D2, Diff),
?assertEqual(D1, Current).

perf_test(ExecFun, Count, MinExecPerSec, FunctionName) ->
Lst = lists:seq(1, Count),
{Time, _} = timer:tc(fun() ->
Expand Down
23 changes: 23 additions & 0 deletions test/prop_maps_utils_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,29 @@ prop_counters() ->
D2 =:= Actual)
end).

%%------------------------------------------------------------------------------
%% prop_counters
%%------------------------------------------------------------------------------
prop_revert_diff(doc) ->
"Test maps_utils:revert_diff/2. Diff = diff(Old, New), "
"?assertEqual(Old, revert_diff(New, Diff)) should be always true.";
prop_revert_diff(opts) ->
[{numtests, 500}].

prop_revert_diff() ->
?FORALL({D1, D2}, {map_like_data(), map_like_data()},
begin
Diff = maps_utils:diff(D1, D2),
Actual = maps_utils:revert_diff(D2, Diff),
?WHENFAIL(?ERROR("Failing test:\n"
"D1 = ~p,\n"
"D2 = ~p,\n"
"Diff = ~p\n"
"Actual value: ~p",
[D1, D2, Diff, Actual]),
D1 =:= Actual)
end).

%%==============================================================================
%% Generators
%%==============================================================================
Expand Down

0 comments on commit 3c63c7e

Please sign in to comment.