Skip to content

Commit 15edb66

Browse files
committed
Add merge to provide a way to create moves.
1 parent d719706 commit 15edb66

File tree

4 files changed

+132
-11
lines changed

4 files changed

+132
-11
lines changed

src/Json/Diff.elm

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ This uses a relatively expensive, but not perfect weight function to decide what
2424
the JSON to a string and looks at the length), but it should give pretty reasonable results most of the time. If you
2525
want something more efficient or accurate you can use [`diffWithCustomWeight`](Json.Diff#diffWithCustomWeight).
2626
27-
Note that this is a simple diff that never produces moves or copies, and as such the patches will be inefficient if
28-
those are common operations in your case.
27+
This won't produce moves in the patch. If you use [`invertibleDiff`](Json.Diff#invertibleDiff) and
28+
[`Invertible.merge`](Json.Patch.Invertible#merge) you can merge adds and removes of the same value into moves.
29+
30+
Note that this diff doesn't search for duplicated values and so will never produces copies, and as such the patches
31+
will not be concise if that is a common operation in your case.
2932
3033
-}
3134
diff : Json.Value -> Json.Value -> JsonP.Patch

src/Json/Patch/Invertible.elm

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Json.Patch.Invertible exposing
22
( Patch, Operation(..)
3-
, invert, apply
3+
, apply, invert, merge
44
, toPatch, toMinimalPatch, fromPatch
55
)
66

@@ -14,7 +14,7 @@ module Json.Patch.Invertible exposing
1414
1515
# Operations
1616
17-
@docs invert, apply
17+
@docs apply, invert, merge
1818
1919
2020
# Compatibility
@@ -26,6 +26,7 @@ module Json.Patch.Invertible exposing
2626
import Json.Decode as Json
2727
import Json.Patch as Json
2828
import Json.Pointer as Json
29+
import Set
2930

3031

3132
{-| A list of invertible [`Operation`](Json.Patch.Invertible#Operation])s that are performed one after another.
@@ -75,6 +76,78 @@ apply =
7576
toMinimalPatch >> Json.apply
7677

7778

79+
{-| Take a patch and merge any `Remove` and `Add` operations that have the same values into single `Move` options.
80+
81+
The resulting patch should always do the same thing as it did before, but may be smaller.
82+
83+
-}
84+
merge : Patch -> Patch
85+
merge patch =
86+
let
87+
filterAdd op =
88+
case op of
89+
Add pointer value ->
90+
Just ( pointer, value )
91+
92+
_ ->
93+
Nothing
94+
95+
filterRemove op =
96+
case op of
97+
Remove pointer value ->
98+
Just ( pointer, value )
99+
100+
_ ->
101+
Nothing
102+
103+
adds =
104+
patch |> List.filterMap filterAdd
105+
106+
removes =
107+
patch |> List.filterMap filterRemove
108+
109+
pairs =
110+
adds |> List.concatMap (\a -> removes |> List.map (\r -> ( a, r )))
111+
112+
isMove ( ( _, av ), ( _, rv ) ) =
113+
av == rv && rv == av
114+
115+
moves =
116+
pairs |> List.filter isMove
117+
118+
filterUsed ( ( ap, av ), ( rp, rv ) ) ( filtered, usedAdds, usedRemoves ) =
119+
if (usedAdds |> Set.member ap) || (usedRemoves |> Set.member rp) then
120+
( filtered, usedAdds, usedRemoves )
121+
122+
else
123+
( ( ( ap, av ), ( rp, rv ) ) :: filtered, usedAdds |> Set.insert ap, usedRemoves |> Set.insert rp )
124+
125+
( filteredMoves, _, _ ) =
126+
moves |> List.foldr filterUsed ( [], Set.empty, Set.empty )
127+
128+
replace op =
129+
case op of
130+
Add pointer value ->
131+
if filteredMoves |> List.any (\( ( p, _ ), _ ) -> p == pointer) then
132+
Nothing
133+
134+
else
135+
Add pointer value |> Just
136+
137+
Remove pointer value ->
138+
case filteredMoves |> List.filter (\( _, ( p, _ ) ) -> p == pointer) |> List.head of
139+
Just ( ( ap, _ ), ( rp, _ ) ) ->
140+
Move rp ap |> Just
141+
142+
Nothing ->
143+
Remove pointer value |> Just
144+
145+
_ ->
146+
op |> Just
147+
in
148+
patch |> List.filterMap replace
149+
150+
78151
{-| Create a normal patch from an invertible one.
79152
80153
Note that this patch would still be invertible, you are just losing the type guarantee of that (i.e: you could run

tests/Cases.elm

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,21 @@ cases =
344344
"a": {"a": { "b" : { "c" : { "1" : 1, "2": 2, "3": 3 } } } },
345345
"b": {"a": { "b" : { "c" : { "x" : 1, "y": 2, "z": 3 } } } },
346346
"patch": [{"op": "replace", "path": "/a/b/c", "value": { "x" : 1, "y": 2, "z": 3 }}]
347+
},
348+
{
349+
"description": "add and remove the same value",
350+
"a": {"a": 1, "b": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
351+
"b": {"d": 1, "b": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
352+
"patch": [{"op": "remove", "path": "/a"}, {"op": "add", "path": "/d", "value": 1}]
353+
},
354+
{
355+
"description": "add and remove the same value multiple times",
356+
"a": {"a": 1, "b": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "c": 1},
357+
"b": {"d": 1, "b": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "e": 1},
358+
"patch": [
359+
{"op": "remove", "path": "/a"}, {"op": "remove", "path": "/c"},
360+
{"op": "add", "path": "/d", "value": 1}, {"op": "add", "path": "/e", "value": 1}
361+
]
347362
}
348363
]
349364
"""

tests/Invertible.elm

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module Invertible exposing (suite)
1+
module Invertible exposing (invertSuite, mergeSuite)
22

33
import Cases exposing (TestCase, suiteUsingTestCases)
44
import Expect exposing (Expectation)
@@ -9,14 +9,14 @@ import Json.Patch.Invertible as Invertable
99
import Test exposing (..)
1010

1111

12-
suite : Test
13-
suite =
14-
suiteUsingTestCases "Invertible correctly handles " toTest
12+
invertSuite : Test
13+
invertSuite =
14+
suiteUsingTestCases "Patch, invert, patch has the same result for: " invertTest
1515

1616

17-
toTest : TestCase -> Test
18-
toTest { a, b } =
19-
test ("Patch and back works | a: " ++ (a |> JsonE.encode 0) ++ " | b: " ++ (b |> JsonE.encode 0))
17+
invertTest : TestCase -> Test
18+
invertTest { a, b } =
19+
test ("a: " ++ (a |> JsonE.encode 0) ++ " | b: " ++ (b |> JsonE.encode 0))
2020
(\_ ->
2121
let
2222
diff =
@@ -34,6 +34,36 @@ toTest { a, b } =
3434
)
3535

3636

37+
mergeSuite : Test
38+
mergeSuite =
39+
suiteUsingTestCases "Patch and Merge Patch have the same result for:" mergeTest
40+
41+
42+
mergeTest : TestCase -> Test
43+
mergeTest { a, b } =
44+
test ("a: " ++ (a |> JsonE.encode 0) ++ " | b: " ++ (b |> JsonE.encode 0))
45+
(\_ ->
46+
let
47+
diff =
48+
Diff.invertibleDiff a b
49+
50+
mergedDiff =
51+
diff |> Invertable.merge
52+
in
53+
case a |> Invertable.apply diff of
54+
Ok patched ->
55+
case a |> Invertable.apply mergedDiff of
56+
Ok mergePatched ->
57+
expectJsonEqual patched mergePatched
58+
59+
Err err ->
60+
Expect.fail err
61+
62+
Err err ->
63+
Expect.fail err
64+
)
65+
66+
3767
expectJsonEqual : Json.Value -> Json.Value -> Expectation
3868
expectJsonEqual a =
3969
Expect.all

0 commit comments

Comments
 (0)