3

Im trying to count occurrences of a string during a for loop in a dictionary (baseX map). It seems that the contents of the dictionary are cleared after each iteration. Is there a way to keep the info throughout the loop?

declare variable $dict as map(*) := map:merge(());

for $x at $cnt in //a order by -$cnt

let $l := (if (map:contains($dict, $x/@line)) then (fn:number(map:get($dict, $x/@line))) else (0))

let $dict := map:put($dict, $x/@line, 1 + $l)

return ( 
     $dict,

     if ($x[@speaker="player.computer" or @speaker = "event.object"]) 
     then ( <add sel="(//{fn:name($x)}[@line='{$x/@line}'])[{fn:string(map:get($dict, $x/@line))}]" type="@hidechoices">false</add> )
     else ( <remove sel="(//{fn:name($x)}[@line='{$x/@line}'])[1]" />)

  )

so for this xml:

<a line="x" />
<a line="y" />
<a line="y" />
<a line="z" />

i should get something like this for the first:

{
  "x": 1
}

and this for the last iteration:

{
  "x": 1,
  "y": 2,
  "z": 1 
}

I have to construct some text out of this in the end, thats the last part of the output.

Right now i only get the current key/value pairs at each iteration, so $dict has only one entry throughout the whole execution, and $l is always 0.


Thankfully this worked:

for $x at $cnt in //a
let $dict := map:merge((
  for $y at $pos in //a
  let $line := $y/@line
  where $pos <= $cnt
  group by $line
  return map:entry($line, count($y))
))
return ( 
         $dict,   
         if ($x[@speaker="player.computer" or @speaker = "event.object"]) 
         then ( <add sel="(//{fn:name($x)}[@line='{$x/@line}'])[{fn:string(map:get($dict, $x/@line))}]" type="@hidechoices">false</add> )
         else ( <remove sel="(//{fn:name($x)}[@line='{$x/@line}'])[1]" />)
       )

For some reason could not use position() to limit the inner for, it returned all nodes right at first iteration. Thanks a lot for your help!

bitwise
  • 170
  • 12
  • 1
    I already indicated it in my answer, but to clarify for your question. Your question looks a lot like you have fallen into the classic trap of XY Problem, you can read more about it here: http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem – dirkk Nov 11 '15 at 22:01

1 Answers1

3

Your whole approach is flawed. XQuery is a functional language and the way you describe your problem and you wrote your query indicates that you not yet fully grasp the functional programming paradigm (which is fully understandable, as it is quite different from procedural programming). I would suggest you read into the topic in general.

Instead of iterating over all elements in a procedural way you can user a FLWOR expression with group by:

let $map := map:merge((
  for $x in //a
  let $line := $x/@line
  group by $line
  return map:entry($line, count($x))
))

This holds the result you expected. It iterates over the a elements and groups them together by their line attribute.

Another remark: Your output XML in the sel attribute looks suspiciously like the path to a certain element. Are you aware of the fn:path function, which gives you exactly that?


Based on your update from the comments you can calculate the map multiple times, but just up to the current position:

for $y at $pos in //a
let $map := map:merge((
  for $x in //a[position() <= $pos]
  let $line := $x/@line
  group by $line
  return map:entry($line, count($x))
))
return $map
dirkk
  • 6,160
  • 5
  • 33
  • 51
  • Plus one. But note that other XQuery processors have non-standard map implementations that *are* mutable. – wst Nov 11 '15 at 22:06
  • Thanks for your time! I thought about that, but my problem is that i need a counter for each iteration, the output should look like this for the two "y" nodes: `` `` So i don't only need the end result, but both 1st and 2nd encounter with the counter being increased. Regarding the sel attribute - its a diffgram, that either adds an attribute to a node or deletes it (the node). – bitwise Nov 11 '15 at 22:09
  • 1
    @wst You can name MarkLogic ;) Such a "map" is also possible with BaseX (e.g. using the Java bindings), however why should one use a side-effect enabled structure if you can have the same result side effect free and with the benefit of possible runtime optimizations? – dirkk Nov 11 '15 at 22:12
  • To make it clear, the output should go like this: Iteration 1: { "x": 1 } Iteration 2: { "x": 1 "y": 1 } Iteration 3: { "x": 1 "y": 2 } Or in text: – bitwise Nov 11 '15 at 22:15
  • Please revise your question to reflect the actual problem X you are trying to solve instead of the (based on your last comment seemingly incorrect) solution Y, this way we can be faster in helping you. However, please check my update. – dirkk Nov 11 '15 at 22:15
  • @dirkk Ha, I agree, just wanted OP to be aware. Sometimes it is nice to have real map though! – wst Nov 11 '15 at 22:42
  • Should i edit the title to count occurrences or similar? Im trying this now: `count(preceding::a[@line=$x/@line])` but it always returns 0 – bitwise Nov 11 '15 at 22:42
  • Thanks a lot, updated title. Weird that `fn:position()` did not work here to limit the inner for, maybe a BaseX speciality. – bitwise Nov 12 '15 at 00:14
  • No, I guess the position context wasn't correct in the way it was applied in your actual code. I am sure, because I testes this on the latest BaseX release and we (I am a member of the BaseX team) strive to be as standard compliant as possible. – dirkk Nov 12 '15 at 00:46
  • It was weird, i even added an if to add entries as 0:0 in case `$pos > $cnt` - while i had the limitation `[position() <= $cnt]` in the inner loop. Right at first iteration i had the 0:0 entry. I can post the repro if it's of any interest, though i checked it many times and it seemed correct, it can still be a newbie mistake on my end. Used 3.1.1 by the way. Either way thanks a lot for taking your time! And BaseX spared me countless hours already <3 – bitwise Nov 12 '15 at 13:07