10101010101001

0101010101001

xsl number format localization

leave a comment »

These days I’ve had to do some xslt stylesheets that are processed by ibex pdf creator.
One of the things that I found little information on was how to format a number based on current regional settings of the user.
Example: I have the decimal number 1234.56.
What I want is that if the current regional settings point to English (US) culture then display 1,234.56 and if the current regional settings point to some other culture, let’s say Romanian culture, then display 1.234,56

Approach 1) – That doesn’t work.
Initially I thought I could use the built-in format-number function to format numbers like this:

  <xsl:decimal-format name="en-US" decimal-separator="." grouping-separator="," />
  <xsl:decimal-format name="ro-RO" decimal-separator="," grouping-separator=" " />
....
  <xsl:variable name="df" select="//NumberDecimalSeparator/."></xsl:variable>
...
 <xsl:value-of select='format-number(1234.56, "###,##0.00",$df)' />

I would only have to serialize from the calling program an element called NumberDecimalSeparator that would contain the correct format name.
But the xslt decimal format cannot be dynamic and the xslt processor complains about it :(
So this approach is no use.

Approach 2) – Works but it’s ugly

You can have a static decimal format and modify it from the calling program just before calling your xslt processor.
In my case before calling ibex from C# I had to modify the following xml called ‘DecimalSeparators.xsl’:

 <?xml version="1.0" ?>
 <xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

   <xsl:decimal-format name="i" decimal-separator="." grouping-separator="," />
  </xsl:stylesheet>

And our initial xsl looks like this.

  <xsl:include href="DecimalSeparators.xsl"/>
  <xsl:variable name="ts" select="//NumberGroupSeparator/."></xsl:variable>
  <xsl:variable name="ds" select="//NumberDecimalSeparator/."></xsl:variable>

  <xsl:value-of select="format-number(1234.56, concat('###',$ts,'##0',$ds,'00'),'i')" />

So because we’re modifying the DecimalSeparators.xsl before runtime with the correct separators and because we’re also inserting the correct separators in the input xml everything will be just fine.

And you could use this piece of code to modify DecimalSeparators.xsl from C#

        private static XmlNamespaceManager CreateNsMgr(XmlDocument doc)
        {
            XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
            foreach (XmlAttribute attr in doc.SelectSingleNode("/*").Attributes)
                if (attr.Prefix == "xmlns") nsmgr.AddNamespace(attr.LocalName, attr.Value);
            return nsmgr;
        }

        public void GenerateCultureSpecificDecimalFormats()
        {
            string numberGroupSeparator = System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator;
            string numberDecimalSeparator = System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;

            XmlDocument doc = new XmlDocument();
            doc.Load("DecimalFormats.xsl");
            XmlElement node = (XmlElement)doc.SelectSingleNode("//xsl:decimal-format", CreateNsMgr(doc));
            node.SetAttribute("name", "i");
            node.SetAttribute("decimal-separator", numberDecimalSeparator);
            node.SetAttribute("grouping-separator", numberGroupSeparator);
            XmlTextWriter writer = new XmlTextWriter("DecimalFormats.xsl", null);
            doc.WriteTo(writer);
            writer.Close();
        }

But it’s a very ugly solution.

So here goes
Approach 3) Elegant.

You can write a custom .net function and call it from the xsl using the extension-objects mechanism provided by xsl.

Here’s the .net class that has that custom function:

    /// <summary>
    /// Provides number formatting methods used by an xslt processor at runtime.
    /// </summary>
    public class XsltNumberFormatter
    {

        /// <summary>
        /// Formats provided number using a culture specific transformation of the provided format
        /// as the format.
        /// </summary>
        ///
<param name="numberFormat">The number format in the en-US culture.</param>
        ///
<param name="strNumber">The decimal number as string.</param>
        /// <returns>
        /// Formatted <see cref="strNumber"/> using a culture specific transformation of <see cref="numberFormat"/>
        /// as the format.
        /// If
<paramref name="strNumber"/> is invalid it returns <see cref="Properties.Resources.NaN"/>
        /// </returns>
        /// <example>
        /// FormatNumber('1234.56','###,##0.00')
        /// will return 1,234.56 if the current culture is en-US or 1.234,56 if the current culture is ro-RO
        /// </example>
        public string FormatNumber(string strNumber, string numberFormat)
        {
            try
            {
                string stringDotFormatArg = "{0:" + numberFormat + "}";
                decimal number = Decimal.Parse(strNumber, new CultureInfo("en-US").NumberFormat);
                string result = String.Format(stringDotFormatArg, number);

                return result;
            }
            catch (Exception ex)
            {
                return "NaN";
            }

        }

    }

You would plug it into ibex xslt processor using this .net code:

using System;
using System.IO;
using System.Collections;

using ibex4;


public class test {

   static void Main( string[] args ) {

        FODocument doc = new FODocument();

        FileStream xml = new FileStream( "exslt.xml", FileMode.Open, FileAccess.Read );
        FileStream xsl = new FileStream( "exslt.xsl", FileMode.Open, FileAccess.Read );
        FileStream pdf = new FileStream( "exslt.pdf", FileMode.Create, FileAccess.Write );

        Hashtable xslArgs = new Hashtable();

        xslArgs.Add("http://www.yourcompany.com/numbers", new XsltNumberFormatter());

        doc.generate( xml, xsl, pdf, true, xslArgs );
   }

}

All what is left is to call it from your xsl file :)

<?xml version='1.0' encoding='utf-8'?>

<xsl:stylesheet version="2.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns:fo="http://www.w3.org/1999/XSL/Format"
	xmlns:ibex="http://www.xmlpdf.com/2003/ibex/Format"
        xmlns:my-num="http://www.yourcompany.com/numbers"  exclude-result-prefixes="my-num">

        <xsl:value-of select="my-num:FormatNumber('1234.56','###,###,##0.00')"/>

</xsl:stylesheet>

So now whenever you need to format something you just specify it’s format using en-US culture formats in your xsl (with ‘,’ ‘.’ as separators) and everything will be translated into the current culture format by our custom FormatNumber function.

Enjoy! :)

Advertisements

Written by Liviu Trifoi

February 11, 2009 at 4:49 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: