How to organically merge nested associations?
Initial data:
peopleFacts = <|
alice -> <|age -> 29, shoeSize -> 7|>,
bob -> <|age -> 27, sex -> male, hair -> <|Color -> RGBColor[1, 0, 0]|>
|>
|>;
Here is a version of RecurAssocMerge
reduced to a single definition.
MergeNested = If[MatchQ[#, {__Association}], Merge[#, #0], Last[#]] &
MergeNested @ {peopleFacts, <|bob -> <|hair -> <|length -> 120|>|>|>}
<| alice -> <| age -> 29, shoeSize -> 7|>, bob -> <| age -> 27, sex -> male, hair -> <|Color -> RGBColor[1, 0, 0], length -> 120|> |> |>
Special case of 2-level deep association
Merge[{
peopleFacts,
<|bob -> <|hairColor -> 1|>|>
},
Association
]
"Tidy" approach to write NestedMerge
:
RecurAssocMerge[a : {__Association}] := Merge[a, RecurAssocMerge];
RecurAssocMerge[a_] := Last[a];
adding key to deep level association:
RecurAssocMerge[ {peopleFacts, <|bob -> <|hair -> <|length -> 120|>|>|>} ]
<|alice -> <|age -> 29, shoeSize -> 7|>, bob -> <|age -> 27, sex -> male, hair -> <| Color -> RGBColor[1, 0, 0], length -> 120 |> |> |>
entirely new tree
RecurAssocMerge[ {peopleFacts, <|kuba -> <|hair -> <|length -> 120|>|>|>} ]
<| alice -> <|age -> 29, shoeSize -> 7|>, bob -> <|age -> 27, sex -> male, hair -> <|Color -> RGBColor[1, 0, 0]|> |>, kuba -> <|hair -> <|length -> 120|>|> |>
Section added by Jess Riedel:
Specialize to single new entry
RecurAssocMerge
defined above is a general method for merging nested Associations
. We can define an abbreviation for the special case when we are adding only a single new entry.
RecurAssocMerge[ini_Association, path_List, value_] := RecurAssocMerge[{
ini, Fold[<|#2 -> #|> &, value, Reverse@path]
}]
Then we can just do
RecurAssocMerge[peopleFacts, {bob, hair, length}, 120]
<|alice -> <|age -> 29, shoeSize -> 7|>, bob -> <|age -> 27, sex -> male, hair -> <| Color -> RGBColor[1, 0, 0], length -> 120 |> |> |>
Notes
If you want to modify peopleFacts
the peopleFacts = Merge...
is needed of course.
Update
Created an upsert
function to update/insert new keys and values into a nested association structure. It automatically inserts nested associations where they do not exists and does not need to be assigned back to the original association. It updates existing keys when they are found.
ClearAll[upsert]
Attributes[upsert] = {HoldFirst};
upsert[dat_?AssociationQ, key_, value__] :=
If[First@Dimensions@{value} == 1,
dat[key] = value,
(
If[KeyExistsQ[dat, key] == False, dat[key] = <||>];
upsert[dat[key], First@{value}, Sequence @@ Rest@{value}]
)
]
Can use upsert
with as many nested levels as needed.
peopleFacts = <|"alice" -> <|"age" -> 29, "shoeSize" -> 7|>,
"bob" -> <|"age" -> 27, "sex" -> "male"|>|>;
Insert "steve"
and association "haircolor"
key/value.
upsert[peopleFacts, "steve", "haircolor", "Red"];
peopleFacts
(* <|"alice" -> <|"age" -> 29, "shoeSize" -> 7|>,
"bob" -> <|"age" -> 27, "sex" -> "male"|>,
"steve" -> <|"haircolor" -> "Red"|>|> *)
Insert "tim"
, association "music"
key/value, and nested association "rock"
key/value.
upsert[peopleFacts, "tim", "music", "rock", "jimmy"];
peopleFacts
(* <|"alice" -> <|"age" -> 29, "shoeSize" -> 7|>,
"bob" -> <|"age" -> 27, "sex" -> "male"|>,
"steve" -> <|"haircolor" -> "Red"|>,
"tim" -> <|"music" -> <|"rock" -> "jimmy"|>|>|> *)
Update "alice"
"age"
.
upsert[peopleFacts, "alice", "age", 25];
peopleFacts
(* <|"alice" -> <|"age" -> 25, "shoeSize" -> 7|>,
"bob" -> <|"age" -> 27, "sex" -> "male"|>,
"steve" -> <|"haircolor" -> "Red"|>,
"tim" -> <|"music" -> <|"rock" -> "lenny"|>|>|> *)
Original Post
Each time there is a new key that has an association as its value you must initialise it as an association. Then you can use the feature of Association
that creates a key when a value is assigned to a non-existing key.
peopleFacts = <|"alice" -> <|"age" -> 29, "shoeSize" -> 7|>, "bob" -> <|"age" -> 27, "sex" -> "male"|>|>;
peopleFacts["steve"] = <||>;
peopleFacts
(* <|alice -> <|age -> 29, shoeSize -> 7|>,
bob -> <|age -> 27, sex -> male|>, steve -> <||>|> *)
peopleFacts["steve"]["hairColor"] = "Red";
peopleFacts
(* <|alice -> <|age -> 29, shoeSize -> 7|>,
bob -> <|age -> 27, sex -> male|>, steve -> <|hairColor -> Red|>|> *)
peopleFacts["bob"]["age"] = 22;
peopleFacts
(* <|alice -> <|age -> 29, shoeSize -> 7|>,
bob -> <|age -> 22, sex -> male|>, steve -> <|hairColor -> Red|>|> *)
peopleFacts["steve"]["major"] = "Physics";
peopleFacts
(* <|alice -> <|age -> 29, shoeSize -> 7|>,
bob -> <|age -> 22, sex -> male|>,
steve -> <|hairColor -> "Red", major -> "Physics"|>|> *)
Hope this helps.
Try this, we're using it on a daily basis
Nest[Merge,f,n]
To your starting data, slightly modified (strings vs symbols):
peopleFacts = <|"alice" -> <|"age" -> 29, "shoeSize" -> 7|>,
"bob" -> <|"age" -> 27, "sex" -> "male"|>|>;
And new facts:
newFacts = <|
"steve" -> <|"hairColor" -> "red", "major" -> "physics"|>,
"bob" -> <|"age" -> 22|>|>
Update semantics for replacing existing values: (1) add newFacts last, (2) apply Last
eg:
{peopleFacts, newFacts} // Nest[Merge, Last, 2]
<|"alice" -> <|"age" -> 29, "shoeSize" -> 7|>, "bob" -> <|"age" -> 22, "sex" -> "male"|>, "steve" -> <|"hairColor" -> "red", "major" -> "physics"|>|>
Keep in mind Merge
does not yet have full complement of options like JoinAcross
eg inner/outer/left/right and will not impute missing keys, so often KeyIntersection
is required.
Also will admit for some types of ragged hierarchies, there's no current easy way to merge all relevant branches automatically (an All
level spec). In other words, knowledge of the schema is required.