سؤال

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.

هل كانت مفيدة؟

المحلول 3

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

نصائح أخرى

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 ()

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
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top