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

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.

Related

XQuery: map to array of sequences

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.

Ordering by a sequence of values in XQuery

I've got some XML data that takes this form:
<products>
<product version="1.2.3"/>
<product version="1.10.0"/>
<product version="2.1.6"/>
</products>
...And so on. I want to order these in XQuery by version number. Trouble is, if I just do order by $thing/#version, it does a lexicographic comparison that puts 1.10.0 before 1.2.3, which is wrong.
What I really want to do is something like:
order by tokenize($thing/#version, '\.') ! number(.)
Unfortunately this doesn't work because XQuery doesn't let you use an entire sequence as an ordering key. How can I get something like this?
A solution that doesn't rely on all the version numbers having the same number of dots would be preferable, but I'll take what I can get.
All you can do is normalize the version numbers so you can apply lexical ordering.
Determine maximum string length in a version step
Pad it with 0's (or space if you prefer, but you will have to change the code for this)
Tokenize each version, pad each version step, rejoin them
Compare based on padded version
I didn't clean up that code and pulled two functions from functx, but it works and should be fine for embedding as needed. The code is also able to deal with single-letters, if necessary you could replace all occurences of "alpha", ... for example by "a", ...
declare namespace functx = "http://www.functx.com";
declare function functx:repeat-string
( $stringToRepeat as xs:string? ,
$count as xs:integer ) as xs:string {
string-join((for $i in 1 to $count return $stringToRepeat),
'')
} ;
declare function functx:pad-integer-to-length
( $integerToPad as xs:anyAtomicType? ,
$length as xs:integer ) as xs:string {
if ($length < string-length(string($integerToPad)))
then error(xs:QName('functx:Integer_Longer_Than_Length'))
else concat
(functx:repeat-string(
'0',$length - string-length(string($integerToPad))),
string($integerToPad))
} ;
declare function local:version-compare($a as xs:string, $max-length as xs:integer)
as xs:string*
{
string-join(tokenize($a, '\.') ! functx:pad-integer-to-length(., $max-length), '.')
};
let $bs := ("1.42", "1.5", "1", "1.42.1", "1.43", "2")
let $max-length := max(
for $b in $bs
return tokenize($b, '\.') ! string-length(.)
)
for $b in $bs
let $normalized := local:version-compare($b, $max-length)
order by $normalized
return $b
Returns:
1 1.5 1.42 1.42.1 1.43 2
Order by doesn't accept a sequence, but you can explicitly tokenize the versions and add them to the order by, separated by commas (note the exclusion of parens).
let $products :=
<products>
<product version="1.2.3"/>
<product version="1.10.0"/>
<product version="2.1.6"/>
</products>
for $p in $products/product
let $toks := tokenize($p/#version, '\.')
let $main := xs:integer($toks[1])
let $point := xs:integer($toks[2])
let $sub := xs:integer($toks[3])
order by $main, $point, $sub
return $p
Update: for a variable number of tokens, you could make the order by more robust:
order by
if (count($toks) gt 0) then $main else (),
if (count($toks) gt 1) then $point else (),
if (count($toks) gt 2) then $sub else ()
I did something similar to Jens's answer:
let $products := //product
let $max-length := max($products/#version ! string-length(.))
for $product in $products
order by string-join(
for $part in tokenize($product/#version, '\.')
return string-join((
for $_ in 1 to $max-length - string-length($part) return ' ',
$part)))
return $product
Here's a version that will handle an arbitrary number of segments, as long as they're numeric and all version strings have the same number of segments. It also assumes no one component ever exceeds 999.
This simply combines each numeric segment into a single big number and sorts by that.
declare function local:version-order ($version as xs:string) as xs:double
{
fn:sum (
let $toks := fn:tokenize ($version, "\.")
let $count := fn:count ($toks)
for $tok at $idx in $toks
return xs:double ($tok) * math:pow (1000, ($count - $idx))
)
};
let $products :=
<products>
<product version="1.10.0"/>
<product version="2.1.6"/>
<product version="1.2.3"/>
</products>
for $p in $products/product
order by local:version-order ($p/#version)
return $p

assigning an operator to a variable in xquery

is there a way to assign a numeric operator to a variable in Xquery?
I have to perform an arithmetic expression on a given pair of values depending upon a node tag.
I've managed to do this but its resulted in a lot of duplicate code. I'd like to simplify the query so that instead of:
Function for Add
Repeated if code - this calls out to other functions but is still repeated
$value1 + $value2
Function for Minus
Repeated if code
$value1 - $value2
etc for multiply, div etc
I'd like to set up a function and send a variable to it, something similar to this:
$value1 $operator $value2
Is there a simple way to do this in xquery?
thank you for your help.
If your query processor supports XQuery 3.0, you can use function items for that:
declare function local:foo($operator, $x, $y) {
let $result := $operator($x, $y)
return 2 * $result
};
local:foo(...) can then be called like this:
let $plus := function($a, $b) { $a + $b },
$mult := function($a, $b) { $a * $b }
return (
local:foo($plus, 1, 2),
local:foo($mult, 3, 4)
)
Why don't you use a simple if-else construct? E.g.
if (repeated code says you should add) then
$value1 + $value2
else
$value1 - $value2
You could also simple put the repeated code in another function instead of copying the code.

Updating counter in XQuery

I want to create a counter in xquery. My initial attempt looked like the following:
let $count := 0
for $prod in $collection
let $count := $count + 1
return
<counter>{$count }</counter>
Expected result:
<counter>1</counter>
<counter>2</counter>
<counter>3</counter>
Actual result:
<counter>1</counter>
<counter>1</counter>
<counter>1</counter>
The $count variable either failing to update or being reset. Why can't I reassign an existing variable? What would be a better way to get the desired result?
Try using 'at':
for $d at $p in $collection
return
element counter { $p }
This will give you the position of each '$d'. If you want to use this together with the order by clause, this won't work since the position is based on the initial order, not on the sort result. To overcome this, just save the sorted result of the FLWOR expression in a variable, and use the at clause in a second FLWOR that just iterates over the first, sorted result.
let $sortResult := for $item in $collection
order by $item/id
return $item
for $sortItem at $position in $sortResult
return <item position="{$position}"> ... </item>
As #Ranon said, all XQuery values are immutable, so you can't update a variable. But if you you really need an updateable number (shouldn't be too often), you can use recursion:
declare function local:loop($seq, $count) {
if(empty($seq)) then ()
else
let $prod := $seq[1],
$count := $count + 1
return (
<count>{ $count }</count>,
local:loop($seq[position() > 1], $count)
)
};
local:loop($collection, 0)
This behaves exactly as you intended with your example.
In XQuery 3.0 a more general version of this function is even defined in the standard library: fn:fold-right($f, $zero, $seq)
That said, in your example you should definitely use at $count as shown by #tohuwawohu.
Immutable variables
XQuery is a functional programming language, which involves amongst others immutable variables, so you cannot change the value of a variable. On the other hand, a powerful collection of functions is available to you, which solves lots of daily programming problems.
let $count := 0
for $prod in $collection]
let $count := $count + 1
return
<counter>{$count }</counter>
let $count in line 1 defines this variable in all scope, which are all following lines in this case. let $count in line 3 defines a new $count which is 0+1, valid in all following lines within this code block - which isn't defined. So you indeed increment $count three times by one, but discard the result immediatly.
BaseX' query info shows the optimized version of this query which is
for $prod in $collection
return element { "counter" } { 1 }
The solution
To get the total number of elements in $collection, you can just use
return count($collection)
For a list of XQuery functions, you could have a look at the XQuery part of functx which contains both a list of XQuery functions and also some other helpful functions which can be included as a module.
Specific to MarkLogic you can also use xdmp:set. But this breaks functional language assumptions, so use it conservatively.
http://docs.marklogic.com/5.0doc/docapp.xqy#display.xqy?fname=http://pubs/5.0doc/apidoc/ExsltBuiltins.xml&category=Extension&function=xdmp:set
For an example of xdmp:set in real-world code, the search parser https://github.com/mblakele/xqysp/blob/master/src/xqysp.xqy might be helpful.
All the solution above are valid but I would like to mention that you can use the XQuery Scripting extension to set variable values:
variable $count := 0;
for $prod in (1 to 10)
return {
$count := $count + 1;
<counter>{$count}</counter>
}
You can try this example live at http://www.zorba-xquery.com/html/demo#twh+3sJfRpHhZR8pHhOdsmqOTvQ=
Use xdmp:set instead of the below query
let $count := 0
for $prod in (1 to 4)
return ( xdmp:set($count,number($count+1)) ,<counter>{$count }</counter>
I think you are looking for something like:
XQUERY:
for $x in (1 to 10)
return
<counter>{$x}</counter>
OUTPUT:
<counter>1</counter>
<counter>2</counter>
<counter>3</counter>
<counter>4</counter>
<counter>5</counter>
<counter>6</counter>
<counter>7</counter>
<counter>8</counter>
<counter>9</counter>
<counter>10</counter>

Get the most repeated element in a sequence with XQuery

I've got a sequence of values. They can all be equal... or not. So with XQuery I want to get the most frequent item in the sequence.
let $counter := 0, $index1 := 0
for $value in $sequence
if (count(index-of($value, $sequence)))
then
{
$counter := count(index-of($value, $sequence)) $index1 := index-of($value)
} else {}
I can't make this work, so I suppose I'm doing something wrong.
Thanks in advance for any help you could give me.
Use:
for $maxFreq in
max(for $val in distinct-values($sequence)
return count(index-of($sequence, $val))
)
return
distinct-values($sequence)[count(index-of($sequence, .)) eq $maxFreq]
Update, Dec. 2015:
This is notably shorter, though may not be too-efficient:
$pSeq[index-of($pSeq,.)[max(for $item in $pSeq return count(index-of($pSeq,$item)))]]
The shortest expression can be constructed for XPath 3.1:
And even shorter and copyable -- using a one-character name:
$s[index-of($s,.)[max($s ! count(index-of($s, .)))]]
You are approaching this problem from too much of an imperative standpoint.
In XQuery you can set the values of variables, but you can never change them.
The correct way to do iterative-type algorithms is with a recursive function:
declare funciton local:most($sequence, $index, $value, $count)
{
let $current=$sequence[$index]
return
if (empty($current))
then $value
else
let $current-count = count(index-of($current, $sequence))
return
if ($current-count > $count)
then local:most($sequence, $index+1, $current, $current-count)
else local:most($sequence, $index+1, $value, $count)
}
but a better way of approaching the problem is by describing the problem in a non-iterative way. In this case of all the distinct values in your sequence you want the one that appears maximum number of times of any distinct value.
The previous sentance translated into XQuery is
let $max-count := max(for $value1 in distinct-values($sequence)
return count(index-of($sequence, $value1)))
for $value2 in distinct-values($sequence)
where (count(index-of($sequence, $value2)) = $max-count
return $value2

Resources