Skip to content

Instantly share code, notes, and snippets.

@adashrod
Created September 7, 2025 00:53
Show Gist options
  • Select an option

  • Save adashrod/4ac039b6310f409cfe4de32f1914be6f to your computer and use it in GitHub Desktop.

Select an option

Save adashrod/4ac039b6310f409cfe4de32f1914be6f to your computer and use it in GitHub Desktop.
An XSLT function to parse strings as decimal numbers that might have extra chars like "$100", "50%", "(+33)"
<!--
Extracts a number from a string, ignoring invalid characters. If a string contains multiple numbers, the first
will be returned. If no number can be extracted, this can be detected like: test="not(normalize-space($result))"
@param {string} input a string to parse for a number
-->
<xsl:function name="fun:LenientParseDecimal">
<xsl:param name="input"/>
<xsl:call-template name="lenientParseDecimalHelper">
<xsl:with-param name="str" select="$input"/>
</xsl:call-template>
</xsl:function>
<xsl:template name="lenientParseDecimalHelper">
<xsl:param name="str"/> <!-- a string to parse -->
<xsl:param name="index" select="1"/> <!-- current index being scanned -->
<xsl:param name="signFound" select="false()"/> <!-- true once an actual plus or minus sign have been found -->
<xsl:param name="dotFound" select="false()"/> <!-- true once an actual decimal place has been found -->
<xsl:param name="capNums" select="false()"/> <!-- true once this has started capturing digits -->
<xsl:param name="quit" select="false()"/> <!-- true if any invalid input has been encountered to short-circuit further iterations -->
<xsl:if test="$index &lt;= string-length($str) and not($quit)">
<xsl:variable name="ch" select="substring($str, $index, 1)"/>
<xsl:variable name="nextIsDigit"><!-- true if the char at index+1 exists and is a digit -->
<xsl:choose>
<xsl:when test="$index + 1 &lt;= string-length($str)">
<xsl:value-of select="matches(substring($str, $index + 1, 1), '\d')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="false()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="nextIsDot"><!-- true if the char at index+1 exists and is a dot -->
<xsl:choose>
<xsl:when test="$index + 1 &lt;= string-length($str)">
<xsl:value-of select="substring($str, $index + 1, 1) = '.'"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="false()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:when test="$ch = '-' or $ch = '+'">
<xsl:choose>
<xsl:when test="$nextIsDigit = true() and not($signFound) and not($capNums)">
<xsl:value-of select="$ch"/>
<xsl:call-template name="lenientParseDecimalHelper">
<xsl:with-param name="str" select="$str"/>
<xsl:with-param name="index" select="$index + 1"/>
<xsl:with-param name="signFound" select="true()"/>
<xsl:with-param name="dotFound" select="$dotFound"/>
<xsl:with-param name="capNums" select="$capNums"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="$nextIsDot = true() and not($signFound) and not($capNums)">
<xsl:variable name="nextAreDotAndDigit"><!-- true if the chars at index+1 and index+2 are a dot and a digit, used for patterns like -.45 -->
<xsl:choose>
<xsl:when test="$index + 2 &lt;= fn:string-length($str)">
<xsl:value-of select="matches(substring($str, $index + 2, 1), '\d')"/>
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:when test="$nextAreDotAndDigit = true()">
<xsl:value-of select="$ch"/><!-- plus or minus -->
<xsl:value-of select="'.'"/>
<xsl:call-template name="lenientParseDecimalHelper">
<xsl:with-param name="str" select="$str"/>
<xsl:with-param name="index" select="$index + 2"/>
<xsl:with-param name="signFound" select="true()"/>
<xsl:with-param name="dotFound" select="true()"/>
<xsl:with-param name="capNums" select="true()"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="lenientParseDecimalHelper">
<xsl:with-param name="str" select="$str"/>
<xsl:with-param name="index" select="$index + 2"/>
<xsl:with-param name="signFound" select="$signFound"/>
<xsl:with-param name="dotFound" select="$dotFound"/>
<xsl:with-param name="capNums" select="$capNums"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="lenientParseDecimalHelper">
<xsl:with-param name="str" select="$str"/>
<xsl:with-param name="index" select="$index + 1"/>
<xsl:with-param name="signFound" select="$signFound"/>
<xsl:with-param name="dotFound" select="$dotFound"/>
<xsl:with-param name="capNums" select="$capNums"/>
<xsl:with-param name="quit" select="$capNums"/><!-- if capNums and found a sign: done -->
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="$ch = '.'">
<xsl:choose>
<xsl:when test="$nextIsDigit = true() and not($dotFound)">
<xsl:value-of select="$ch"/>
<xsl:call-template name="lenientParseDecimalHelper">
<xsl:with-param name="str" select="$str"/>
<xsl:with-param name="index" select="$index + 1"/>
<xsl:with-param name="signFound" select="$signFound"/>
<xsl:with-param name="dotFound" select="true()"/>
<xsl:with-param name="capNums" select="true()"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="lenientParseDecimalHelper">
<xsl:with-param name="str" select="$str"/>
<xsl:with-param name="index" select="$index + 1"/>
<xsl:with-param name="signFound" select="$signFound"/>
<xsl:with-param name="dotFound" select="$dotFound"/>
<xsl:with-param name="capNums" select="$capNums"/>
<xsl:with-param name="quit" select="$dotFound"/><!-- found a dot and we've previously already found the actual decimal place: done -->
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:when test="matches($ch, '\d')">
<xsl:value-of select="$ch"/>
<xsl:call-template name="lenientParseDecimalHelper">
<xsl:with-param name="str" select="$str"/>
<xsl:with-param name="index" select="$index + 1"/>
<xsl:with-param name="signFound" select="$signFound"/>
<xsl:with-param name="dotFound" select="$dotFound"/>
<xsl:with-param name="capNums" select="true()"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="lenientParseDecimalHelper">
<xsl:with-param name="str" select="$str"/>
<xsl:with-param name="index" select="$index + 1"/>
<xsl:with-param name="signFound" select="$signFound"/>
<xsl:with-param name="dotFound" select="$dotFound"/>
<xsl:with-param name="capNums" select="$capNums"/>
<xsl:with-param name="quit" select="$capNums"/><!-- capturing numbers and find anything not in +-.\d: done -->
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment