18th October 2008
Formatting dates is something we do all the time and .NET provides some really easy to use methods to format based on a string. However for human readable situations we so often want the day suffixes.
Some time ago I was looking into finding a 'nice' way to format dates with the suffixes on the day i.e. 'st', 'nd', 'rd' and 'th'. I ended up coming across a Codeproject Article by Mark Gwilliam that discussed an interesting approach.
A particular point he made was:
...if you're considering adding some functionality, it's worth checking to see if you can extend the framework in some way. This allows developers to consume your code more easily and hopefully expands your knowledge of .NET along the way... Mark Gwilliam
So, with this in mind, and without wanting to extend my never ending list of usernames/passwords I decided I would just take what was discussed in the article and create my own solution based on his approach rather than download his source. The plan was to create an ICustomFormatter:
// To go from string.Format("{0:dd MMMM yyyy}", DateTime.Now); //Outputs: 18 October 2008 // To string.Format(new BritishDateTimeFormatter(), "{0:ddx MMMM yyyy}", DateTime.Now); // Outputs: 18th October 2008fig. 1.0
This ICustomFormatter would provide a Format method that could understand the 'x' char as a placeholder for the day suffix. It would also need a way of identifying which suffix to put in its place.
An easy way to achieve this is to create an array of the suffixes referencable by the day of the month value (as below). This could also be a relatively minor algorithm to decide on suffix, it's personal preference (especially when you consider how small an effect we're talking about), but I quite like the simplicity of a static array.
namespace Formatting { /// <summary></summary> public class BritishDateTimeFormatter : IFormatProvider, ICustomFormatter { // Member containing day suffixes // Usage: extensions[dateInstance.Day]; #region Members private static string[] extensions = //0 1 2 3 4 5 6 7 8 9 { "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th", //10 11 12 13 14 15 16 17 18 19 "th", "th", "th", "th", "th", "th", "th", "tn", "th", "th", //20 21 22 23 24 25 26 27 28 29 "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th", //30 31 "th", "st" }; #endregion #region IFormatProvider Implementation public object GetFormat(Type formatType) { if (formatType == typeof(ICustomFormatter)) return this; else return null; } #endregion ... } }fig. 2.0
Included above is also the IFormatProvider implementation. Theres nothing too complicated here, merely returning the instance as the formatter.
The ICustomFormatter implementation of the Format function can then be filled in. As you can see the short format strings are manually re-represented to ensure that they continue to work using the default behaviour. Some minor string manipulation allows the suffix to be added when and where necessary.
The default behaviour of the switch on format string is to attempt to replace the x wherever it appears in the string (except when it is escaped by a '\'. This is achieved by a cascade of formats and regex replaces that ensure that the escaped x remains as a normal x and a non-escaped x gets transformed into the appropriate suffix.
public string Format(string fmt, object arg, IFormatProvider formatProvider) { string ret = arg.ToString(); DateTime date; if (DateTime.TryParse(ret, out date)) { switch (fmt) // Switch on all single character style format rules { case "ddx": ret = string.Format("{0}{1}", date.ToString("dd"), extensions[date.Day]); break; case "dx": // Doesn't make sense to add extensions to: 08/22/2006 ret = date.ToString("d"); break; case "Dx": ret = date.ToString("D"); ret = ret.Insert(ret.IndexOf(" "), extensions[date.Day]); break; case "Fx": ret = date.ToString("F"); ret = ret.Insert(ret.IndexOf(" "), extensions[date.Day]); break; case "fx": ret = date.ToString("f"); ret = ret.Insert(ret.IndexOf(" "), extensions[date.Day]); break; case "gx": // Doesn't make sense to add extensions to: 08/22/2006 06:30 ret = date.ToString("g"); break; case "Gx": // Doesn't make sense to add extensions to: 08/22/2006 06:30:07 ret = date.ToString("G"); break; case "mx": ret = date.ToString("m"); ret = ret.Insert(ret.IndexOf(" "), extensions[date.Day]); break; case "Rx": ret = date.ToString("R"); ret = ret.Insert(ret.IndexOf(" ", ret.IndexOf(", ") + 2), extensions[date.Day]); break; case "rx": ret = date.ToString("r"); ret = ret.Insert(ret.IndexOf(" ", ret.IndexOf(", ") + 2), extensions[date.Day]); break; case "sx": // Doesn't make sense to add extensions to: 2006-08-22T06:30:07 ret = date.ToString("s"); break; case "ux": // Doesn't make sense to add extensions to: 2006-08-22 06:30:07Z ret = date.ToString("u"); break; case "Ux": ret = date.ToString("U"); ret = ret.Insert(ret.IndexOf(" "), extensions[date.Day]); break; case "yx": // Doesn't make sense to add extensions to: 2006 August ret = date.ToString("y"); break; case "x": ret = extensions[date.Day]; break; default: // Attempt a format with 'x's included System.Text.RegularExpressions.Regex addExtraEscape = new System.Text.RegularExpressions.Regex("(\\\\)(x)"); fmt = addExtraEscape.Replace(fmt, "\\\\$2"); System.Text.RegularExpressions.Regex replaceUnescaped = new System.Text.RegularExpressions.Regex("([^\\\\])(x)"); ret = replaceUnescaped.Replace(date.ToString(fmt), string.Format("$1{0}", extensions[date.Day])); System.Text.RegularExpressions.Regex removeExtraEscape = new System.Text.RegularExpressions.Regex("(\\\\)(x)"); ret = removeExtraEscape.Replace(ret, "$2"); break; } } return ret; } } }fig. 2.1
By simply adding an instance method that takes the date and the format we can then add the BritishDateTimeFormatter as an extension object:
public string FormatDate(object arg, string fmt) { return Format(fmt, arg, this); }fig. 3.0
...and call it direct from the XSL.
<?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl date" xmlns:date="urn:BritishDateFormatter"> ... <xsl:value-of select="date:FormatDate(string(@time),$formatString)"/>fig. 4.0
An example of this article can be found within the Links section of this site where XML is the source and a BritishDateFormatter extension object is used to format the dates using "'dx MMM \’yy'" as the format string.
This solution is obviously very british/english language specific. A further step could be to look into support for localisation.
All article content is licenced under a Creative Commons Attribution-Noncommercial Licence.