0

I have the following xml file:

<Contract>
    <TotalCosts>31003</TotalCosts>
    <PaymentSchedules>
        <PaymentSchedule>
            <Amount>516.7</Amount>
            <NumberOfPayments>3</NumberOfPayments>
        </PaymentSchedule>
        <PaymentSchedule>
            <Amount>529.7</Amount>
            <NumberOfPayments>1</NumberOfPayments>
        </PaymentSchedule>
        <PaymentSchedule>
            <Amount>516.7</Amount>
            <NumberOfPayments>55</NumberOfPayments>
        </PaymentSchedule>
        <PaymentSchedule>
            <Amount>504.70</Amount>
            <NumberOfPayments>1</NumberOfPayments>
        </PaymentSchedule>
    </PaymentSchedules>
</Contract>

In my schematron file, I would like to make a TotalCosts are the same as the amount in the paymentSchedule.

For that, I need to do the following for each PaymentSchedule:

Amount * NumberOfPayments

After that, I need to take the sum of all PaymentSchedules, and that number should be exactly the same. If you try this for the given example, you will see that the amounts are exactly the same.

To validate this in schematron, I created this schematron file:

<iso:schema xmlns:iso="http://purl.oclc.org/dsdl/schematron"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" queryBinding="xslt2">
    <iso:ns uri="http://www.someurl.com" prefix="func"/>

    <xsl:function name="func:getPaymentScheduleTotal">
        <xsl:param name="contract" as="element()"/>
        <xsl:value-of
            select="sum($contract/PaymentSchedules/PaymentSchedule/(NumberOfPayments * Amount))"/>
    </xsl:function>

    <iso:pattern id="Contract">
        <iso:rule context="Contract">
            <iso:assert test="func:getPaymentScheduleTotal(.) = number(TotalCosts)">amount in paymentschedule(<iso:value-of select="func:getPaymentScheduleTotal(.)"/>)    doesn't match total amount(<iso:value-of select="TotalCosts"/>)</iso:assert>
        </iso:rule>
    </iso:pattern>

</iso:schema>

But this is where I'm running into a problem. I get the following validation result:

amount in paymentschedule(31003.000000000004) doesn't match total amount(31003) (func:getPaymentScheduleTotal(.) = number(TotalCosts)) [assert]

I can't figure out where the .000000000004 is coming from. I can of course use something like floor to round the number, but I believe the .000000000004 shouldn't be there in the first place.

Any ideas?

ErikL
  • 2,031
  • 6
  • 34
  • 57
  • That looks like a rounding error coming from treating the values as floating point numbers. If you can treat the numbers as `xs:decimal` then it should use arbitrary precision arithmetic rather than floating point. – Ian Roberts Feb 19 '13 at 15:34

2 Answers2

3

The problem that you are experiencing is an expected behavior of how double variables work. The XSLT processor is interpreting Amount and NumberOfPayments as xs:double data types.

As you probably know, xs:double is represented with 64 bits and follows the IEEE754 standard (the specification can be found http://grouper.ieee.org/groups/754/). The main issue is that some numbers cannot be represented as an xs:double so those numbers are represented as the nearest number that can be represented (sometimes it is an intermediate result of an arithmetic operation which is the one that cannot be represented).

However there is a solution for your problem. Instead of using xs:double (which uses an exponent to store the number) you can use xs:decimal which according to the data type specification cannot contain an exponent so it must store the number as it is. So, replacing the value-of expression with

    <xsl:value-of select="sum($contract/PaymentSchedules/PaymentSchedule/(xs:decimal(Amount) * xs:decimal(NumberOfPayments)))" />

should work as expected.

UPDATE: The complete solution would be:

<iso:schema xmlns:iso="http://purl.oclc.org/dsdl/schematron"
            xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
            xmlns:xsl="http://www.w3.org/2001/XMLSchema" queryBinding="xslt2">

    <iso:ns uri="http://www.someurl.com" prefix="func"/>

    <xsl:function name="func:getPaymentScheduleTotal" as="xs:decimal">
        <xsl:param name="contract" as="element()"/>
        <xsl:sequence
            select="sum($contract/PaymentSchedules/PaymentSchedule/(xs:decimal(NumberOfPayments) * xs:decimal(Amount)))"/>
    </xsl:function>

    <iso:pattern id="Contract">
        <iso:rule context="Contract">
            <iso:assert test="func:getPaymentScheduleTotal(.) = xs:decimal(TotalCosts)">amount in paymentschedule(<iso:value-of select="func:getPaymentScheduleTotal(.)"/>)    doesn't match total amount(<iso:value-of select="TotalCosts"/>)</iso:assert>
        </iso:rule>
    </iso:pattern>

</iso:schema>
Pablo Pozo
  • 1,880
  • 13
  • 9
  • You'll also need to declare the `xs` prefix, and use the same type cast in the assertion (`func:getPaymentScheduleTotal(.) = xs:decimal(TotalCosts)`) – Ian Roberts Feb 19 '13 at 15:37
  • I have added the complete solution to my answer. To be completely accurate he would need to change also the result type of the function by adding as="xs:decimal" to the function and returning a typed element using xsl:sequence instead of returning a text node. Thanks for your suggestion :) – Pablo Pozo Feb 19 '13 at 15:51
  • This works for the given example. However, IMHO the solution for dealing with input where you expect an arbitrary number of decimal places is to not test against an exact equality like func:getPaymentScheduleTotal(.) = xs:decimal(TotalCosts).So rounding and testing against an interval is the better solution. – Clemens Feb 19 '13 at 16:56
  • Well in this case the amounts are prices so probably he is not expecting an arbitrary long number. Also the xs:decimal implementation depends on the XSLT processor(according to the specification they have to support at least 18 decimal digits) so some processors like SAXON supports unlimited precision (http://saxonica.com/documentation9.4-demo/html/conformance/xpath20.html). I agree with you that if you do not take into account the current example (I think my solution is better for the code that OP posted)and the code is not targeted for a specific XSLT processor, then your solution is better. – Pablo Pozo Feb 20 '13 at 11:47
  • The numbers are indeed prices, so I will only expect a maximum of 2 decimals. Your solutions seems to work fine for my problem. Thanks for the solution and the explanation. – ErikL Feb 20 '13 at 12:25
  • Fair enough. Just wanted to say that testing for equal values can have its pitfalls. – Clemens Feb 21 '13 at 17:04
0

sounds like that the decimal representation (e.g. 516.7, 529.7 ...) is finite but the binary representation of the same value might not be. Therefore you get the differences in the result. I would say that there is no other option beside rounding the results.

Hope this helps.

Clemens
  • 1,744
  • 11
  • 20