Fix in excel/math.xq.

Fix in excel/math.xq.

(:
 : Copyright 2006-2009 The FLWOR Foundation.
 :
 : Licensed under the Apache License, Version 2.0 (the "License");
 : you may not use this file except in compliance with the License.
 : You may obtain a copy of the License at
 :
 : http://www.apache.org/licenses/LICENSE-2.0
 :
 : Unless required by applicable law or agreed to in writing, software
 : distributed under the License is distributed on an "AS IS" BASIS,
 : WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 : See the License for the specific language governing permissions and
 : limitations under the License.
 :)

(:~
 : This is a library module offering the same set of functions
 : defined by Microsoft Excel.
 :
 : @see Excel Documentation: Math Functions
 :
 : @spec XQuery Specification: January 2007
 : @author Daniel Turcanu
 : @version 1.0
 :)
module namespace  excel = "http://www.zorba-xquery.com/modules/excel/math" ;

(:~
 : Use excel-err module functions for throwing errors.
 :)
import module namespace excel-err="http://www.zorba-xquery.com/modules/excel/errors";

(:~
 : Checks if the xs:anyAtomicType argument is actually a numeric type
 : or can be converted to numeric.
 :
 : @param $value Parameter to be checked.
 : @return true if the value can be casted to numeric.
 :)
declare function excel:is-a-number($value as xs:anyAtomicType) as xs:boolean
{
  fn:string(fn:number($value)) ne 'NaN'
};

(:~
 : Cast the xs:anyAtomicType to a numeric type.
 : If the value is already of a numeric type then nothing is changed.
 : Otherwise the value is casted to the numeric type that is most appropriate.
 :
 : @param $number The parameter can be a number, string, boolean value.
 : @return The casted value.
 : @error XQP0021(errValue) if the value cannot be casted to numeric type.
 :)
declare function excel:cast-as-numeric($number as xs:anyAtomicType) as xs:anyAtomicType
{
  typeswitch ($number)
    case xs:double return $number
    case xs:decimal return $number
    case xs:double return $number
    case xs:float return $number
    default return
      if ($number castable as xs:integer) then
        xs:integer($number)
      else if ($number castable as xs:decimal) then
        xs:decimal($number)
      else if ($number castable as xs:double) then
        xs:double($number)
      else
        fn:error($excel-err:errValue, "Provided value is not a number", $number)
};

(: ---------------- Excel Math functions ----------------------- :)

(:~
 : Compute the abs of a numeric value.
 : The value can also be a string and it will be casted to the appropriate numeric first.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052089781033.aspx
 : @param $arg The parameter can be a number, string, boolean value.
 : @return The abs value as a numeric type.
 : @error XQP0021(errValue) if arg cannot be casted to numeric type.
 :)
declare function excel:abs($arg as xs:anyAtomicType) as xs:anyAtomicType
{
  fn:abs(excel:cast-as-numeric($arg))
};

(:~
 : Returns number rounded up, away from zero, to the nearest multiple of significance.
 : Significance must have the same sign as number.
 : Number and significance must be of a numeric type or castable to numeric.
 : Significance must not be zero.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052090071033.aspx
 : @param $number The value you want to round.
 : @param $significance The multiple to which you want to round.
 : @return The rounded value.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 : @error XQP0021(errNum) if significance is zero or it doesn't have the same sign as number.
 :)
declare function excel:ceiling(
  $number        as xs:anyAtomicType,
  $significance  as xs:anyAtomicType) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  let $sig := excel:cast-as-numeric($significance)
  return
    if ($sig eq 0) then
      fn:error($excel-err:errNum, "Ceiling function does not accept significance 0")
    else if ($num * $sig ge 0) then
	    fn:ceiling($num div $sig) * $significance
    else
      fn:error($excel-err:errNum, "Ceiling function: both arguments must have the same sign")
};

(:~
 : Returns number rounded up to the nearest even integer.
 : Regardless of the sign of number, a value is rounded up when adjusted away from zero.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052090801033.aspx
 : @param $number The value to round.
 : @return The rounded value casted as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:even($number as xs:anyAtomicType) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  return
    if ($num = 0) then
      0
    else
      let $intnum := excel:ceiling($num, excel:sign($num))
      return
        if ($intnum mod 2 ne 0) then
          if ($intnum gt 0) then
            $intnum + 1
          else
            $intnum - 1
        else
          $intnum
};

(:~
 : Helper function for computing factorial.
 : This function should not be used outside this module.
 : This recursive function computes: number * fact(number-1)
 :
 : @param $intnum A positive integer.
 : @return The factorial of intnum.
:)
declare function excel:fact-integer($intnum as xs:integer) as xs:integer
{
  if ($intnum = 1) then
    1
  else
    $intnum * excel:fact-integer($intnum - 1)
};

(:~
 : Returns the factorial of a number.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052090841033.aspx
 : @param $number The nonnegative number you want the factorial of.
 :        If number is not an integer, it is truncated.
 : @return Returns the factorial of a number. The factorial of a number is equal to 1*2*3*...* number.
 : @error XQP0021(errNum) if the number is smaller than zero
:)
declare function excel:fact($number as xs:anyAtomicType) as xs:integer
{
  let $num := excel:cast-as-numeric($number) return
    if ($num eq 0) then
      1
    else
      if ($num lt 0) then
        fn:error($excel-err:errNum, "Fact function does not accept numbers less than zero")
      else
        excel:fact-integer(xs:integer($num))
};

(:~
 : Rounds number down, toward zero, to the nearest multiple of significance.
 : Significance must have the same sign as number.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052090941033.aspx
 : @param $number The value you want to round. The value is casted to numeric.
 : @param $significance The multiple to which you want to round.
 : @return The rounded value as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 : @error XQP0021(errNum) if significance is zero or it doesn't have the same sign as number.
:)
declare function excel:floor(
  $number as xs:anyAtomicType,
  $significance as xs:anyAtomicType) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  let $sig := excel:cast-as-numeric($significance)
  return
    if ($sig eq 0) then
      fn:error($excel-err:errNum, "Floor function does not accept significance 0")
    else if ($num * $sig ge 0) then
      fn:floor($num div $sig) * $sig
    else
      fn:error($excel-err:errNum, "Floor function: both arguments must have the same sign")
};

(:~
 : Rounds a number down to the nearest integer.
 : Positive numbers are rounded toward zero, negative numbers are rounded away from zero.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052091421033.aspx
 : @param $number The value to be rounded.
 : @return The rounded integer.
 : @error XQP0021(errValue) if parameter cannot be casted to numeric type
:)
declare function excel:int($number as xs:anyAtomicType) as xs:integer
{
  xs:integer(fn:floor(excel:cast-as-numeric($number)))
};

(:~
 : Returns the remainder after number is divided by divisor.
 : The result has the same sign as divisor.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052091821033.aspx
 : @param $number The number for which you want to find the remainder.
 : @param $divisor The number by which you want to divide number.
 :        This cannot be zero.
 : @return The remainder from division as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 : @error XQP0021(errDiv0) if divisor is zero after casting to numeric.
 :)
declare function excel:mod(
  $number as xs:anyAtomicType,
  $divisor as xs:anyAtomicType) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  let $div := excel:cast-as-numeric($divisor) return
    if ($div eq 0) then
      fn:error($excel-err:errDiv0, "Mod operator: divide by 0")
    else
      let $result := $num mod $div
      return
        if ($result * $div lt 0) then
          -$result
        else
          $result
};

(:~
 : Returns number rounded up to the nearest odd integer, away from zero.
 :
 : @see  http://office.microsoft.com/en-us/excel/HP052092031033.aspx
 : @param $number The value to round.
 : @return The odd integer.
 : @error XQP0021(errValue) if parameter cannot be casted to numeric type.
 :)
declare function excel:odd($number as xs:anyAtomicType) as xs:integer
{
  let $num := excel:cast-as-numeric($number) return
  if ($num eq 0) then
    1
  else
    let $intnum := excel:ceiling($num, excel:sign($num))
    return
      if ($intnum mod 2 eq 0) then
        if ($intnum ge 0) then
          ($intnum + 1) cast as xs:integer
        else
          ($intnum - 1) cast as xs:integer
      else
        $intnum cast as xs:integer
};

(:~
 : Return the value of PI as decimal with 15 digits.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092141033.aspx
 : @return The value of PI with 15 digits.
 :)
