Content of math module

xquery version "3.0";

(:
 : 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 a part of the set of functions
 : defined by Microsoft Excel 2003.
 : 
 : @see <a href="http://office.microsoft.com/en-us/excel/CH062528291033.aspx"
 : target="_blank">Excel 2003 Documentation: Math Functions</a>
 :
 : @author Daniel Turcanu
 : @project Zorba/Excel/Math
 :
 :)
module namespace  excel = "http://zorba.io/modules/excel/math" ;

declare namespace excel-err = "http://zorba.io/modules/excel/errors";

declare namespace ver = "http://zorba.io/options/versioning";
declare option ver:module-version "1.0";

(:~
 : 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 excel-err:Value 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(fn:QName("http://zorba.io/modules/excel/errors", "excel-err:Value"), "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 excel-err:Value if arg cannot be casted to numeric type.
 : @example test/Queries/excel/math/abs1.xq
 : @example test/Queries/excel/math/abs2.xq
 : @example test/Queries/excel/math/abs3.xq
 : @example test/Queries/excel/math/abs4.xq
 : @example test/Queries/excel/math/abs5.xq
 : @example test/Queries/excel/math/abs6.xq
 :)
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 excel-err:Value if parameters cannot be casted to numeric type.
 : @error excel-err:Num if significance is zero or it doesn't have the same sign as number.
 : @example test/Queries/excel/math/ceiling1.xq
 : @example test/Queries/excel/math/ceiling2.xq
 : @example test/Queries/excel/math/ceiling3.xq
 : @example test/Queries/excel/math/ceiling4.xq
 : @example test/Queries/excel/math/ceiling5.xq
 : @example test/Queries/excel/math/ceiling6.xq
 : @example test/Queries/excel/math/ceiling7.xq
 :)
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(fn:QName("http://zorba.io/modules/excel/errors", "excel-err:Num"), "Ceiling function does not accept significance 0")
    else if ($num * $sig ge 0) then
	    fn:ceiling($num div $sig) * $sig
    else
      fn:error(fn:QName("http://zorba.io/modules/excel/errors", "excel-err:Num"), "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 excel-err:Value if parameters cannot be casted to numeric type.
 : @example test/Queries/excel/math/even1.xq
 : @example test/Queries/excel/math/even2.xq
 : @example test/Queries/excel/math/even3.xq
 : @example test/Queries/excel/math/even4.xq
 : @example test/Queries/excel/math/even5.xq
 : @example test/Queries/excel/math/even6.xq
 :)
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
};

