XQuery: map to array of sequences - xquery

I have the following map:
let $input := map { 'a-key': 'a-value', 'b-key': ['b-value-1', 'b-value-2'] }
(the length of the b-key array can vary, or be absent; a-key can be present or absent)
I need to create the following array:
[ ('a', 'b', 'b'), 'a-value', 'b-value-1', 'b-value-2' ]
The number of bs in the first sequence should correspond to the number of b-values.
I've tried just about every combination of iterating/map:for-each, etc., and the array either ends up with too much nesting or completely flat...
(n.b. the array is to be passed to a function -- not mine!-- using fn:apply, so needs to be in this form)

It seems hard to build an array containing a sequence that is constructed dynamically, I think you first need to construct the sequence into a variable and then use the square bracket array constructor [$seq] to construct the array with the sequence as the single item. Then you can array:join the other values:
let $input := map { 'a-key': 'a-value', 'b-key': ['b-value-1', 'b-value-2'] }
let $seq := ($input?a-key!'a', (1 to array:size($input?b-key))!'b')
return
array:join(([$seq], array {$input?a-key, $input?b-key?* }))
https://xqueryfiddle.liberty-development.net/nbUY4ku/2

If you are comfortable with the functional-programming side of XQuery, you can create the whole output in two nested "loops" (i.e. folds), one over the keys and the other one over those values that are arrays:
(: the input :)
let $input := map { 'a-key': 'a-value', 'b-key': ['b-value-1', 'b-value-2'] }
(: utility function that adds a single key/value pair to the output array :)
let $update-arr :=
function($arr, $k, $v) {
array:append(array:put($arr, 1, ($arr(1), $k)), $v)
}
(: nested folds over keys and values :)
return fold-left(map:keys($input), [()], function($arr, $key) {
let $k := substring-before($key, '-key')
let $val := $input($key)
return typeswitch($val)
case array(*)
return array:fold-left($val, $arr, function($arr2, $v) {
$update-arr($arr2, $k, $v)
})
default
return $update-arr($arr, $k, $val)
})
You can even abbreviate the array(*) case as return array:fold-left($val, $arr, $update-arr(?, $k, ?)) if you want.
The result is [("a", "b", "b"), "a-value", "b-value-1", "b-value-2"] as expected.

Related

XQuery: create array of maps

I don't understand how to dynamically create an array of maps in Xquery which in the end should be serialised as JSON:
I can create a simple/static array of maps like this
let $array := [map{"id": "4711"}, map{"id": "4712"}]
I can add another map like this:
let $array := array:append($array, map {"id": "4713"})
using the following
return
serialize($array,
<output:serialization-parameters>
<output:method>json</output:method>
</output:serialization-parameters>)
results in [{"id":"4711"},{"id":"4712"},{"id":"4713"}] which is perfect JSON for me.
But I have an arbitrary number of maps to add to this array based on a sequence, like this:
let $mylist := ("4714", "4715")
What I'd like to have as result is this:
[{"id":"4711"},{"id":"4712"},{"id":"4713"},{"id":"4714"},{"id":"475"}]
I'm trying to achieve this by:
let $array := array:append($array,
for $n in $mylist
return map {"id": $n}
)
But this returns as result:
[{"id":"4711"},{"id":"4712"},{"id":"4713"},[{"id":"4714"},{"id":"4715"}]]
So, obviously the for loop creates another array and appends that to the existing one. How do I avoid that?
I think the following should do it:
let $mylist := ("4714", "4715")
return array{ $mylist ! map{'id': .} }
It's a bit unfortunate in my view that array{} is special-purpose syntax rather than just being a function.
You can also do it with
array:join( $mylist ! [map{'id': .}] )

XQuery - wrong indexes in substring after reverse-string function use