declare function excel:pi() as xs:decimal
{
  3.14159265358979
};

(:~
 : Returns the result of a number raised to a power.
 : The result is computed through successive multiplications.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092171033.aspx
 : @param $number The base number.
 : @param $power The exponent as integer (cannot be floating point like in Excel).
 : @return The result as numeric type.
 : @error XQP0021(errValue) if parameter cannot be casted to numeric type.
 : @error XQP0021(errValue) if power is smaller than zero.
 :)
declare function excel:power(
  $number as xs:anyAtomicType,
  $power as xs:integer) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  return
    if ($power lt 0) then
      fn:error($excel-err:errValue, "Power function: power must be greater or equal than zero")
    else if ($power eq 0) then
      1
    else if ($power = 1) then
      $num
    else
      let $retval := excel:power($num, $power idiv 2)
      return
        $retval * $retval * excel:power($num, $power mod 2)
 };

(:~
 : Helper function for product.
 : This function should not be used outside this module.
 : Multiplies all numbers in the sequence.
 :
 : @param $numbers The list of arguments to be casted to numeric and multiplied.
 : @return The multiplication result as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:product-internal($numbers as xs:anyAtomicType*) as xs:anyAtomicType
{
  if (fn:empty($numbers)) then
    1
  else
    let $x := excel:cast-as-numeric($numbers[1])
    return
      $x * excel:product-internal(fn:subsequence($numbers, 2))
};

(:~
 : Multiplies all the numbers given as arguments and returns the product.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092231033.aspx
 : @param $numbers The sequence of arguments convertable to numeric types.
 :        The sequence can be of any length.
 : @return The multiplication result as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:product($numbers as xs:anyAtomicType*) as xs:anyAtomicType
{
  if (fn:empty($numbers)) then
    0
  else
    excel:product-internal($numbers)
};

(:~
 : Returns the integer portion of a division.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092271033.aspx
 : @param $numerator The divident.
 : @param $denominator The divisor. It cannot be zero.
 : @return The result value as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 : @error XQP0021(div0) if denominator casted as numeric type has value zero.
 :)
declare function excel:quotient(
  $numerator   as xs:anyAtomicType,
  $denominator as xs:anyAtomicType) as xs:integer
{
  let $numer := excel:cast-as-numeric($numerator)
  let $denom := excel:cast-as-numeric($denominator)
  return
    if ($denom eq 0) then
      fn:error($excel-err:errDiv0, "Quotient function: divide by 0")
    else
      xs:integer($numer div $denom)
};

(:~
 : Rounds a number to a specified number of digits.
 : If precision is greater than 0 (zero), then number is rounded
 : to the specified number of decimal places.
 : If num_digits is 0, then number is rounded to the nearest integer.
 : If num_digits is less than 0, then number is rounded to the left of the decimal point.
 : The 0.5 is rounded away from zero.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092391033.aspx
 : @param $number The number to round, castable to a numeric type.
 : @param $precision The number of decimal places to keep.
 : @return The rounded number as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:round(
  $number as xs:anyAtomicType,
  $precision as xs:integer) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  return
    if ($precision ge 0) then
      let $exp_prec := excel:power(10, $precision)
      return
        if ($num ge 0) then
          fn:floor($num * $exp_prec + 0.5) div $exp_prec
        else
          -fn:floor(-$num * $exp_prec + 0.5) div $exp_prec
    else
      let $exp_prec := excel:power(10, -$precision)
      return
        if ($num ge 0) then
          fn:floor($num div $exp_prec + 0.5) * $exp_prec
        else
          -fn:floor(-$num div $exp_prec + 0.5) * $exp_prec
};

(:~
 : Rounds a number down, toward zero.
 : If num_digits is greater than 0 (zero), then number is rounded down
 : to the specified number of decimal places.
 : If num_digits is 0, then number is rounded down to the nearest integer.
 : If num_digits is less than 0, then number is rounded down to the left of the decimal point.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092411033.aspx
 : @param $number The number to round, castable to numeric type.
 : @param $precision The number of decimal places to keep.
 : @return the truncated number toward zero, as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:rounddown(
  $number     as xs:anyAtomicType,
  $precision  as xs:integer) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  return
    if ($precision ge 0) then
      let $exp_prec := excel:power(10, $precision)
      return
        if ($num ge 0) then
          fn:floor($num * $exp_prec) div $exp_prec
        else
          -fn:floor(-$num * $exp_prec) div $exp_prec
    else
      let $exp_prec := excel:power(10, -$precision)
      return
        if ($num ge 0) then
          fn:floor($num div $exp_prec) * $exp_prec
        else
          -fn:floor(-$num div $exp_prec) * $exp_prec
};

(:~
 : Rounds a number up, away from 0 (zero).
 : If num_digits is greater than 0 (zero), then number is rounded down
 : to the specified number of decimal places.
 : If num_digits is 0, then number is rounded down to the nearest integer.
 : If num_digits is less than 0, then number is rounded down to the left of the decimal point.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092421033.aspx
 : @param $number The number to round, castable to numeric type.
 : @param $precision The number of decimal places to keep.
 : @return The truncated number away from zero, as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:roundup(
  $number     as xs:anyAtomicType,
  $precision  as xs:integer) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  return
    if ($precision ge 0) then
      let $exp_prec := excel:power(10, $precision)
      return
        if ($num ge 0) then
           fn:ceiling($num * $exp_prec) div $exp_prec
        else
          -fn:ceiling(-$num * $exp_prec) div $exp_prec
    else
      let $exp_prec := excel:power(10, -$precision)
      return
        if ($num ge 0) then
          fn:ceiling($num div $exp_prec) * $exp_prec
        else
          -fn:ceiling(-$num div $exp_prec) * $exp_prec
};

(:~
 : Determines the sign of a number.
 : Returns 1 if the number is positive, zero (0) if the number is 0,
 : and -1 if the number is negative.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092551033.aspx
 : @param $number The argument castable to numeric type.
 : @return The sign as (-1, 0, 1).
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:sign($number as xs:anyAtomicType) as xs:integer
{
  let $num := excel:cast-as-numeric($number)
  return
    if ($num eq 0) then
      0
    else if ($num gt 0) then
      1
    else
      -1
 };

(:~
 : Adds all the numbers in the sequence.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092901033.aspx
 : @param $numbers The sequence of arguments castable to numeric types.
 :        The sequence can be of any length.
 : @return The sum as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:sum($numbers as xs:anyAtomicType*) as xs:anyAtomicType
{
  if (fn:empty($numbers)) then
    0
  else
    let $x := excel:cast-as-numeric($numbers[1])
    return
      $x + excel:sum(fn:subsequence($numbers,2))
 };

(:~
 : Truncates a number to an integer by removing the fractional part of the number.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052093241033.aspx
 : @param $number The argument castable to numeric type.
 : @return The integer value.
 : @error XQP0021(errValue) if parameter cannot be casted to numeric type.
 :)
declare function excel:trunc($number as xs:anyAtomicType ) as xs:integer
{
  xs:integer(excel:cast-as-numeric($number))
};

(:~
 : Truncates a number down to precision.
 : This behaves exactly like rounddown.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052093241033.aspx
 : @param $number The argument castable to numeric type.
 : @param $precision The number of decimal places to keep .
 : @return The integer value.
 : @error XQP0021(errValue) if parameter cannot be casted to numeric type.
 :)
declare function excel:trunc(
  $number as xs:anyAtomicType,
  $precision as xs:integer) as xs:anyAtomicType
{
  excel:rounddown(excel:cast-as-numeric($number), $precision)
};

(:~
 : Helper function.
 : This function should not be used outside this module.
 : Sorts a sequence of numbers or arguments castable to numeric.
 : It first casts all arguments to numeric and then sorts ascending.
 :
 : @param $numbers The sequence of arguments castable to numeric.
 : @return The sorted sequence as numeric types.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:sort-numbers($numbers as xs:anyAtomicType*) as xs:anyAtomicType*
{
  let $sorted-numbers :=
    (
      for $number in $numbers
      let $num := excel:cast-as-numeric($number)
      order by $num
      return $num
    )
  return $sorted-numbers
};

(: priority 1 :)

(:~
 : Converts radians into degrees.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052090561033.aspx
 : @param $radian The value in radians.
 : @return The value in degrees 0 .. 360 or 0 .. -360.
 :)
declare function excel:degrees($radian as xs:double) as xs:integer
{
  xs:integer(($radian div excel:pi() * 180) mod 360)
};

(:~
 : Returns the double factorial of a number.
 : Computes the double factorial of n as n(n-2)(n-4)...
 :
 : @see http://office.microsoft.com/en-us/excel/HP052090851033.aspx
 : @param $number The positive integer value.
 : @return The result as integer.
 : @error XQP0021(errNum) if the number is negative.
 :)
declare function excel:factdouble($number as xs:integer) as xs:integer
{
  if ($number lt 0) then
    fn:error($excel-err:errNum, "Factdouble function: number should be greater than zero or equal")
  else if ($number eq 1) then
    1
  else if ($number eq 2) then
    2
  else
    $number * excel:factdouble($number - 2)
};

(:~
 : Helper function for computing GCD.
 : This function should not be used outside this module.
 : It calculates the minimum value from a sequence of positive integers,
 : not taking into account the zero value.
 :
 : @param $numbers The sequence of positive integers.
 : @return The minimum value. If the sequence contains only zero values, then zero is returned.
 :)
declare function excel:min-without-zero($numbers as xs:integer+) as xs:integer
{
  if (fn:count($numbers) eq 1) then
    $numbers[1]
  else
    let $min-other := excel:min-without-zero(fn:subsequence($numbers, 2))
    return
      if ($numbers[1] eq 0) then
        $min-other
      else if ($min-other eq 0) then
        $numbers[1]
      else if ($numbers[1] lt $min-other) then
        $numbers[1]
      else
        $min-other
};

(:~
 : Helper function for computing GCD.
 : This function should not be used outside this module.
 : Checks if all integer numbers from a sequence divide exactly to a divident.
 :
 : @param $numbers The positive integers.
 : @param $divident The divident to be tried.
 : @return true if the numbers divide exactly.
:)
declare function excel:try-exact-divide(
  $numbers as xs:integer*,
  $divident as xs:integer) as xs:boolean
{
  if (fn:empty($numbers)) then
    fn:true()
  else
    if ($numbers[1] mod $divident ne 0) then
      fn:false()
    else
      excel:try-exact-divide(fn:subsequence($numbers, 2), $divident)
};

(:~
 : Helper function for computing GCD.
 : This function should not be used outside this module.
 : This function iterates through possible divisors and checks if the sequence
 : divides exactly to any of those. It starts from the minimum value from the
 : sequence and searches downwards.
 :
 : @param $numbers The sequence of positive integers.
 : @param $min-nonzero The minimum value of numbers sequence, excluding the zero value.
 : @param $iteration Which iteration is it. It starts from 1 and continues
 :        to min-nonzero/2.
 : @return The greatest common divisor if found, or 1 if not found.
 :)
declare function excel:iterate-all-gcd(
  $numbers as xs:integer*,
  $min-nonzero as xs:integer,
  $iteration as xs:integer) as xs:integer
{
  if ($min-nonzero mod $iteration eq 0) then
    if (excel:try-exact-divide($numbers, $min-nonzero idiv $iteration)) then
      $min-nonzero idiv $iteration
    else
      excel:iterate-all-gcd($numbers, $min-nonzero, $iteration + 1)
  else
    if ($iteration > $min-nonzero idiv 2) then
      1
    else
      excel:iterate-all-gcd($numbers, $min-nonzero, $iteration + 1)
};

(:~
 : Returns the greatest common divisor GCD of a sequence of integers.
 : The sequence can have one or more positive integers.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052091041033.aspx
 : @param $numbers The sequence of positive integers.
 : @return The GCD as integer.
 : @error XQP0021(errNum) if any number is smaller than zero.
 :)
declare function excel:gcd($numbers as xs:integer+) as xs:integer
{
  if (fn:count($numbers) = 1) then
    $numbers[1]
  else
    let $minval := excel:min-without-zero($numbers)
    return
      if ($minval lt 0) then
        fn:error($excel-err:errNum, "gcd function: numbers should be greater than zero or equal")
      else if ($minval eq 0) then
        0
      else
        excel:iterate-all-gcd($numbers, $minval, 1)
};

(:~
 : Returns the least common multiple of integers.
 : LCM is computed by multiplying all the numbers and dividing with GCD.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052091521033.aspx
 : @param $numbers The sequence of one or more positive integers.
 : @return The LCM as integer.
 : @error XQP0021(errNum) if any number is smaller than zero.
 :)
declare function excel:lcm($numbers as xs:integer+) as xs:integer
{
  let $product := excel:product(fn:distinct-values($numbers))
  return
    if ($product eq 0) then
      0
    else
      $product idiv excel:gcd($numbers)
};

(:~
 : Returns a number rounded to the desired multiple.
 : MROUND rounds up, away from zero, if the remainder of dividing number by multiple
 : is greater than or equal to half the value of multiple.
 : MROUND is computed through floor function.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052091851033.aspx
 : @param $number The value to round, castable to numeric type.
 : @param $multiple The multiple to which you want to round number.
 : @return The rounded number up to the desired multiple.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:mround(
  $number   as xs:anyAtomicType,
  $multiple as xs:anyAtomicType) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  let $mul := excel:cast-as-numeric($multiple)
  let $floor := excel:floor($num, $mul) return
  if ($num ge 0) then
    if (($num - $floor) ge ($mul div 2)) then
      $floor + $mul
    else
      $floor
  else
    if ((-$num + $floor) ge (-$mul div 2)) then
      $floor + $mul
    else
      $floor
};

(:~
 : Converts degrees to radians.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092281033.aspx
 : @param $degree An angle in degrees that you want to convert.
 : @return The value in radians.
 :)
declare function excel:radians($degree as xs:integer) as xs:decimal
{
  ($degree mod 360) div 180.0 * excel:pi()
};

(:~
 : Converts an arabic numeral to roman, as text.
 : Only the clasic format is supported (out of all formats Excel requires).

 : M is the largest digit, it represents 1000.
 : Numbers bigger than 2000 will be represented by a sequence of "M".

 : D = 500, C = 100, L = 50, X = 10, V = 5, I = 1.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092381033.aspx
 : @param $number A positive integer.
 : @return The roman string representation.
 : @error XQP0021(errNum) if the input integer is negative
 :)
declare function excel:roman($number as xs:integer) as xs:string
{
  if ($number lt 0) then
    fn:error($excel-err:errNum, "Roman function: number should be greater than zero or equal")
  else if ($number ge 1000) then
    fn:concat("M", excel:roman($number - 1000))
  else if ($number ge 900) then
    fn:concat("CM", excel:roman($number - 900))
  else if ($number ge 800) then
    fn:concat("DCCC", excel:roman($number - 800))
  else if ($number ge 700) then
    fn:concat("DCC", excel:roman($number - 700))
  else if ($number ge 600) then
    fn:concat("DC", excel:roman($number - 600))
  else if ($number ge 500) then
    fn:concat("D", excel:roman($number - 500))
  else if ($number ge 400) then
    fn:concat("CD", excel:roman($number - 400))
  else if ($number ge 300) then
    fn:concat("CCC", excel:roman($number - 300))
  else if ($number ge 200) then
    fn:concat("CC", excel:roman($number - 200))
  else if ($number ge 100) then
    fn:concat("C", excel:roman($number - 100))
  else if ($number ge 90) then
    fn:concat("XC", excel:roman($number - 90))
  else if ($number ge 80) then
    fn:concat("LXXX", excel:roman($number - 80))
  else if ($number ge 70) then
    fn:concat("LXX", excel:roman($number - 70))
  else if ($number ge 60) then
    fn:concat("LX", excel:roman($number - 60))
  else if ($number ge 50) then
    fn:concat("L", excel:roman($number - 50))
  else if ($number ge 40) then
    fn:concat("XL", excel:roman($number - 40))
  else if ($number ge 30) then
    fn:concat("XXX", excel:roman($number - 30))
  else if ($number ge 20) then
    fn:concat("XX", excel:roman($number - 20))
  else if ($number ge 10) then
    fn:concat("X", excel:roman($number - 10))
  else if ($number eq 9) then
    "IX"
  else if ($number eq 8) then
    "VIII"
  else if ($number eq 7) then
    "VII"
  else if ($number eq 6) then
    "VI"
  else if ($number eq 5) then
    "V"
  else if ($number eq 4) then
    "IV"
  else if ($number eq 3) then
    "III"
  else if ($number eq 2) then
    "II"
  else if ($number eq 1) then
    "I"
  else
    ""
};

(:
 : Copyright 2006-2009 The FLWOR Foundation.
 :
 : Licensed under the Apache License, Version 2.0 (the "License");
 : you may not use this file except in compliance with the License.
 : You may obtain a copy of the License at
 :
 : http://www.apache.org/licenses/LICENSE-2.0
 :
 : Unless required by applicable law or agreed to in writing, software
 : distributed under the License is distributed on an "AS IS" BASIS,
 : WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 : See the License for the specific language governing permissions and
 : limitations under the License.
 :)

(:~
 : This is a library module offering the same set of functions
 : defined by Microsoft Excel.
 :
 : @see Excel Documentation: Math Functions
 :
 : @spec XQuery Specification: January 2007
 : @author Daniel Turcanu
 : @version 1.0
 :)
module namespace  excel = "http://www.zorba-xquery.com/modules/excel/math" ;

(:~
 : Use excel-err module functions for throwing errors.
 :)
import module namespace excel-err="http://www.zorba-xquery.com/modules/excel/errors";

(:~
 : Checks if the xs:anyAtomicType argument is actually a numeric type
 : or can be converted to numeric.
 :
 : @param $value Parameter to be checked.
 : @return true if the value can be casted to numeric.
 :)
declare function excel:is-a-number($value as xs:anyAtomicType) as xs:boolean
{
  fn:string(fn:number($value)) ne 'NaN'
};

(:~
 : Cast the xs:anyAtomicType to a numeric type.
 : If the value is already of a numeric type then nothing is changed.
 : Otherwise the value is casted to the numeric type that is most appropriate.
 :
 : @param $number The parameter can be a number, string, boolean value.
 : @return The casted value.
 : @error XQP0021(errValue) if the value cannot be casted to numeric type.
 :)
declare function excel:cast-as-numeric($number as xs:anyAtomicType) as xs:anyAtomicType
{
  typeswitch ($number)
    case xs:double return $number
    case xs:decimal return $number
    case xs:double return $number
    case xs:float return $number
    default return
      if ($number castable as xs:integer) then
        xs:integer($number)
      else if ($number castable as xs:decimal) then
        xs:decimal($number)
      else if ($number castable as xs:double) then
        xs:double($number)
      else
        fn:error($excel-err:errValue, "Provided value is not a number", $number)
};

(: ---------------- Excel Math functions ----------------------- :)

(:~
 : Compute the abs of a numeric value.
 : The value can also be a string and it will be casted to the appropriate numeric first.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052089781033.aspx
 : @param $arg The parameter can be a number, string, boolean value.
 : @return The abs value as a numeric type.
 : @error XQP0021(errValue) if arg cannot be casted to numeric type.
 :)
declare function excel:abs($arg as xs:anyAtomicType) as xs:anyAtomicType
{
  fn:abs(excel:cast-as-numeric($arg))
};

(:~
 : Returns number rounded up, away from zero, to the nearest multiple of significance.
 : Significance must have the same sign as number.
 : Number and significance must be of a numeric type or castable to numeric.
 : Significance must not be zero.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052090071033.aspx
 : @param $number The value you want to round.
 : @param $significance The multiple to which you want to round.
 : @return The rounded value.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 : @error XQP0021(errNum) if significance is zero or it doesn't have the same sign as number.
 :)
declare function excel:ceiling(
  $number        as xs:anyAtomicType,
  $significance  as xs:anyAtomicType) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  let $sig := excel:cast-as-numeric($significance)
  return
    if ($sig eq 0) then
      fn:error($excel-err:errNum, "Ceiling function does not accept significance 0")
    else if ($num * $sig ge 0) then
	    fn:ceiling($num div $sig) * $significance
    else
      fn:error($excel-err:errNum, "Ceiling function: both arguments must have the same sign")
};

(:~
 : Returns number rounded up to the nearest even integer.
 : Regardless of the sign of number, a value is rounded up when adjusted away from zero.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052090801033.aspx
 : @param $number The value to round.
 : @return The rounded value casted as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:even($number as xs:anyAtomicType) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  return
    if ($num = 0) then
      0
    else
      let $intnum := excel:ceiling($num, excel:sign($num))
      return
        if ($intnum mod 2 ne 0) then
          if ($intnum gt 0) then
            $intnum + 1
          else
            $intnum - 1
        else
          $intnum
};

(:~
 : Helper function for computing factorial.
 : This function should not be used outside this module.
 : This recursive function computes: number * fact(number-1)
 :
 : @param $intnum A positive integer.
 : @return The factorial of intnum.
:)
declare function excel:fact-integer($intnum as xs:integer) as xs:integer
{
  if ($intnum = 1) then
    1
  else
    $intnum * excel:fact-integer($intnum - 1)
};

(:~
 : Returns the factorial of a number.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052090841033.aspx
 : @param $number The nonnegative number you want the factorial of.
 :        If number is not an integer, it is truncated.
 : @return Returns the factorial of a number. The factorial of a number is equal to 1*2*3*...* number.
 : @error XQP0021(errNum) if the number is smaller than zero
:)
declare function excel:fact($number as xs:anyAtomicType) as xs:integer
{
  let $num := excel:cast-as-numeric($number) return
    if ($num eq 0) then
      1
    else
      if ($num lt 0) then
        fn:error($excel-err:errNum, "Fact function does not accept numbers less than zero")
      else
        excel:fact-integer(xs:integer($num))
};

(:~
 : Rounds number down, toward zero, to the nearest multiple of significance.
 : Significance must have the same sign as number.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052090941033.aspx
 : @param $number The value you want to round. The value is casted to numeric.
 : @param $significance The multiple to which you want to round.
 : @return The rounded value as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 : @error XQP0021(errNum) if significance is zero or it doesn't have the same sign as number.
:)
declare function excel:floor(
  $number as xs:anyAtomicType,
  $significance as xs:anyAtomicType) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  let $sig := excel:cast-as-numeric($significance)
  return
    if ($sig eq 0) then
      fn:error($excel-err:errNum, "Floor function does not accept significance 0")
    else if ($num * $sig ge 0) then
      fn:floor($number div $sig) * $sig
    else
      fn:error($excel-err:errNum, "Floor function: both arguments must have the same sign")
};

(:~
 : Rounds a number down to the nearest integer.
 : Positive numbers are rounded toward zero, negative numbers are rounded away from zero.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052091421033.aspx
 : @param $number The value to be rounded.
 : @return The rounded integer.
 : @error XQP0021(errValue) if parameter cannot be casted to numeric type
:)
declare function excel:int($number as xs:anyAtomicType) as xs:integer
{
  xs:integer(fn:floor(excel:cast-as-numeric($number)))
};

(:~
 : Returns the remainder after number is divided by divisor.
 : The result has the same sign as divisor.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052091821033.aspx
 : @param $number The number for which you want to find the remainder.
 : @param $divisor The number by which you want to divide number.
 :        This cannot be zero.
 : @return The remainder from division as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 : @error XQP0021(errDiv0) if divisor is zero after casting to numeric.
 :)
declare function excel:mod(
  $number as xs:anyAtomicType,
  $divisor as xs:anyAtomicType) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  let $div := excel:cast-as-numeric($divisor) return
    if ($div eq 0) then
      fn:error($excel-err:errDiv0, "Mod operator: divide by 0")
    else
      let $result := $num mod $div
      return
        if ($result * $div lt 0) then
          -$result
        else
          $result
};

(:~
 : Returns number rounded up to the nearest odd integer, away from zero.
 :
 : @see  http://office.microsoft.com/en-us/excel/HP052092031033.aspx
 : @param $number The value to round.
 : @return The odd integer.
 : @error XQP0021(errValue) if parameter cannot be casted to numeric type.
 :)
declare function excel:odd($number as xs:anyAtomicType) as xs:integer
{
  let $num := excel:cast-as-numeric($number) return
  if ($num eq 0) then
    1
  else
    let $intnum := excel:ceiling($num, excel:sign($num))
    return
      if ($intnum mod 2 eq 0) then
        if ($intnum ge 0) then
          ($intnum + 1) cast as xs:integer
        else
          ($intnum - 1) cast as xs:integer
      else
        $intnum cast as xs:integer
};

(:~
 : Return the value of PI as decimal with 15 digits.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092141033.aspx
 : @return The value of PI with 15 digits.
 :)
declare function excel:pi() as xs:decimal
{
  3.14159265358979
};

(:~
 : Returns the result of a number raised to a power.
 : The result is computed through successive multiplications.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092171033.aspx
 : @param $number The base number.
 : @param $power The exponent as integer (cannot be floating point like in Excel).
 : @return The result as numeric type.
 : @error XQP0021(errValue) if parameter cannot be casted to numeric type.
 : @error XQP0021(errValue) if power is smaller than zero.
 :)
declare function excel:power(
  $number as xs:anyAtomicType,
  $power as xs:integer) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  return
    if ($power lt 0) then
      fn:error($excel-err:errValue, "Power function: power must be greater or equal than zero")
    else if ($power eq 0) then
      1
    else if ($power = 1) then
      $num
    else
      let $retval := excel:power($num, $power idiv 2)
      return
        $retval * $retval * excel:power($num, $power mod 2)
 };

(:~
 : Helper function for product.
 : This function should not be used outside this module.
 : Multiplies all numbers in the sequence.
 :
 : @param $numbers The list of arguments to be casted to numeric and multiplied.
 : @return The multiplication result as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:product-internal($numbers as xs:anyAtomicType*) as xs:anyAtomicType
{
  if (fn:empty($numbers)) then
    1
  else
    let $x := excel:cast-as-numeric($numbers[1])
    return
      $x * excel:product-internal(fn:subsequence($numbers, 2))
};

(:~
 : Multiplies all the numbers given as arguments and returns the product.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092231033.aspx
 : @param $numbers The sequence of arguments convertable to numeric types.
 :        The sequence can be of any length.
 : @return The multiplication result as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:product($numbers as xs:anyAtomicType*) as xs:anyAtomicType
{
  if (fn:empty($numbers)) then
    0
  else
    excel:product-internal($numbers)
};

(:~
 : Returns the integer portion of a division.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092271033.aspx
 : @param $numerator The divident.
 : @param $denominator The divisor. It cannot be zero.
 : @return The result value as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 : @error XQP0021(div0) if denominator casted as numeric type has value zero.
 :)
declare function excel:quotient(
  $numerator   as xs:anyAtomicType,
  $denominator as xs:anyAtomicType) as xs:integer
{
  let $numer := excel:cast-as-numeric($numerator)
  let $denom := excel:cast-as-numeric($denominator)
  return
    if ($denom eq 0) then
      fn:error($excel-err:errDiv0, "Quotient function: divide by 0")
    else
      xs:integer($numer div $denom)
};

(:~
 : Rounds a number to a specified number of digits.
 : If precision is greater than 0 (zero), then number is rounded
 : to the specified number of decimal places.
 : If num_digits is 0, then number is rounded to the nearest integer.
 : If num_digits is less than 0, then number is rounded to the left of the decimal point.
 : The 0.5 is rounded away from zero.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092391033.aspx
 : @param $number The number to round, castable to a numeric type.
 : @param $precision The number of decimal places to keep.
 : @return The rounded number as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:round(
  $number as xs:anyAtomicType,
  $precision as xs:integer) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  return
    if ($precision ge 0) then
      let $exp_prec := excel:power(10, $precision)
      return
        if ($num ge 0) then
          fn:floor($num * $exp_prec + 0.5) div $exp_prec
        else
          -fn:floor(-$num * $exp_prec + 0.5) div $exp_prec
    else
      let $exp_prec := excel:power(10, -$precision)
      return
        if ($num ge 0) then
          fn:floor($num div $exp_prec + 0.5) * $exp_prec
        else
          -fn:floor(-$num div $exp_prec + 0.5) * $exp_prec
};

(:~
 : Rounds a number down, toward zero.
 : If num_digits is greater than 0 (zero), then number is rounded down
 : to the specified number of decimal places.
 : If num_digits is 0, then number is rounded down to the nearest integer.
 : If num_digits is less than 0, then number is rounded down to the left of the decimal point.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092411033.aspx
 : @param $number The number to round, castable to numeric type.
 : @param $precision The number of decimal places to keep.
 : @return the truncated number toward zero, as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:rounddown(
  $number     as xs:anyAtomicType,
  $precision  as xs:integer) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  return
    if ($precision ge 0) then
      let $exp_prec := excel:power(10, $precision)
      return
        if ($num ge 0) then
          fn:floor($num * $exp_prec) div $exp_prec
        else
          -fn:floor(-$num * $exp_prec) div $exp_prec
    else
      let $exp_prec := excel:power(10, -$precision)
      return
        if ($num ge 0) then
          fn:floor($num div $exp_prec) * $exp_prec
        else
          -fn:floor(-$num div $exp_prec) * $exp_prec
};

(:~
 : Rounds a number up, away from 0 (zero).
 : If num_digits is greater than 0 (zero), then number is rounded down
 : to the specified number of decimal places.
 : If num_digits is 0, then number is rounded down to the nearest integer.
 : If num_digits is less than 0, then number is rounded down to the left of the decimal point.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092421033.aspx
 : @param $number The number to round, castable to numeric type.
 : @param $precision The number of decimal places to keep.
 : @return The truncated number away from zero, as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:roundup(
  $number     as xs:anyAtomicType,
  $precision  as xs:integer) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  return
    if ($precision ge 0) then
      let $exp_prec := excel:power(10, $precision)
      return
        if ($num ge 0) then
           fn:ceiling($num * $exp_prec) div $exp_prec
        else
          -fn:ceiling(-$num * $exp_prec) div $exp_prec
    else
      let $exp_prec := excel:power(10, -$precision)
      return
        if ($num ge 0) then
          fn:ceiling($num div $exp_prec) * $exp_prec
        else
          -fn:ceiling(-$num div $exp_prec) * $exp_prec
};

(:~
 : Determines the sign of a number.
 : Returns 1 if the number is positive, zero (0) if the number is 0,
 : and -1 if the number is negative.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092551033.aspx
 : @param $number The argument castable to numeric type.
 : @return The sign as (-1, 0, 1).
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:sign($number as xs:anyAtomicType) as xs:integer
{
  let $num := excel:cast-as-numeric($number)
  return
    if ($num eq 0) then
      0
    else if ($num gt 0) then
      1
    else
      -1
 };

(:~
 : Adds all the numbers in the sequence.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092901033.aspx
 : @param $numbers The sequence of arguments castable to numeric types.
 :        The sequence can be of any length.
 : @return The sum as numeric type.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:sum($numbers as xs:anyAtomicType*) as xs:anyAtomicType
{
  if (fn:empty($numbers)) then
    0
  else
    let $x := excel:cast-as-numeric($numbers[1])
    return
      $x + excel:sum(fn:subsequence($numbers,2))
 };

(:~
 : Truncates a number to an integer by removing the fractional part of the number.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052093241033.aspx
 : @param $number The argument castable to numeric type.
 : @return The integer value.
 : @error XQP0021(errValue) if parameter cannot be casted to numeric type.
 :)
declare function excel:trunc($number as xs:anyAtomicType ) as xs:integer
{
  xs:integer(excel:cast-as-numeric($number))
};

(:~
 : Truncates a number down to precision.
 : This behaves exactly like rounddown.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052093241033.aspx
 : @param $number The argument castable to numeric type.
 : @param $precision The number of decimal places to keep .
 : @return The integer value.
 : @error XQP0021(errValue) if parameter cannot be casted to numeric type.
 :)
declare function excel:trunc(
  $number as xs:anyAtomicType,
  $precision as xs:integer) as xs:anyAtomicType
{
  excel:rounddown(excel:cast-as-numeric($number), $precision)
};

(:~
 : Helper function.
 : This function should not be used outside this module.
 : Sorts a sequence of numbers or arguments castable to numeric.
 : It first casts all arguments to numeric and then sorts ascending.
 :
 : @param $numbers The sequence of arguments castable to numeric.
 : @return The sorted sequence as numeric types.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:sort-numbers($numbers as xs:anyAtomicType*) as xs:anyAtomicType*
{
  let $sorted-numbers :=
    (
      for $number in $numbers
      let $num := excel:cast-as-numeric($number)
      order by $num
      return $num
    )
  return $sorted-numbers
};

(: priority 1 :)

(:~
 : Converts radians into degrees.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052090561033.aspx
 : @param $radian The value in radians.
 : @return The value in degrees 0 .. 360 or 0 .. -360.
 :)
declare function excel:degrees($radian as xs:double) as xs:integer
{
  xs:integer(($radian div excel:pi() * 180) mod 360)
};

(:~
 : Returns the double factorial of a number.
 : Computes the double factorial of n as n(n-2)(n-4)...
 :
 : @see http://office.microsoft.com/en-us/excel/HP052090851033.aspx
 : @param $number The positive integer value.
 : @return The result as integer.
 : @error XQP0021(errNum) if the number is negative.
 :)
declare function excel:factdouble($number as xs:integer) as xs:integer
{
  if ($number lt 0) then
    fn:error($excel-err:errNum, "Factdouble function: number should be greater than zero or equal")
  else if ($number eq 1) then
    1
  else if ($number eq 2) then
    2
  else
    $number * excel:factdouble($number - 2)
};

(:~
 : Helper function for computing GCD.
 : This function should not be used outside this module.
 : It calculates the minimum value from a sequence of positive integers,
 : not taking into account the zero value.
 :
 : @param $numbers The sequence of positive integers.
 : @return The minimum value. If the sequence contains only zero values, then zero is returned.
 :)
declare function excel:min-without-zero($numbers as xs:integer+) as xs:integer
{
  if (fn:count($numbers) eq 1) then
    $numbers[1]
  else
    let $min-other := excel:min-without-zero(fn:subsequence($numbers, 2))
    return
      if ($numbers[1] eq 0) then
        $min-other
      else if ($min-other eq 0) then
        $numbers[1]
      else if ($numbers[1] lt $min-other) then
        $numbers[1]
      else
        $min-other
};

(:~
 : Helper function for computing GCD.
 : This function should not be used outside this module.
 : Checks if all integer numbers from a sequence divide exactly to a divident.
 :
 : @param $numbers The positive integers.
 : @param $divident The divident to be tried.
 : @return true if the numbers divide exactly.
:)
declare function excel:try-exact-divide(
  $numbers as xs:integer*,
  $divident as xs:integer) as xs:boolean
{
  if (fn:empty($numbers)) then
    fn:true()
  else
    if ($numbers[1] mod $divident ne 0) then
      fn:false()
    else
      excel:try-exact-divide(fn:subsequence($numbers, 2), $divident)
};

(:~
 : Helper function for computing GCD.
 : This function should not be used outside this module.
 : This function iterates through possible divisors and checks if the sequence
 : divides exactly to any of those. It starts from the minimum value from the
 : sequence and searches downwards.
 :
 : @param $numbers The sequence of positive integers.
 : @param $min-nonzero The minimum value of numbers sequence, excluding the zero value.
 : @param $iteration Which iteration is it. It starts from 1 and continues
 :        to min-nonzero/2.
 : @return The greatest common divisor if found, or 1 if not found.
 :)
declare function excel:iterate-all-gcd(
  $numbers as xs:integer*,
  $min-nonzero as xs:integer,
  $iteration as xs:integer) as xs:integer
{
  if ($min-nonzero mod $iteration eq 0) then
    if (excel:try-exact-divide($numbers, $min-nonzero idiv $iteration)) then
      $min-nonzero idiv $iteration
    else
      excel:iterate-all-gcd($numbers, $min-nonzero, $iteration + 1)
  else
    if ($iteration > $min-nonzero idiv 2) then
      1
    else
      excel:iterate-all-gcd($numbers, $min-nonzero, $iteration + 1)
};

(:~
 : Returns the greatest common divisor GCD of a sequence of integers.
 : The sequence can have one or more positive integers.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052091041033.aspx
 : @param $numbers The sequence of positive integers.
 : @return The GCD as integer.
 : @error XQP0021(errNum) if any number is smaller than zero.
 :)
declare function excel:gcd($numbers as xs:integer+) as xs:integer
{
  if (fn:count($numbers) = 1) then
    $numbers[1]
  else
    let $minval := excel:min-without-zero($numbers)
    return
      if ($minval lt 0) then
        fn:error($excel-err:errNum, "gcd function: numbers should be greater than zero or equal")
      else if ($minval eq 0) then
        0
      else
        excel:iterate-all-gcd($numbers, $minval, 1)
};

(:~
 : Returns the least common multiple of integers.
 : LCM is computed by multiplying all the numbers and dividing with GCD.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052091521033.aspx
 : @param $numbers The sequence of one or more positive integers.
 : @return The LCM as integer.
 : @error XQP0021(errNum) if any number is smaller than zero.
 :)
declare function excel:lcm($numbers as xs:integer+) as xs:integer
{
  let $product := excel:product(fn:distinct-values($numbers))
  return
    if ($product eq 0) then
      0
    else
      $product idiv excel:gcd($numbers)
};

(:~
 : Returns a number rounded to the desired multiple.
 : MROUND rounds up, away from zero, if the remainder of dividing number by multiple
 : is greater than or equal to half the value of multiple.
 : MROUND is computed through floor function.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052091851033.aspx
 : @param $number The value to round, castable to numeric type.
 : @param $multiple The multiple to which you want to round number.
 : @return The rounded number up to the desired multiple.
 : @error XQP0021(errValue) if parameters cannot be casted to numeric type.
 :)
declare function excel:mround(
  $number   as xs:anyAtomicType,
  $multiple as xs:anyAtomicType) as xs:anyAtomicType
{
  let $num := excel:cast-as-numeric($number)
  let $mul := excel:cast-as-numeric($multiple)
  let $floor := excel:floor($num, $mul) return
  if ($num ge 0) then
    if (($num - $floor) ge ($mul div 2)) then
      $floor + $mul
    else
      $floor
  else
    if ((-$num + $floor) ge (-$mul div 2)) then
      $floor + $mul
    else
      $floor
};

(:~
 : Converts degrees to radians.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092281033.aspx
 : @param $degree An angle in degrees that you want to convert.
 : @return The value in radians.
 :)
declare function excel:radians($degree as xs:integer) as xs:decimal
{
  ($degree mod 360) div 180.0 * excel:pi()
};

(:~
 : Converts an arabic numeral to roman, as text.
 : Only the clasic format is supported (out of all formats Excel requires).

 : M is the largest digit, it represents 1000.
 : Numbers bigger than 2000 will be represented by a sequence of "M".

 : D = 500, C = 100, L = 50, X = 10, V = 5, I = 1.
 :
 : @see http://office.microsoft.com/en-us/excel/HP052092381033.aspx
 : @param $number A positive integer.
 : @return The roman string representation.
 : @error XQP0021(errNum) if the input integer is negative
 :)
declare function excel:roman($number as xs:integer) as xs:string
{
  if ($number lt 0) then
    fn:error($excel-err:errNum, "Roman function: number should be greater than zero or equal")
  else if ($number ge 1000) then
    fn:concat("M", excel:roman($number - 1000))
  else if ($number ge 900) then
    fn:concat("CM", excel:roman($number - 900))
  else if ($number ge 800) then
    fn:concat("DCCC", excel:roman($number - 800))
  else if ($number ge 700) then
    fn:concat("DCC", excel:roman($number - 700))
  else if ($number ge 600) then
    fn:concat("DC", excel:roman($number - 600))
  else if ($number ge 500) then
    fn:concat("D", excel:roman($number - 500))
  else if ($number ge 400) then
    fn:concat("CD", excel:roman($number - 400))
  else if ($number ge 300) then
    fn:concat("CCC", excel:roman($number - 300))
  else if ($number ge 200) then
    fn:concat("CC", excel:roman($number - 200))
  else if ($number ge 100) then
    fn:concat("C", excel:roman($number - 100))
  else if ($number ge 90) then
    fn:concat("XC", excel:roman($number - 90))
  else if ($number ge 80) then
    fn:concat("LXXX", excel:roman($number - 80))
  else if ($number ge 70) then
    fn:concat("LXX", excel:roman($number - 70))
  else if ($number ge 60) then
    fn:concat("LX", excel:roman($number - 60))
  else if ($number ge 50) then
    fn:concat("L", excel:roman($number - 50))
  else if ($number ge 40) then
    fn:concat("XL", excel:roman($number - 40))
  else if ($number ge 30) then
    fn:concat("XXX", excel:roman($number - 30))
  else if ($number ge 20) then
    fn:concat("XX", excel:roman($number - 20))
  else if ($number ge 10) then
    fn:concat("X", excel:roman($number - 10))
  else if ($number eq 9) then
    "IX"
  else if ($number eq 8) then
    "VIII"
  else if ($number eq 7) then
    "VII"
  else if ($number eq 6) then
    "VI"
  else if ($number eq 5) then
    "V"
  else if ($number eq 4) then
    "IV"
  else if ($number eq 3) then
    "III"
  else if ($number eq 2) then
    "II"
  else if ($number eq 1) then
    "I"
  else
    ""
};


[bugfix] Fix incorrect assertions in positional-nested.xql

xquery version "3.1";

module namespace npt="http://exist-db.org/test/nested-positional-predicate";

declare namespace test="http://exist-db.org/xquery/xqsuite";

declare variable $npt:TEST_COLLECTION_NAME := "test-positional-nested";
declare variable $npt:TEST_COLLECTION_URI := "/db/" || $npt:TEST_COLLECTION_NAME;
declare variable $npt:TEST_DOC_NAME := "test.xml";
declare variable $npt:TEST_DOC_URI := $npt:TEST_COLLECTION_URI || "/" || $npt:TEST_DOC_NAME;

declare variable $npt:DATA :=
    document {
        <xml>

                B1

                B2
                <c>correct</c>

                B3
                <c>wrong</c>

        </xml>
    };

declare
    %test:setUp
function npt:setup() {
    xmldb:create-collection("/db", $npt:TEST_COLLECTION_NAME),
    xmldb:store($npt:TEST_COLLECTION_URI, $npt:TEST_DOC_NAME, $npt:DATA)
};

declare
    %test:tearDown
function npt:cleanup() {
    xmldb:remove($npt:TEST_COLLECTION_URI)
};

declare
    %test:assertEquals("<c>correct</c>", "<c>wrong</c>")
function npt:in-memory() {
    $npt:DATA//c[../preceding-sibling::a]
};

declare
    %test:assertEquals("<c>correct</c>", "<c>wrong</c>")
function npt:in-database() {
    doc($npt:TEST_DOC_URI)//c[../preceding-sibling::a]
};

declare
    %test:assertEquals("<c>correct</c>", "<c>wrong</c>")
function npt:in-memory-predicate() {
    $npt:DATA//c[../preceding-sibling::a[1]]
};

declare
    %test:assertEquals("<c>correct</c>", "<c>wrong</c>")
function npt:in-database-predicate() {
    doc($npt:TEST_DOC_URI)//c[../preceding-sibling::a[1]]
};

declare
    %test:assertEquals("<c>correct</c>", "<c>wrong</c>")
function npt:in-memory-position() {
    $npt:DATA//c[../preceding-sibling::a[position() eq 1]]
};

declare
    %test:assertEquals("<c>correct</c>", "<c>wrong</c>")
function npt:in-database-position() {
    doc($npt:TEST_DOC_URI)//c[../preceding-sibling::a[position() eq 1]]
};

declare
    %test:assertEquals("<c>correct</c>")
function npt:in-memory-predicate-and-path() {
    $npt:DATA//c[../preceding-sibling::a[1]/b = 'B1']
};

declare
    %test:assertEquals("<c>correct</c>")
function npt:in-database-predicate-and-path() {
    doc($npt:TEST_DOC_URI)//c[../preceding-sibling::a[1]/b = 'B1']
};

declare
    %test:assertEquals("<c>correct</c>")
function npt:in-memory-position-and-path() {
    $npt:DATA//c[../preceding-sibling::a[position() eq 1]/b = 'B1']
};

declare
    %test:assertEquals("<c>correct</c>")
function npt:in-database-position-and-path() {
    doc($npt:TEST_DOC_URI)//c[../preceding-sibling::a[position() eq 1]/b = 'B1']
};

xquery version "3.1";

module namespace npt="http://exist-db.org/test/nested-positional-predicate";

declare namespace test="http://exist-db.org/xquery/xqsuite";

declare variable $npt:TEST_COLLECTION_NAME := "test-positional-nested";
declare variable $npt:TEST_COLLECTION_URI := "/db/" || $npt:TEST_COLLECTION_NAME;
declare variable $npt:TEST_DOC_NAME := "test.xml";
declare variable $npt:TEST_DOC_URI := $npt:TEST_COLLECTION_URI || "/" || $npt:TEST_DOC_NAME;

declare variable $npt:DATA :=
    document {
        <xml>

                B1

                B2
                <c>correct</c>

                B3
                <c>wrong</c>

        </xml>
    };

declare
    %test:setUp
function npt:setup() {
    xmldb:create-collection("/db", $npt:TEST_COLLECTION_NAME),
    xmldb:store($npt:TEST_COLLECTION_URI, $npt:TEST_DOC_NAME, $npt:DATA)
};

declare
    %test:tearDown
function npt:cleanup() {
    xmldb:remove($npt:TEST_COLLECTION_URI)
};

declare
    %test:assertEquals("<c>correct</c><c>wrong</c>")
function npt:in-memory() {
    $npt:DATA//c[../preceding-sibling::a]
};

declare
    %test:assertEquals("<c>correct</c><c>wrong</c>")
function npt:in-database() {
    doc($npt:TEST_DOC_URI)//c[../preceding-sibling::a]
};

declare
    %test:assertEquals("<c>correct</c><c>wrong</c>")
function npt:in-memory-predicate() {
    $npt:DATA//c[../preceding-sibling::a[1]]
};

declare
    %test:assertEquals("<c>correct</c><c>wrong</c>")
function npt:in-database-predicate() {
    doc($npt:TEST_DOC_URI)//c[../preceding-sibling::a[1]]
};

declare
    %test:assertEquals("<c>correct</c><c>wrong</c>")
function npt:in-memory-position() {
    $npt:DATA//c[../preceding-sibling::a[position() eq 1]]
};

declare
    %test:assertEquals("<c>correct</c><c>wrong</c>")
function npt:in-database-position() {
    doc($npt:TEST_DOC_URI)//c[../preceding-sibling::a[position() eq 1]]
};

declare
    %test:assertEquals("<c>correct</c>")
function npt:in-memory-predicate-and-path() {
    $npt:DATA//c[../preceding-sibling::a[1]/b = 'B1']
};

declare
    %test:assertEquals("<c>correct</c>")
function npt:in-database-predicate-and-path() {
    doc($npt:TEST_DOC_URI)//c[../preceding-sibling::a[1]/b = 'B1']
};

declare
    %test:assertEquals("<c>correct</c>")
function npt:in-memory-position-and-path() {
    $npt:DATA//c[../preceding-sibling::a[position() eq 1]/b = 'B1']
};

declare
    %test:assertEquals("<c>correct</c>")
function npt:in-database-position-and-path() {
    doc($npt:TEST_DOC_URI)//c[../preceding-sibling::a[position() eq 1]/b = 'B1']
};


fixed document to doc

fixed document to doc

svn path=/trunk/eXist/; revision=13620

xquery version "1.0";

declare namespace repo="http://exist-db.org/xquery/repo";

declare namespace xdb="http://exist-db.org/xquery/xmldb";
declare namespace package="http://expath.org/ns/pkg";

declare variable $repo-coll := "/db/repo";

declare function local:entry-data($path as xs:anyURI, $type as xs:string, $data as item()?, $param as item()*) as item()?
{

	<entry>
		<path>{$path}</path>
		<type>{$type}</type>
		<data>{$data}</data>
	</entry>
};

declare function local:entry-filter($path as xs:anyURI, $type as xs:string, $param as item()*) as xs:boolean
{

	true()
};

declare function local:process-action() as element()*
{
    let $repocol :=  if (collection($repo-coll)) then () else xmldb:create-collection('/db','repo')
    let $action := request:get-parameter("action", ())
    return
        util:catch("java.lang.Exception",
            if($action eq "Upload Package") then
              local:upload()
            else
              (),

                An error occurred while processing the action:

                {$util:exception-message}

        )
};

declare function local:upload() as element()
{
    let $name := request:get-parameter("name", ()),
    $docName := if($name) then $name else request:get-uploaded-file-name("upload"),
    $file := request:get-uploaded-file-data("upload") return

Actions:

                {
                if (contains($docName,'.xar')) then
                    (
uploaded package: {$docName}
,
                   xdb:decode-uri(xs:anyURI(xdb:store($repo-coll, xdb:encode-uri($docName), $file)))
                   ,
                    let $meta := compression:unzip($file, util:function(xs:QName("local:entry-filter"), 3), (),  util:function(xs:QName("local:entry-data"), 4), ())
                    let $package := $meta//package:package
                    let $repo := $meta//repo:meta
                    return
                     xdb:store($repo-coll, concat(substring-before($docName,'.xar'),'.xml'), <meta>{$package}{$repo}</meta>)
                   )
                else

Error uploading - Must be a valid Package archive (.xar file extension)

                }

};

    <title>eXist XML Database Public Package Repository: http://{request:get-hostname()}:{request:get-server-port()}/exist/repo/public/all/ </title>
    <link rel="stylesheet" type="text/css" href="../../styles/default-style2.css" />

eXist XML Database Public Repository  http://{request:get-hostname()}:{request:get-server-port()}/exist/repo/public/all/

 To install packages from this repository you will require:

In your eXist repo manager use this uri : http://{request:get-hostname()}:{request:get-server-port()}/exist/repo/public/all/

access permission to this repository (set by the administrator of this repository)

    {local:process-action()}

Library Packages

            Package Name
            Description
            Namespace(s)
            Date Created
            Author
            License
            Status

     {
     let $files := if (collection($repo-coll)) then collection($repo-coll)/util:document-name(.) else ()
     return
       for $file in $files[contains(.,'.xar')]
       let $name := substring-before($file,'.xar')
       let $package := doc(concat($repo-coll,'/',$name,'.xml'))
       return
        if ($package//repo:type eq 'library') then

            {$name}

            {$package//repo:type}

            {$package//package:title/text()}

            {if ($package//package:java/package:namespace/text()) then (concat('java: ',$package//package:java/package:namespace/text()),
) else () }
            {if ($package//package:xquery/package:namespace/text()) then (concat('xquery: ',$package//package:xquery/package:namespace/text()),
) else () }
            {if ($package//package:xslt/package:import-uri/text()) then (concat('xslt: ',$package//package:xslt/package:import-uri/text()),
) else () }

            {xmldb:created($repo-coll, $file)}
            {$package//repo:author}
            {$package//repo:license}
            {$package//repo:status}

        else
         ()

     }

Application Packages

            Package Name
            Description
            Namespace(s)
            Date Created
            Author
            License
            Status

     {
     let $files := if (collection($repo-coll)) then collection($repo-coll)/util:document-name(.) else ()
     return
       for $file in $files[contains(.,'.xar')]
       let $name := substring-before($file,'.xar')
       let $package := doc(concat($repo-coll,'/',$name,'.xml'))
       return
        if ($package//repo:type eq 'application') then

            {$name}

            {if ($package//repo:deploy) then 'Application' else 'Library'}

            {$package//package:title/text()}

            {if ($package//package:java/package:namespace/text()) then (concat('java: ',$package//package:java/package:namespace/text()),
) else () }
            {if ($package//package:xquery/package:namespace/text()) then (concat('xquery: ',$package//package:xquery/package:namespace/text()),
) else () }
            {if ($package//package:xslt/package:import-uri/text()) then (concat('xslt: ',$package//package:xslt/package:import-uri/text()),
) else () }

            {xmldb:created($repo-coll, $file)}
            {$package//repo:author}
            {$package//repo:license}
            {$package//repo:status}

        else
          ()

     }

Add Package

You will require access rights to upload new packages for distribution from this repository.

    <form method="POST" enctype="multipart/form-data">

        <input type="submit" name="action" value="Upload Package"/>
        <input type="file" size="30" name="upload"/>

    Note: You can upload example .xar packages located under webapp/repo/packages
    </form>

xquery version "1.0";

declare namespace repo="http://exist-db.org/xquery/repo";

declare namespace xdb="http://exist-db.org/xquery/xmldb";
declare namespace package="http://expath.org/ns/pkg";

declare variable $repo-coll := "/db/repo";

declare function local:entry-data($path as xs:anyURI, $type as xs:string, $data as item()?, $param as item()*) as item()?
{

	<entry>
		<path>{$path}</path>
		<type>{$type}</type>
		<data>{$data}</data>
	</entry>
};

declare function local:entry-filter($path as xs:anyURI, $type as xs:string, $param as item()*) as xs:boolean
{

	true()
};

declare function local:process-action() as element()*
{
    let $repocol :=  if (collection($repo-coll)) then () else xmldb:create-collection('/db','repo')
    let $action := request:get-parameter("action", ())
    return
        util:catch("java.lang.Exception",
            if($action eq "Upload Package") then
              local:upload()
            else
              (),

                An error occurred while processing the action:

                {$util:exception-message}

        )
};

declare function local:upload() as element()
{
    let $name := request:get-parameter("name", ()),
    $docName := if($name) then $name else request:get-uploaded-file-name("upload"),
    $file := request:get-uploaded-file-data("upload") return

Actions:

                {
                if (contains($docName,'.xar')) then
                    (
uploaded package: {$docName}
,
                   xdb:decode-uri(xs:anyURI(xdb:store($repo-coll, xdb:encode-uri($docName), $file)))
                   ,
                    let $meta := compression:unzip($file, util:function(xs:QName("local:entry-filter"), 3), (),  util:function(xs:QName("local:entry-data"), 4), ())
                    let $package := $meta//package:package
                    let $repo := $meta//repo:meta
                    return
                     xdb:store($repo-coll, concat(substring-before($docName,'.xar'),'.xml'), <meta>{$package}{$repo}</meta>)
                   )
                else

Error uploading - Must be a valid Package archive (.xar file extension)

                }

};

    <title>eXist XML Database Public Package Repository: http://{request:get-hostname()}:{request:get-server-port()}/exist/repo/public/all/ </title>
    <link rel="stylesheet" type="text/css" href="../../styles/default-style2.css" />

eXist XML Database Public Repository  http://{request:get-hostname()}:{request:get-server-port()}/exist/repo/public/all/

 To install packages from this repository you will require:

In your eXist repo manager use this uri : http://{request:get-hostname()}:{request:get-server-port()}/exist/repo/public/all/

access permission to this repository (set by the administrator of this repository)

    {local:process-action()}

Library Packages

            Package Name
            Description
            Namespace(s)
            Date Created
            Author
            License
            Status

     {
     let $files := if (collection($repo-coll)) then collection($repo-coll)/util:document-name(.) else ()
     return
       for $file in $files[contains(.,'.xar')]
       let $name := substring-before($file,'.xar')
       let $package := document(concat($repo-coll,'/',$name,'.xml'))
       return
        if ($package//repo:type eq 'library') then

            {$name}

            {$package//repo:type}

            {$package//package:title/text()}

            {if ($package//package:java/package:namespace/text()) then (concat('java: ',$package//package:java/package:namespace/text()),
) else () }
            {if ($package//package:xquery/package:namespace/text()) then (concat('xquery: ',$package//package:xquery/package:namespace/text()),
) else () }
            {if ($package//package:xslt/package:import-uri/text()) then (concat('xslt: ',$package//package:xslt/package:import-uri/text()),
) else () }

            {xmldb:created($repo-coll, $file)}
            {$package//repo:author}
            {$package//repo:license}
            {$package//repo:status}

        else
         ()

     }

Application Packages

            Package Name
            Description
            Namespace(s)
            Date Created
            Author
            License
            Status

     {
     let $files := if (collection($repo-coll)) then collection($repo-coll)/util:document-name(.) else ()
     return
       for $file in $files[contains(.,'.xar')]
       let $name := substring-before($file,'.xar')
       let $package := document(concat($repo-coll,'/',$name,'.xml'))
       return
        if ($package//repo:type eq 'application') then

            {$name}

            {if ($package//repo:deploy) then 'Application' else 'Library'}

            {$package//package:title/text()}

            {if ($package//package:java/package:namespace/text()) then (concat('java: ',$package//package:java/package:namespace/text()),
) else () }
            {if ($package//package:xquery/package:namespace/text()) then (concat('xquery: ',$package//package:xquery/package:namespace/text()),
) else () }
            {if ($package//package:xslt/package:import-uri/text()) then (concat('xslt: ',$package//package:xslt/package:import-uri/text()),
) else () }

            {xmldb:created($repo-coll, $file)}
            {$package//repo:author}
            {$package//repo:license}
            {$package//repo:status}

        else
          ()

     }

Add Package

You will require access rights to upload new packages for distribution from this repository.

    <form method="POST" enctype="multipart/form-data">

        <input type="submit" name="action" value="Upload Package"/>
        <input type="file" size="30" name="upload"/>

    Note: You can upload example .xar packages located under webapp/repo/packages
    </form>