(:~
 : 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 %private 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 excel-err:Num if the number is smaller than zero
 : @example test/Queries/excel/math/fact1.xq
 : @example test/Queries/excel/math/fact2.xq
 : @example test/Queries/excel/math/fact3.xq
 : @example test/Queries/excel/math/fact4.xq
 : @example test/Queries/excel/math/fact5.xq
:)
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(fn:QName("http://zorba.io/modules/excel/errors", "excel-err:Num"), "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 excel-err:Value if parameters cannot be casted to numeric type.
 : @error excel-err:Num if significance is zero or it doesn't have the same sign as number.
 : @example test/Queries/excel/math/floor1.xq
 : @example test/Queries/excel/math/floor2.xq
 : @example test/Queries/excel/math/floor3.xq
 : @example test/Queries/excel/math/floor4.xq
 : @example test/Queries/excel/math/floor5.xq
:)
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(fn:QName("http://zorba.io/modules/excel/errors", "excel-err:Num"), "Floor function does not accept significance 0")
    else if ($num * $sig ge 0) then
      fn:floor($num div $sig) * $sig
    else
      fn:error(fn:QName("http://zorba.io/modules/excel/errors", "excel-err:Num"), "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 excel-err:Value if parameter cannot be casted to numeric type
 : @example test/Queries/excel/math/int1.xq
 : @example test/Queries/excel/math/int2.xq
 : @example test/Queries/excel/math/int3.xq
 : @example test/Queries/excel/math/int4.xq
:)
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 excel-err:Value if parameters cannot be casted to numeric type.
 : @error excel-err:Div0 if divisor is zero after casting to numeric.
 : @example test/Queries/excel/math/mod1.xq
 : @example test/Queries/excel/math/mod2.xq
 : @example test/Queries/excel/math/mod3.xq
 : @example test/Queries/excel/math/mod4.xq
 :)
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(fn:QName("http://zorba.io/modules/excel/errors", "excel-err:Div0"), "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 excel-err:Value if parameter cannot be casted to numeric type.
 : @example test/Queries/excel/math/odd1.xq
 : @example test/Queries/excel/math/odd2.xq
 : @example test/Queries/excel/math/odd3.xq
 : @example test/Queries/excel/math/odd4.xq
 : @example test/Queries/excel/math/odd5.xq
 : @example test/Queries/excel/math/odd6.xq
 :)
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 excel-err:Value if parameter cannot be casted to numeric type.
 : @error excel-err:Value if power is smaller than zero.
 : @example test/Queries/excel/math/power1.xq
 : @example test/Queries/excel/math/power3.xq
 : @example test/Queries/excel/math/power4.xq
 : @example test/Queries/excel/math/power5.xq
 :)
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(fn:QName("http://zorba.io/modules/excel/errors", "excel-err:Value"), "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)   
 };
 
(:~
 : 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 excel-err:Value if parameters cannot be casted to numeric type.
 :)
declare %private 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 excel-err:Value if parameters cannot be casted to numeric type.
 : @example test/Queries/excel/math/product1.xq
 : @example test/Queries/excel/math/product2.xq
 : @example test/Queries/excel/math/product3.xq
 : @example test/Queries/excel/math/product4.xq
 : @example test/Queries/excel/math/product5.xq
 :)
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 excel-err:Value if parameters cannot be casted to numeric type.
 : @error excel-err:Div0 if denominator casted as numeric type has value zero.
 : @example test/Queries/excel/math/quotient1.xq
 : @example test/Queries/excel/math/quotient2.xq
 : @example test/Queries/excel/math/quotient3.xq
 : @example test/Queries/excel/math/quotient4.xq
 :)
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(fn:QName("http://zorba.io/modules/excel/errors", "excel-err:Div0"), "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 excel-err:Value if parameters cannot be casted to numeric type.
 : @example test/Queries/excel/math/round1.xq
 : @example test/Queries/excel/math/round2.xq
 : @example test/Queries/excel/math/round3.xq
 : @example test/Queries/excel/math/round4.xq
 :)
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 excel-err:Value if parameters cannot be casted to numeric type.
 : @example test/Queries/excel/math/rounddown1.xq
 : @example test/Queries/excel/math/rounddown2.xq
 : @example test/Queries/excel/math/rounddown3.xq
 : @example test/Queries/excel/math/rounddown4.xq
 : @example test/Queries/excel/math/rounddown5.xq
 :)
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 excel-err:Value if parameters cannot be casted to numeric type.
 : @example test/Queries/excel/math/roundup1.xq
 : @example test/Queries/excel/math/roundup2.xq
 : @example test/Queries/excel/math/roundup3.xq
 : @example test/Queries/excel/math/roundup4.xq
 : @example test/Queries/excel/math/roundup5.xq
 :)
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 excel-err:Value if parameters cannot be casted to numeric type.
 : @example test/Queries/excel/math/sign1.xq
 : @example test/Queries/excel/math/sign2.xq
 : @example test/Queries/excel/math/sign3.xq
 :)
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 excel-err:Value if parameters cannot be casted to numeric type.
 : @example test/Queries/excel/math/sum1.xq
 : @example test/Queries/excel/math/sum2.xq
 : @example test/Queries/excel/math/sum3.xq
 :)
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 excel-err:Value if parameter cannot be casted to numeric type.
 : @example test/Queries/excel/math/trunc1.xq
 : @example test/Queries/excel/math/trunc2.xq
 :)
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 excel-err:Value if parameter cannot be casted to numeric type.
 : @example test/Queries/excel/math/trunc3.xq
 :)
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.<p/>
 : 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 excel-err:Value 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
};