Im trying to implement base64 coding in a very simple way. In my approach (lets for a second put away whether its appropriate or not) I need to reverse strings and then concate them. After that this concated string is used in substring function. Strings are joined properly but when I use substring basex seems to lose it.
Funny thing is substring works for well for all indexes starting at 8. So substring($string, 1, 8) and higher gives correct output. But everything below that is messed up. Starting with one disappeared number: substring($string, 1, 7 (and below) ) results in 6 length string.
Moreover substring can start only with 1st or 0 index. Anything greater results in empty return.
declare variable $array := [];
declare function bs:encode
( $input as xs:string ) {
bs:integer-to-binary(string-to-codepoints($input), "", $array)
} ;
declare function bs:integer-to-binary
( $input as xs:integer*, $string as xs:string, $array as array(xs:string) ) {
let $strings :=
for $i in $input
return
if ($i != 0)
then if ($i mod 2 = 0)
then bs:integer-to-binary(xs:integer($i div 2), concat($string, 0), $array)
else bs:integer-to-binary(xs:integer($i div 2), concat($string, 1), $array)
else if ($i <= 0)
then array:append($array, $string)
return bs:check-if-eight($strings)
} ;
declare function bs:check-if-eight
( $strings as item()+ ) {
let $fullBinary :=
for $string in $strings
return if (string-length($string) < 8)
then bs:check-if-eight(concat($string, 0))
else $string (: add as private below :)
return bs:concat-strings($fullBinary)
} ;
declare function bs:concat-strings
( $strings as item()+ ) {
let $firstStringToConcat := functx:reverse-string($strings[position() = 1])
let $secondStringToConcat := functx:reverse-string($strings[position() = 2])
let $thirdStringToConcat := functx:reverse-string($strings[position() = 3])
let $concat :=
concat
($firstStringToConcat,
$secondStringToConcat,
$thirdStringToConcat)
(: this returns correct string of binary value for Cat word :)
return bs:divide-into-six($concat)
} ;
declare function bs:divide-into-six
( $binaryString as xs:string) {
let $sixBitString := substring($binaryString, 1, 6)
(: this should return 010000 instead i get 000100 which is not even in $binaryString at all :)
return $sixBitString
} ;
bs:encode("Cat")
I expect first six letters from string (010000) instead I get some random sequence I guess (00100). The whole module is meant to encode strings into base64 format but for now (the part i uploaded) should just throw first six bits for 'C'
Alright so I figured it out I guess.
First of all in function concat-strings I changed concat to fn:string-join. It allowed me to pass as an argument symbol that separates joined strings.
declare function bs:concat-strings ( $strings as item()+ ) {
let $firstStringToConcat := xs:string(functx:reverse-string($strings[position() = 1]))
let $secondStringToConcat := xs:string(functx:reverse-string($strings[position() = 2]))
let $thirdStringToConcat := xs:string(functx:reverse-string($strings[position() = 3]))
let $concat :=
****fn:string-join(****
($firstStringToConcat,
$secondStringToConcat,
$thirdStringToConcat),****'X'****)
return bs:divide-into-six($concat) } ;
I saw that my input looked like this:
XXXXXXXX01000011XXXXXXXXXXXXXXXXX01100001XXXXXXXXXXXXXXXXX01110100XXXXXXXX
Obviously it had to looping somewhere without clear for loop and as I novice to Xquery i must have been missed that. And indeed. I found it in check-if-eight function:
> declare function bs:check-if-eight ( $strings as item()+ ) {
> **let $fullBinary :=**
> for $string in $strings
> return if (string-length($string) < 8)
> then bs:check-if-eight(concat($string, 0))
> else $string (: add as private below :)
> **return bs:concat-strings($fullBinary)** } ;
Despite being above FOR keyword, $fullBinary variable was in a loop and produced empty spaces(?) and it was clearly shown when i used X as a separator.
DISCLAIMER: I thought about this before and used functx:trim but for some reason it doesnt work like I expected. So it might not for you too if having similar issue.
At this point it was clear that let $fullBinary cannot be bided in FLWR statement at least can't trigger concat-strings function. I changed it and now it produces only string and now im trying to figure out new sequence of running whole module but I think the main problem here is solved.

