La Mengambrea

Sexo, UNIX y rocanrol
2006/03/10

Cómo sumar números mafufos con XSLT

Buenas. Su amigable editor está saliendo de una ligera crisis personal relacionada con la triste vida del asalariado. Para que no se me aburra, y para que mi estado de ánimo general no permee en observaciones sarcásticas y agrias, hoy hablaremos de un viejo tema favorito para el sano entretenimiento familiar: ¡transformaciones de XML con XSLT!

Calma, contenga su entusiasmo. Necesito su atención. Considere un documento de la siguiente forma:
<cuentas>
<saldo>1000</saldo>
<saldo>1500+</saldo>
<saldo>250-</saldo>
<saldo>100</saldo>
<saldo>150+</saldo>
<saldo>25-</saldo>
</cuentas>
Se le solicita una transformación XSL que obtenga la suma de estas cantidades.

Lo primero que salta a la mente, obviamente, es usar la función sum(node-set) de XPath. Digamos, algo como:
<xs:template match="cuentas">
<xs:value-of select="sum(saldo)"/>
</xs:template>
Tan fácil como eso, ¿no es cierto? Es fácil, sí. Lástima que no funciona.

Eso no funciona porque, si recuerda, sum() opera sobre el string-value de cada nodo en el node-set. Y el problema son esos signos más y menos al final del número, que si bien a nuestro mainframe evidentemente le gustan, a XPath le causan indigestión. Es necesario realizar una transformación del contenido de cada nodo antes de poder usarlo con sum(). Digamos,
<xs:template name="numerifica">
<xs:param name="texto"/>
<xs:choose>
<xs:when test="substring($texto, string-length($texto), 1) = '+'">
<xs:value-of select="substring($texto, 1, string-length($texto) - 1)"/>
</xs:when>
<xs:when test="substring($texto, string-length($texto), 1) = '-'">
<xs:value-of
select="concat('-', substring($texto, 1, string-length($texto) - 1))"/>
</xs:when>
<xs:otherwise>
<xs:value-of select="$texto"/>
</xs:otherwise>
</xs:choose>
</xs:template>
y luego aplicar esto al string-value de cada nodo.

¿Y luego? Ah, sin duda ya notó que esto por sí mismo no nos resuelve la vida. Por supuesto, al aplicar una transformación a cada nodo en una colección de nodos, lo que producirá es un result-tree-fragment, no un node-set. Los conceptos tienen sus similitudes, pero el primero lo define XSLT, y el segundo XPath. Y sum(), que es parte de XPath, espera un node-set. Definitivamente no funciona sobre un result-tree-fragment.

La solución final a este pequeño acertijo puede alcanzarse de dos maneras. La primera, burda pero efectiva, es utilizando una extensión del procesador XSLT que convierta un result-tree-fragment en un node-set. Por ejemplo, 4XSLT y Xalan soportan exsl:node-set(result-tree-fragment) (donde el espacio de nombres "exsl" se refiere a http://exslt.org/common). LibXSLT y Saxon soportan esto parcialmente, creo. En cualquier caso, el uso de extensiones por definición no es portable.

La otra manera de solucionar nuestro problema es, por supuesto, implementando nuestro propio sum(). Y aquí es donde todos sus conocimientos de LISP se hacen útiles. Recuerda usted los refinamientos en el tema de recursión? ¿Las funciones tail-recursive y eso? Pues bien, implementemos una:
<xs:template name="suma">
<xs:param name="items"/>
<xs:param name="result" select="0"/>
<xs:choose>
<xs:when test="$items">
<xs:variable name="num">
<xs:call-template name="numerifica">
<xs:with-param name="texto" select="$items[1]"/>
</xs:call-template>
</xs:variable>
<xs:call-template name="suma">
<xs:with-param name="items" select="$items[position() > 1]"/>
<xs:with-param name="result" select="$result + number($num)"/>
</xs:call-template>
</xs:when>
<xs:otherwise>
<xs:value-of select="$result"/>
</xs:otherwise>
</xs:choose>
</xs:template>
Esto, a diferencia del template numerifica, arriba, es código denso. Esto es, hay lógica muy sutil embebida en una función breve y elegante, muy a la manera de LISP. Observe: el template suma recibe un parámetro items, que es un result-tree-fragment, transforma el primer nodo con la transformación numerifica y recurre sobre sí mismo para procesar el resto de los nodos, sumando el nodo transformado con el resultado de la invocación recursiva. El resultado final es la suma de todos los nodos.

Esta función se invoca de esta forma:
<xs:template match="cuentas">
<resultado>
<xs:call-template name="suma">
<xs:with-param name="items" select="saldo"/>
</xs:call-template>
</resultado>
</xs:template>
La forma general de la solución presentada es aplicable en el procesamiento de cualquier tipo de información en la que haya que transformar los elementos de una colección antes de producir un resultado agregado.

Gracias por su atención, ya me siento mucho mejor. Recuerde: si bebe, no transforme XML.

¡Buen fin de semana!

Vínculos: Especificación de XSLT, especificación de XPath, la iniciativa EXSLT (todo en inglés). El código completo presentado en este artículo, ejemplo.xml y suma.xsl (probados con xsltproc).

[Foto: un lector más, satisfecho con este artículo. Fuente: Sol Dust Love's photostream en Flickr! (parte de su colección Men). Copyright © 2005 Sol Dust Love, algunos derechos reservados. El uso de esta fotografía está permitido bajo los términos de la licencia Attribution publicada por Creative Commons.]

Etiquetas: ,

Enlace permanente   - 17:24 - deje un comentario (hay 2)
Anonymous Anónimo dijo:
Joder!!! muchas gracias tío.
Llevaba un buen rato intentando hacer algo parecido y me he basado en lo que tienes en el blog.

Quería realizar el sumatorio de cant*precio para todas las líneas de compra:

<linea-de-compra>
  ....
  <precio>120</precio>
  <cant>5</cant>
  ...
</linea-de-compra>
Blogger Unknown dijo:
Necesito ayuda con algo parecido, yo necesito sumar esta trama '5;5;15;' obviamente ya realice un template para poder sacar uno por uno y a lo que me devuelve uno por uno (*), es que necesito sumarlo y luego mostrar la sumatoria.

(*) al mostrarlo me sale así 5515 pero es por lo que el template me retorna uno por uno.

Agradecería tu ayuda.

Saludos, ten un excelente día.
Haga clic aquí para dejar un comentario
g