(:~
 : 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 * 180 div excel:pi()) 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 excel-err:Num if the number is negative.
 : @example test/Queries/excel/math/priority1/factdouble1.xq
 : @example test/Queries/excel/math/priority1/factdouble2.xq
 :)
declare function excel:factdouble($number as xs:integer) as xs:integer
{
  if ($number lt 0) then
    fn:error(fn:QName("http://zorba.io/modules/excel/errors", "excel-err:Num"), "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) 
};

(:~
 : 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 %private 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
};

(:~
 : 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 %private 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)
};

(:~
 : 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 %private 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 excel-err:Num if any number is smaller than zero.
 : @example test/Queries/excel/math/priority1/gcd1.xq
 : @example test/Queries/excel/math/priority1/gcd2.xq
 : @example test/Queries/excel/math/priority1/gcd3.xq
 : @example test/Queries/excel/math/priority1/gcd4.xq
 : @example test/Queries/excel/math/priority1/gcd5.xq
 : @example test/Queries/excel/math/priority1/gcd6.xq
 : @example test/Queries/excel/math/priority1/gcd7.xq
 : @example test/Queries/excel/math/priority1/gcd8.xq
 :)
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(fn:QName("http://zorba.io/modules/excel/errors", "excel-err:Num"), "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.<p/>
 : LCM for two numbers is computed by multiplying them and dividing with GCD. <p/>
 : The function is applied recursively replacing the first two numbers in the sequence with their LCM.
 : 
 : @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 excel-err:Num if any number is smaller than zero.
 : @example test/Queries/excel/math/priority1/lcm1.xq
 : @example test/Queries/excel/math/priority1/lcm2.xq
 : @example test/Queries/excel/math/priority1/lcm3.xq
 : @example test/Queries/excel/math/priority1/lcm4.xq
 : @example test/Queries/excel/math/priority1/lcm5.xq
 :)
declare function excel:lcm($numbers as xs:integer+) as xs:integer
{
  if(count($numbers) eq 1) then
    $numbers[1]
  else
  if(count($numbers) eq 2) then
    let $product := excel:product(fn:distinct-values($numbers))
    return
      if ($product eq 0) then
        0
      else
        $product idiv excel:gcd($numbers)
  else
    excel:lcm((excel:lcm(($numbers[1], $numbers[2])), subsequence($numbers, 3)))

};

(:~
 : 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 excel-err:Value if parameters cannot be casted to numeric type.
 : @example test/Queries/excel/math/priority1/mround1.xq
 : @example test/Queries/excel/math/priority1/mround2.xq
 : @example test/Queries/excel/math/priority1/mround3.xq
 :)
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.
 : @example test/Queries/excel/math/priority1/radians1.xq
 : @example test/Queries/excel/math/priority1/radians2.xq
 : @example test/Queries/excel/math/priority1/radians3.xq
 :)
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).<p/>
 : M is the largest digit, it represents 1000.
 : Numbers bigger than 2000 will be represented by a sequence of "M".<p/>
 : 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 excel-err:Num if the input integer is negative 
 : @example test/Queries/excel/math/priority1/roman1.xq
 : @example test/Queries/excel/math/priority1/roman2.xq
 : @example test/Queries/excel/math/priority1/roman3.xq
 :)
declare function excel:roman($number as xs:integer) as xs:string
{
  if ($number lt 0) then
    fn:error(fn:QName("http://zorba.io/modules/excel/errors", "excel-err:Num"), "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
    ""
};