I need some help on an XQuery sequence merge that preserves order

I am working on a function to merge a set of sequences that will preserve the order of all of the sequences as best as possible. Doing a distinct-values($sequences) on all of the sequences does not preserve the order.
I have the following MarkLogic XQuery code:
xquery version "1.0-ml";
declare function local:map-sequence($map, $list as xs:string*) {
let $count := fn:count($list) - 1
return for $idx in (1 to $count)
return if (map:contains($map, $list[$idx]))
then map:put($map, $list[$idx], fn:distinct-values((map:get($map, $list[$idx]), $list[$idx + 1])))
else map:put($map, $list[$idx], $list[$idx + 1])
};
declare function local:first($map) {
let $all-children := for $key in map:keys($map) return map:get($map, $key)
return distinct-values(map:keys($map)[not(.=$all-children)])
};
declare function local:next($map, $key as xs:string) {
if (map:contains($map, $key))
then if (fn:count(map:get($map, $key)) eq 1)
then map:get($map, $key)
else
let $children := map:get($map, $key)
return
for $next in $children
let $others := $children[fn:not(.=$next)]
let $descedents := local:descendents($map, $next)
return if ($descedents[.=$others])
then $next
else ()
else ()
};
declare function local:descendents($map, $key as xs:string) {
for $child in map:get($map, $key)
return ($child, local:descendents($map, $child))
};
declare function local:sequence($map, $key as xs:string) {
let $next := local:next($map, $key)
return if (fn:count($next) gt 1)
then
for $choice in $next
return $choice
else if (fn:count($next) eq 1)
then ($next, local:sequence($map, $next))
else ()
};
let $map := map:map()
let $seq1 := local:map-sequence($map, ('fred', 'barney', 'pebbles'))
let $seq2 := local:map-sequence($map, ('fred', 'wilma', 'betty', 'pebbles'))
let $seq3 := local:map-sequence($map, ('barney', 'wilma', 'betty'))
let $first := local:first($map)
return ($map,
for $top in $first
return ($top, local:sequence($map, $top))
)
it returns
{"barney":["pebbles", "wilma"], "fred":["barney", "wilma"], "wilma":"betty", "betty":"pebbles"}
fred
barney
wilma
betty
pebbles
It still needs work. If you add:
let $seq4 := local:map-sequence($map, ('fred', 'bambam'))
bambam does not show up. I am still working on it, but if others have suggestions, then I would like to hear them.
Thanks,
Loren
As far as I understand your problem, each sequence represents a hierarchy of values, so from the sequence ("foo", "bar", "baz") we can follow that "foo" < "bar", "foo" < "baz" and "bar" < "baz" should preferably hold in the resulting ordering.
From your expected output it seems that you want the values to be sorted from the one with the smallest number of (transitive) predecessors ("fred" in your case) to that with the most ones ("pebbles" with four predecessors: ("barney", "fred", "betty", "wilma")).
I do not have access to MarkLogic and its proprietary maps, so I'll use standard XQuery 3.0 maps instead. The underlying algorithms should be easy to translate.
As a first step we build a map of all immediate predecessors of each unique value found in at least one of the input sequences. Because XQuery 3.0 maps cannot be modified in-place, we use fn:fold-left(...) to build one up incrementally. Note also that even the first element of each list is added to the map with an empty sequence of predecessors.
declare function local:add-preds($map0, $list as xs:string*) {
fn:fold-left(
1 to fn:count($list),
$map0,
function($map, $idx) {
map:put(
$map,
$list[$idx],
(: add the current predecessor to the list :)
fn:distinct-values((map:get($map, $list[$idx]), $list[$idx - 1]))
)
}
)
};
Next we need the transitive closure of this map of predecessors, so we need to gather all values that can be reached from a given key by a chain of predecessors. We can do this using a simple depth-first search:
declare function local:transitive($preds) {
map:merge(
for $key in map:keys($preds)
return map:entry($key, local:all-predecessors($preds, $key, $key)[not(. = $key)])
)
};
declare function local:all-predecessors($succ, $key, $seen0) {
fold-left(
map:get($succ, $key),
$seen0,
function($seen, $next) {
if($next = $seen) then $seen
else local:all-predecessors($succ, $next, ($seen, $next))
}
)
};
This transforms your example initial predecessor map
map {
"bambam": "fred",
"pebbles": ("barney", "betty"),
"fred": (),
"wilma": ("fred", "barney"),
"barney": "fred",
"betty": "wilma"
}
and transforms it into
map {
"bambam": "fred",
"pebbles": ("barney", "fred", "betty", "wilma"),
"fred": (),
"wilma": ("fred", "barney"),
"barney": "fred",
"betty": ("wilma", "fred", "barney")
}
With that map your sorting now becomes very easy: Just take all keys in the map, order them by the number of their predecessors, and output them:
let $map0 := map{}
let $map1 := local:add-preds($map0, ('fred', 'barney', 'pebbles'))
let $map2 := local:add-preds($map1, ('fred', 'wilma', 'betty', 'pebbles'))
let $map3 := local:add-preds($map2, ('barney', 'wilma', 'betty'))
let $map4 := local:add-preds($map3, ('fred', 'bambam'))
let $trans := local:transitive($map4)
for $key in map:keys($trans)
order by count(map:get($trans, $key))
return $key
This returns your desired result: "fred", "bambam", "barney", "wilma", "betty", "pebbles"

