Created
September 7, 2025 00:53
-
-
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)"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- | |
| 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 <= 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 <= 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 <= 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 <= 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