使用 XSLT 计算同级元素的值总和
Calculating the sum of values from sibling elements using XSLT
我正在使用 XSLT 进行测量值的自动转换。从一个系统(e.q.英制)到另一个系统(例如公制)的单个测量值的转换工作正常。但是英制尺寸可能采用“5 英尺 10 英寸”的形式,我想将其转换为单个公制值。
在我的 XML 模型中,我通过允许单个值或多个子节点来适应此类组合测量。因此,当我发现我有子节点时,我需要将这些子元素中的每一个都转换为公制单位,然后将这些值相加以获得一个单独的公制结果。
我正在努力寻找处理多个子节点并将结果值相加的最佳方法。在迭代语言中,我只是从第一个处理到下一个并更新一个全局变量,但在 XSLT 中,我不知道是否存在可以从对同一模板的后续调用中更新的全局变量。
这是一个(简化的)转换 - 这个转换只处理 [ft_i] 和 [in_i] 到 m.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="2.0">
<xsl:output method="xml" encoding="UTF-8"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*,node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="measurement">
<xsl:copy>
<xsl:choose>
<xsl:when test="measurement">
<xsl:apply-templates select="*"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="normalise">
<xsl:with-param name="val" as="xs:double" select="number(text())"/>
<xsl:with-param name="unitin" select="@ucum"/>
<xsl:with-param name="count" as="xs:integer" select="1"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:copy>
</xsl:template>
<xsl:template name="normalise">
<xsl:param name="val" as="xs:double"/>
<xsl:param name="unitin"/>
<xsl:param name="count" as="xs:integer"/>
<xsl:choose>
<xsl:when test="$unitin eq '[ft_i]'">
<xsl:attribute name="ucum">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:attribute name="unit">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:value-of select="$val * 0.3048"/>
</xsl:when>
<xsl:when test="$unitin eq '[in_i]'">
<xsl:attribute name="ucum">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:attribute name="unit">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:value-of select="$val * 0.0254"/>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
一个简单的测试文件:
<topic>
<p>This piece is
<measurement>
<measurement unit="ft"
ucum=" [ft_i]">10</measurement>
<measurement unit="in"
ucum="[in_i]">2</measurement>
</measurement>
long
</p>
</topic>
变换给出了这个:
<topic>
<p>This piece is
<measurement>
<measurement ucum="m"
unit="m">3.048</measurement>
<measurement ucum="m"
unit="m">0.0508</measurement>
</measurement>
long
</p>
</topic>
显然,我想看到这个:
<topic>
<p>This piece is
<measurement ucum="m"
unit="m">3.0988</measurement>
long
</p>
</topic>
我可以在子测量节点上使用 xsl:for-each,但是如何将单独的值添加到全局值,然后可以从主模板输出该值?
假设某些值和属性是常量,最简单的方法是使用复杂的 XPath-2.0 表达式。然后您可以将 measurement
模板缩减为:
<xsl:template match="measurement">
<measurement ucum="m" unit="m">
<xsl:copy-of select="sum(for $x in measurement return if (normalize-space($x/@ucum)= '[ft_i]') then xs:double(normalize-space($x))*0.3048 else xs:double(normalize-space($x))*0.0254)" />
</measurement>
</xsl:template>
它假定属性保持不变并且只有两个单元。但您可以轻松扩展它。
根据您的做法构建模板,也可以采用以下方案:
<xsl:template match="measurement">
<xsl:copy>
<xsl:variable name="summary">
<xsl:for-each select="measurement">
<val>
<xsl:call-template name="normalise">
<xsl:with-param name="val" as="xs:double" select="number(.)"/>
<xsl:with-param name="unitin" select="@ucum"/>
<xsl:with-param name="count" as="xs:integer" select="1"/>
</xsl:call-template>
</val>
</xsl:for-each>
</xsl:variable>
<xsl:copy-of select="$summary/val[1]/@*" />
<xsl:copy-of select="sum($summary/val)" />
</xsl:copy>
</xsl:template>
<xsl:template name="normalise">
<xsl:param name="val" as="xs:double"/>
<xsl:param name="unitin"/>
<xsl:param name="count" as="xs:integer"/>
<xsl:choose>
<xsl:when test="normalize-space($unitin) = '[ft_i]'">
<xsl:attribute name="ucum">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:attribute name="unit">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:value-of select="$val * 0.3048"/>
</xsl:when>
<xsl:when test="normalize-space($unitin) = '[in_i]'">
<xsl:attribute name="ucum">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:attribute name="unit">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:value-of select="$val * 0.0254"/>
</xsl:when>
</xsl:choose>
</xsl:template>
它更灵活,对变量使用两阶段方法。
结果是一样的。我想如果你把两者结合起来,你会找到一个适合你需要的好方法。
不需要多遍处理 -- XPath 2.0 对这类问题已经足够强大了:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="measurement[*]">
<measurement ucum="m" unit="m">
<xsl:value-of select=
"sum(measurement/(. * (if(@ucum eq '[ft_i]') then 0.3048 else 0.0254)))"/>
</measurement>
</xsl:template>
</xsl:stylesheet>
当此转换应用于提供的 XML 文档时:
<topic>
<p>This piece is
<measurement>
<measurement unit="ft"
ucum="[ft_i]">10</measurement>
<measurement unit="in"
ucum="[in_i]">2</measurement>
</measurement>
long
</p>
</topic>
产生了想要的正确结果:
<topic>
<p>This piece is
<measurement ucum="m" unit="m">3.0988</measurement>
long
</p>
</topic>
二.更通用的解决方案
可以生成更通用的解决方案,其中将转换隔离到单独的 XSLT <xsl:function>
。请注意,即使在转换逻辑非常复杂以至于无法像上述解决方案那样用单个 XPath 表达式 表达的情况下,此解决方案也能正常工作。如果转换不仅仅是简单的乘法,那么一般不可能将其呈现为 table 并使用 xsl:key
:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:f="my:f" exclude-result-prefixes="f">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="measurement[*]">
<measurement ucum="m" unit="m">
<xsl:value-of select="sum(measurement/f:convert(@ucum, .))"/>
</measurement>
</xsl:template>
<xsl:function name="f:convert">
<xsl:param name="pFromUnit"/>
<xsl:param name="pValue"/>
<xsl:value-of select=
"$pValue * (if($pFromUnit eq '[ft_i]') then 0.3048 else 0.0254)"/>
</xsl:function>
</xsl:stylesheet>
当此转换应用于同一个 XML 文档(上图)时,将再次生成相同的正确、想要的结果:
<topic>
<p>This piece is
<measurement ucum="m" unit="m">3.0988</measurement>
long
</p>
</topic>
假设您可以拥有不止 2 个单位,将它们存储在一个变量中并使用 key:
检索相应的转换因子会很方便
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="unit" match="unit" use="@name" />
<xsl:variable name="units">
<unit name="ft" factor=".3048"/>
<unit name="in" factor=".0254"/>
<!-- add more units here ... -->
</xsl:variable>
<!-- identity transform -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="measurement[measurement]">
<measurement ucum="m" unit="m">
<xsl:value-of select="sum(measurement/(. * key('unit', @unit, $units)/@factor))"/>
</measurement>
</xsl:template>
</xsl:stylesheet>
我正在使用 XSLT 进行测量值的自动转换。从一个系统(e.q.英制)到另一个系统(例如公制)的单个测量值的转换工作正常。但是英制尺寸可能采用“5 英尺 10 英寸”的形式,我想将其转换为单个公制值。
在我的 XML 模型中,我通过允许单个值或多个子节点来适应此类组合测量。因此,当我发现我有子节点时,我需要将这些子元素中的每一个都转换为公制单位,然后将这些值相加以获得一个单独的公制结果。
我正在努力寻找处理多个子节点并将结果值相加的最佳方法。在迭代语言中,我只是从第一个处理到下一个并更新一个全局变量,但在 XSLT 中,我不知道是否存在可以从对同一模板的后续调用中更新的全局变量。
这是一个(简化的)转换 - 这个转换只处理 [ft_i] 和 [in_i] 到 m.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="2.0">
<xsl:output method="xml" encoding="UTF-8"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*,node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="measurement">
<xsl:copy>
<xsl:choose>
<xsl:when test="measurement">
<xsl:apply-templates select="*"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="normalise">
<xsl:with-param name="val" as="xs:double" select="number(text())"/>
<xsl:with-param name="unitin" select="@ucum"/>
<xsl:with-param name="count" as="xs:integer" select="1"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:copy>
</xsl:template>
<xsl:template name="normalise">
<xsl:param name="val" as="xs:double"/>
<xsl:param name="unitin"/>
<xsl:param name="count" as="xs:integer"/>
<xsl:choose>
<xsl:when test="$unitin eq '[ft_i]'">
<xsl:attribute name="ucum">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:attribute name="unit">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:value-of select="$val * 0.3048"/>
</xsl:when>
<xsl:when test="$unitin eq '[in_i]'">
<xsl:attribute name="ucum">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:attribute name="unit">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:value-of select="$val * 0.0254"/>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
一个简单的测试文件:
<topic>
<p>This piece is
<measurement>
<measurement unit="ft"
ucum=" [ft_i]">10</measurement>
<measurement unit="in"
ucum="[in_i]">2</measurement>
</measurement>
long
</p>
</topic>
变换给出了这个:
<topic>
<p>This piece is
<measurement>
<measurement ucum="m"
unit="m">3.048</measurement>
<measurement ucum="m"
unit="m">0.0508</measurement>
</measurement>
long
</p>
</topic>
显然,我想看到这个:
<topic>
<p>This piece is
<measurement ucum="m"
unit="m">3.0988</measurement>
long
</p>
</topic>
我可以在子测量节点上使用 xsl:for-each,但是如何将单独的值添加到全局值,然后可以从主模板输出该值?
假设某些值和属性是常量,最简单的方法是使用复杂的 XPath-2.0 表达式。然后您可以将 measurement
模板缩减为:
<xsl:template match="measurement">
<measurement ucum="m" unit="m">
<xsl:copy-of select="sum(for $x in measurement return if (normalize-space($x/@ucum)= '[ft_i]') then xs:double(normalize-space($x))*0.3048 else xs:double(normalize-space($x))*0.0254)" />
</measurement>
</xsl:template>
它假定属性保持不变并且只有两个单元。但您可以轻松扩展它。
根据您的做法构建模板,也可以采用以下方案:
<xsl:template match="measurement">
<xsl:copy>
<xsl:variable name="summary">
<xsl:for-each select="measurement">
<val>
<xsl:call-template name="normalise">
<xsl:with-param name="val" as="xs:double" select="number(.)"/>
<xsl:with-param name="unitin" select="@ucum"/>
<xsl:with-param name="count" as="xs:integer" select="1"/>
</xsl:call-template>
</val>
</xsl:for-each>
</xsl:variable>
<xsl:copy-of select="$summary/val[1]/@*" />
<xsl:copy-of select="sum($summary/val)" />
</xsl:copy>
</xsl:template>
<xsl:template name="normalise">
<xsl:param name="val" as="xs:double"/>
<xsl:param name="unitin"/>
<xsl:param name="count" as="xs:integer"/>
<xsl:choose>
<xsl:when test="normalize-space($unitin) = '[ft_i]'">
<xsl:attribute name="ucum">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:attribute name="unit">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:value-of select="$val * 0.3048"/>
</xsl:when>
<xsl:when test="normalize-space($unitin) = '[in_i]'">
<xsl:attribute name="ucum">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:attribute name="unit">
<xsl:value-of select="'m'"/>
</xsl:attribute>
<xsl:value-of select="$val * 0.0254"/>
</xsl:when>
</xsl:choose>
</xsl:template>
它更灵活,对变量使用两阶段方法。 结果是一样的。我想如果你把两者结合起来,你会找到一个适合你需要的好方法。
不需要多遍处理 -- XPath 2.0 对这类问题已经足够强大了:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="measurement[*]">
<measurement ucum="m" unit="m">
<xsl:value-of select=
"sum(measurement/(. * (if(@ucum eq '[ft_i]') then 0.3048 else 0.0254)))"/>
</measurement>
</xsl:template>
</xsl:stylesheet>
当此转换应用于提供的 XML 文档时:
<topic>
<p>This piece is
<measurement>
<measurement unit="ft"
ucum="[ft_i]">10</measurement>
<measurement unit="in"
ucum="[in_i]">2</measurement>
</measurement>
long
</p>
</topic>
产生了想要的正确结果:
<topic>
<p>This piece is
<measurement ucum="m" unit="m">3.0988</measurement>
long
</p>
</topic>
二.更通用的解决方案
可以生成更通用的解决方案,其中将转换隔离到单独的 XSLT <xsl:function>
。请注意,即使在转换逻辑非常复杂以至于无法像上述解决方案那样用单个 XPath 表达式 表达的情况下,此解决方案也能正常工作。如果转换不仅仅是简单的乘法,那么一般不可能将其呈现为 table 并使用 xsl:key
:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:f="my:f" exclude-result-prefixes="f">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="measurement[*]">
<measurement ucum="m" unit="m">
<xsl:value-of select="sum(measurement/f:convert(@ucum, .))"/>
</measurement>
</xsl:template>
<xsl:function name="f:convert">
<xsl:param name="pFromUnit"/>
<xsl:param name="pValue"/>
<xsl:value-of select=
"$pValue * (if($pFromUnit eq '[ft_i]') then 0.3048 else 0.0254)"/>
</xsl:function>
</xsl:stylesheet>
当此转换应用于同一个 XML 文档(上图)时,将再次生成相同的正确、想要的结果:
<topic>
<p>This piece is
<measurement ucum="m" unit="m">3.0988</measurement>
long
</p>
</topic>
假设您可以拥有不止 2 个单位,将它们存储在一个变量中并使用 key:
检索相应的转换因子会很方便XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="unit" match="unit" use="@name" />
<xsl:variable name="units">
<unit name="ft" factor=".3048"/>
<unit name="in" factor=".0254"/>
<!-- add more units here ... -->
</xsl:variable>
<!-- identity transform -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="measurement[measurement]">
<measurement ucum="m" unit="m">
<xsl:value-of select="sum(measurement/(. * key('unit', @unit, $units)/@factor))"/>
</measurement>
</xsl:template>
</xsl:stylesheet>