5
$\begingroup$
asso = <|"x" -> {
    <|"a" -> 5, "b" -> "y", "c" -> {5, 6, 7}|>,
    <|"a" -> 6, "b" -> "z", "c" -> {}|>}
|>

I want to find "x" value's element where "c" value is {} and modify its "b" value by applying Framed to it.

asso // Query["x", Select[#c === {} &], Framed@*"b"]
{Framed["z"]}

But what I want to either return a full asso together with the result or to modify it in place.

I can rewrite the Query:

asso // Query[
  {"x"},
  MapAt[
    MapAt[Framed, #, "b"] &,
    #,
    Position[#, _?(Function[asso, asso["c"] === {}])]] &
  ]
<|"x" -> {
 <|"a" -> 5, "b" -> "y", "c" -> {5, 6, 7}|>, 
 <|"a" -> 6, "b" -> Framed["z"], "c" -> {}|>}
|>

But it will quickly become unreadable for more complicated cases.

What is a generic approach to this problem?

When I need to approach it I just write a Module:

bSel = MapAt[Framed, #, "b"] &;
cPos = Position[#, _?(#c === {} &)] &
cSel = MapAt[bSel, #, cPos[#]] &
cSel /@ asso

to make it more maintainable. It requires more effort though.

$\endgroup$
7
$\begingroup$

One way is to use a deferred subquery:

asso // Query[{"x"}, All, Query[If[#c === {}, {"b" -> Framed}, All]][#]&]

result screenshot

The inner subqueries are computed from the value of the "c" key of each inner association. This is reasonably pleasant notationally, but it comes at the cost of multiple query compilations. Compilation results are cached, so this overhead should not be significant unless the computed subqueries are truly different each time (unlike the present simple example which uses only two distinct subquery forms).

Deeper Associations

As noted in a comment, the use of Query in this way becomes unworkable for deeper assocations. Consider:

asso2 =
  <| "x" ->
       { <| "a" -> 5
          , "b" -> "y"
          , "c" -> {5, 6, 7}
          |>
       , <| "a" -> 6
          , "b" -> <| "x" ->
                        { <|"a" -> 5, "b" -> "y", "c" -> {5, 6, 7}|>
                        , <|"a" -> 6, "b" -> "z", "c" -> {}|>
                        }
                    |>
          , "c" -> {}
          |>
       }
   |>

What if we want to apply Frame to the innermost "b" provided both the outer and inner values for "c" were {}?

The deferred subquery approach "works", but is an unreadable mess:

asso2 //
  Query[{"x"},All,Query[If[#c==={},Query[{"b"->Query[{"x"},All,Query[If[#c==={},{"b"->Framed},All]][#]&]}][#]&,All]][#]&]

result of unreadable mess

For this transformation, ReplacePart can do (a little?) better:

ReplacePart[
  asso2
, ii:{"x", i_, "b", "x", j_, "b"}
    /; asso2[["x", i, "c"]] === {}
    && asso2[["x", i, "b", "x", j, "c"]] === {}
    :> RuleCondition@Extract[asso2, ii, Framed]
]

result of not-quite-as-unreadable mess

... if we ignore the weird-looking RuleCondition@Extract[...] at the end. That cryptic recipe is necessary to work-around the held evaluations within associations. If we replace RuleCondition@... with the simpler Framed[asso2[[ii]]] then we get problematic output:

problematic output

Beware that even the RuleCondition trick has limitations -- it will not fully evaluate compound function expressions.

$\endgroup$
  • $\begingroup$ I feel your pain. I am not aware of a general solution to this problem using just Query. See my update for an alternative using ReplacePart. $\endgroup$ – WReach Jan 17 '18 at 21:51
  • $\begingroup$ @WReach thanks for the update. To be clear, I don't need Query at the end, it is only to show the query to be performed. $\endgroup$ – Kuba Jan 17 '18 at 22:30
3
$\begingroup$

You may use a recursive deferred sub-query.

drill[row_] :=
 Query[
   {"x"},
   All,
   Which[
     #["c"] != {}, #
     , Head@#["b"] === Association, Query[{"b" -> drill}]@#
     , True, Query[{"b" -> Framed}]@#
     ] &
   ]@row 

Then

drill@asso
drill@asso2 (* from WReach post *)

return as expected.

This is particular to this particular structure. However, it can be generalised by adding commands for each level of the nested association should the structure and/or conditions change down the levels.

ClearAll[seekReplace]
seekReplace[row_, {}] := row
seekReplace[row_, cmd_?(VectorQ[#, AssociationQ] &)] :=
 Query[
   {"x"},
   All,
   Which[
     First[cmd]["ExitSeek"]@#, #
     , First[cmd]["Seek"]@#, 
     Query[{First[cmd]["SeekKey"] -> (seekReplace[#, Rest@cmd] &)}]@#
     , True, 
     Query[{First[cmd]["ReplaceKey"] -> First[cmd]["Replace"]}]@#
     ] &
   ]@row 

Here the commands are the same since the structure and conditions are the same at each level.

seekCommands = <|
   "ExitSeek" -> (#["c"] != {} &), 
   "Seek" -> (Head@#["b"] === Association &), "SeekKey" -> "b", 
   "Replace" -> Framed, "ReplaceKey" -> "b"|>;
seekCommands = ConstantArray[seekCommands, 2]

Then the following return as expected.

seekReplace[asso, seekCommands]
seekReplace[asso2, seekCommands]

Hope this helps.

$\endgroup$
0
$\begingroup$
asso = <|"x" -> {<|"a" -> 5, "b" -> "y", "c" -> {5, 6, 7}|>, <|
     "a" -> 6, "b" -> "z", "c" -> {}|>}|>

targetpos = 
 Most /@ Select[Position[asso, {}], MatchQ[#, {__, Key["c"]}] &]

MapAt[Query[{"b" -> Framed}], asso, targetpos]

Blockquote

$\endgroup$
  • $\begingroup$ Isn't Riffle[Key /@ searchKeys, __Integer] too optimistic in general? :) $\endgroup$ – Kuba Jan 18 '18 at 9:54
  • $\begingroup$ I'm not sure yet, need to test, but isn't Riffle[Key /@ searchKeys, __Integer] likely to find false matches? Will see, p.s. you are assuming that we know the value of the target, but that is not the case in my example. $\endgroup$ – Kuba Jan 18 '18 at 10:21
  • $\begingroup$ Sorry for a delay. There is a filter based on {} value in my example but at the end I apply Framed to a particular "b" key, which has "z" value in this case but it is now known up front. $\endgroup$ – Kuba Jan 18 '18 at 21:25
  • $\begingroup$ @Kuba Sorry, i completely misread your post. See my edit. Not very elegant but short and does it job. $\endgroup$ – SquareOne Jan 18 '18 at 22:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.