Sorting multiple maps in marklogic 8

This is more of an XQuery than MarkLogic. I have three map:map and each map has key-value pair of "id" and score. I would like to sort all the distinct ids based on the score from each maps.
For eg:
map1 : 1:2048, 5:2000
map2 : 2:5000, 1:1000, 4:3000
map3 : 6:100, 7:5000, 2:2000
In the above example, each map is id:score for key value (did not know how to represent here :))..
I want the sorted list of id from three maps based on score..
Is there a good way or better way of doing the sorting, or do I have to union the keys of the map and iterate the sequence of keys and sort them ?
This seems like a great use case for folding. Its part of Xquery 3.0 spec.
Folding can go through a sequence of items and gets the result for each item as it goes through. In this example $combinedMaps is the result of the last call and $mapToMerge is the item in the sequence it is currently going through.
Here an example of what you would want to do.
declare function local:sortMaps(
$newMap as map:map,
$mapA as map:map,
$mapB as map:map
) as map:map {
let $build :=
for $key in map:keys($mapA)
let $otherMapValue :=
(map:get($mapB, $key), 0)[1]
let $value := map:get($mapA, $key)
return
if ($value gt $otherMapValue) then (
map:put($newMap, $key, $value)
) else (
map:put($newMap, $key, $otherMapValue)
)
return $newMap
};
let $map1 :=
map:new((
map:entry("1",2048),
map:entry("5",2000)
))
let $map2 :=
map:new((
map:entry("2",5000),
map:entry("1",1000),
map:entry("4",3000)
))
let $map3 :=
map:new((
map:entry("6",100),
map:entry("7",5000),
map:entry("2",2000)
))
let $maps := ($map1, $map2, $map3)
return
fn:fold-left(
function($combinedMaps, $mapToMerge) {
let $newMap := map:map()
let $newMap := local:sortMaps($newMap, $combinedMaps, $mapToMerge)
let $newMap := local:sortMaps($newMap, $mapToMerge, $combinedMaps)
return $newMap
},
$maps[1],
$maps
)

Build dictionary in XQuery for loop and count occurrences of similar nodes

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!
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

Resources