daviskren
daviskren

Reputation: 1

How to use xquery to perform four arithmetic operations on xml nodes

<Expression>
            <description/>
            <minus>
                <plus>
                    <int value="2"/>
                    <int value="3"/>
                    <times>
                        <int value="5"/>
                        <minus>
                            <int value="6"/>
                            <int value="3"/>
                        </minus>
                    </times>
                </plus>
                <int value="4"/>
            </minus>
        </Expression>

I want to get expression like this and output the result:(2+3+(6-3)*5)-4=16

Upvotes: 0

Views: 335

Answers (2)

Michael Kay
Michael Kay

Reputation: 163342

This is a good use case for higher-order functions:

declare variable $evaluators := map {
   'int' : function($e){xs:integer($e/@value)},
   'plus' : function($e){sum($e/*/local:eval(.)},
   'minus' : function($e){local:eval($e/*[1] - local:eval($e/*[2])},
   'times' : function($e){local:eval($e/*[1] * local:eval($e/*[2])},
   ...
}
declare function local:eval($e as element(*)) {
   $evaluators(name($e))($e)
}
local:eval(Expression/(* except description))

The details depend a bit on aspects of the specification that you haven't shared with us, for example whether description can appear anywhere, whether times (like plus) can take more than two operands, etc.

Acknowledgements: I saw this problem solved in 1972 using Algol 68 by my late friend and colleague Andrew Birrell (https://cacm.acm.org/news/210689-in-memoriam-andrew-birrell-1951-2016/fulltext) and it was this problem and solution that convinced me of the value of treating functions as first-class objects.

Upvotes: 2

Martin Honnen
Martin Honnen

Reputation: 167571

Here is an XSLT 4 example as runnable by SaxonCS from Saxonica:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="4.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="#all"
  expand-text="yes">
  
  <xsl:output method="text"/>

  <xsl:mode on-no-match="shallow-skip"/>
  
  <xsl:strip-space elements="*"/>
  
  <xsl:param name="operator-map" as="map(xs:string, xs:string)"
    select="map { 'minus' : ' - ',
                  'plus' : ' + ',
                  'times' : ' * ' }"/>

  <xsl:template match="Expression">
    <xsl:variable name="expression">
      <xsl:apply-templates/>
    </xsl:variable>
    <xsl:variable name="result" as="xs:decimal">
      <xsl:evaluate xpath="$expression"/>
    </xsl:variable>
    <xsl:value-of select="$expression, $result" separator=" = "/>
  </xsl:template>
  
  <xsl:template match="minus | plus | times">
    <xsl:text>(</xsl:text>
    <xsl:apply-templates separator="{$operator-map(local-name())}"/>
    <xsl:text>)</xsl:text>
  </xsl:template>
  
  <xsl:template match="int[@value]">
    <xsl:value-of select="@value"/>
  </xsl:template>

</xsl:stylesheet>

Or as XSLT 3:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="3.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="#all"
  expand-text="yes">
  
  <xsl:output method="text"/>

  <xsl:mode on-no-match="shallow-skip"/>
  
  <xsl:strip-space elements="*"/>
  
  <xsl:param name="operator-map" as="map(xs:string, xs:string)"
    select="map { 'minus' : ' - ',
                  'plus' : ' + ',
                  'times' : ' * ' }"/>

  <xsl:template match="Expression">
    <xsl:variable name="expression">
      <xsl:apply-templates/>
    </xsl:variable>
    <xsl:variable name="result" as="xs:decimal">
      <xsl:evaluate xpath="$expression"/>
    </xsl:variable>
    <xsl:value-of select="$expression, $result" separator=" = "/>
  </xsl:template>
  
  <xsl:template match="minus | plus | times">
    <xsl:if test="position() gt 1"> {$operator-map(local-name(..))} </xsl:if>
    <xsl:text>(</xsl:text>
    <xsl:apply-templates/>
    <xsl:text>)</xsl:text>
  </xsl:template>
  
  <xsl:template match="int[@value]">
    <xsl:if test="position() gt 1"> {$operator-map(local-name(..))} </xsl:if>
    <xsl:value-of select="@value"/>
  </xsl:template>

</xsl:stylesheet>

Solely evaluation in XQuery (3) perhaps as

declare function local:evaluate($expression)
{
  local:evaluate-operator($expression/(* except description))
};

declare function local:evaluate-operator($operator)
{
  typeswitch ($operator)
    case element(minus)
      return fold-left(tail($operator/*), local:evaluate-operator(head($operator/*)), function($a, $op) { $a - local:evaluate-operator($op) })
    case element(plus)
      return sum($operator/*!local:evaluate-operator(.))
    case element(times)
      return fold-left($operator/*, 1, function($a, $op) { $a * local:evaluate-operator($op) })
    case element(int)
      return xs:integer($operator/@value)
    default return ()
};

local:evaluate(Expression)

Upvotes: 1

Related Questions