String representation of enumerations

12 01 2008


1. Introduction
2. The problem
3. Other solutions
4. The goal
5. Example
6. Conclusion
7. Advantages of this solution
8. Full source code

1. Introduction

Use of enumerations greatly enhance readability of code and let client code be type safe and easier to write.
We don’t need to write sets of constant values and related validation code, we only need to properly define enumeration items.

2. The problem

Sometimes we want to use clear and simple names for enumeration values but we want to show more meaningful string representations in some GUI.
This is were simply defining our custom enumeration is not enough.
In fact default string representation of enum items relies on the EnumConverter class. But EnumConverter simply uses the enum item’s name as its string representation.

3. Other solutions

When I first needed to solve this issue, I browsed the web and found few good articles about repurposing enumerations, such as: Repurposing Enumerations by Mathew Nolton and Localizing .NET Enums by Grant Frisken, and few more.
All of them rely on implementing a custom EnumConverter to be applied declaratively to the enumeration:

[TypeConverter(typeof(CustomConverter))]
public enum MyEnum
{
   ...
}

I found very instructive and interesting such approach but I still was not satisfied.
What I dislike is the need to change client code from a simple:

MyObject.Property = MyEnum.Item;
string humanReadable = MyObject.Property;

to:

MyObject.Property = MyEnum.Item;
string humanReadable = (string)TypeDescriptor.
GetConverter(typeof(MyEnum)).
ConvertTo(MyObject.Property, typeof(string));

→ go to top

4. The goal

What I wanted was a pattern whose goal is to minimize the changes to client code while being as simple as possible to implement.
All the articles that I read implied the need of a helper class (the custom EnumConverter), so I found it acceptable to implement a simple helper class too.
I wanted a pattern providing a sort of separation of concerns: keeping Enumeration behavior unchanged while providing a means to control its string representation.
The solution I’m going to illustrate is based on the use of conversion operators and I hope you will find it simple and straightforward.

→ go to top

5. Example

A typical situation could be a Clock application that we want to be able to display date and/or time in different formats.
We will name the format choices using simple enumeration items (e.g.: DateTimeFormat.Date, DateTimeFormat.Time) but we’ll need to convert them in actual formats.
The actual formats (e.g.: ‘dd/MM/yyyy’, ‘HH:mm:ss’) are needed to instruct the Clock object about how to render itself and its embedded DateTime in the form of a text string in a specific format.
Take a look at the following code:

remark: note that comments on method calls in the client code state only the expected behavior.
We will obtain it only at the end of the example.

/***************
* Client code: *
****************/

Clock clock = new Clock();
// clock.DisplayedFormat accepts enumeration values
// (e.g.: 'DateTimeFormat.Date')
clock.DisplayedFormat = DateTimeFormat.Date;
// Enum value will be converted internally in the
// actual format and used by the Clock so that we can
// simply tell it to represent as string.
Console.WriteLine(clock);

// clock.DisplayedFormat returns the actual format
// string (e.g.: 'dd/MM/yyyy')
string format = clock.DisplayedFormat;
Console.WriteLine(format);

// ouptus:
// 10/01/2008
// dd/MM/yyyy

Let’s implement the Types we need:

/*************
* Enum code: *
**************/

public enum DateTimeFormat
{
    Date,
    Time
}

Let’s Try and implement Clock’s basic functionality:

remark: the following code doesn’t work yet: using Enum values directly as formats
for DateTime.ToString( ) leads to incorrect results.
/*************
* Clock code:*
**************/

public class Clock
{

  #region fields

  private DateTime m_CurrentDateTime = DateTime.Now;
  // defaults to Time format
  private DateTimeFormat m_Format = DateTimeFormat.Time; 

  #endregion
  #region properties

  /// <summary>
  /// format used to display current datetime
  /// </summary>
  public DateTimeFormat DisplayedFormat
  {
      get
      {
          return m_Format;
      }
      set
      {
          m_Format = value;
      }
  }

  #endregion
  #region methods

   /// <summary>
   /// provides string representation of current DateTime
   /// depending on the format set.
   /// </summary>
   /// <returns>returns current date or time</returns>
   public override string ToString()
   {
       return m_CurrentDateTime.
              ToString(DisplayedFormat.ToString());
   }

   #endregion
}

The three preceding slices of code don’t even compile because in the client code we need to provide explicit conversion from the Enum value to a string.
So we need to replace the line:



string format = clock.DisplayedFormat;

with:


string format = clock.DisplayedFormat.ToString();

Now that the code compiles we still have some wierd results.
In fact we provided a not meaningful format (the string representation of the enum value: ‘date’) to the m_CurrentDateTime.ToString( ) method.
To let this code work we could implement a custom EnumConverter in order to represent the enum values as we like. But this choice would force us to modify client code as shown in paragraph: ‘Other solutions’.

We don’t want to do such a modification and we even want the correct string to be returned by simply writing:


string format = clock.DisplayedFormat;

To obtain the expected result we will implement a nested helper class named: Format.

public class Format
{
   #region const

   const string
      Date = "dd/MM/yyyy",
      Time = "HH:mm:ss";

   #endregion
   #region fields

   DateTimeFormat m_CalendarFormat;

   #endregion
   #region .CTOR

   /// <summary>
   /// each Format instance represents a different value
   /// of DateTimeFormat enumeration
   /// </summary>
   /// <param name="calendarFormat">DateTimeFormat value
   /// </param>
   public Format(DateTimeFormat calendarFormat)
   {
       m_CalendarFormat = calendarFormat;
   }

   #endregion
   #region methods

   /// <summary>
   /// converts current Format instance into its string
   /// representation, i.e. the actual format string
   /// </summary>
   /// <returns>actual format string represented by the
   /// current Format instance (e.g.: "dd/MM/yyy")</returns>
   public override string ToString()
   {
       return Convert();
   }

    /// <summary>
    /// converts enum values in actual datetime formats
    /// </summary>
    /// <returns>returns valid datetime format</returns>
    private string Convert()
    {
        string format = null;
        switch (m_CalendarFormat)
        {
            case DateTimeFormat.Date:
                format = Date;
                break;
            case DateTimeFormat.Time:
                format = Time;
                break;
            default:
                throw new Exception(
                              "Format is not initialized!!");
         }
         return format;
     }

     #endregion
}

Every instance of Format is initialized with a DateTimeFormat value and represents that
value and the string representation we want for it.
Now that we have the helper class we can replace Clock.DisplayedFormat returned type with: Format.

private Format m_Format;
public Format DisplayedFormat
{
	get
	{
		return m_Format;
	}
	set
	{
		m_Format = value;
	}
}

This would force us to change our client code like the following:

Clock clock = new Clock();
// FOLLOWING ROW IS CHANGED
clock.DisplayedFormat= new Clock.Format(DateTimeFormat.Date);
Console.WriteLine(clock);

// clock.DisplayedFormat returns the actual
// format string (i.e.: 'dd/MM/yyyy')
string format = clock.DisplayedFormat.ToString();
Console.WriteLine(format);

Now in client and Clock code the following row:


string format = clock.DisplayedFormat.ToString();

returns the correct string representation of the chosen format (i.e.: ‘dd/MM/yyyy’).
At this point we’re still forced to make too many modifications to client code.
This is where conversion operators come in place!!

Conversion operators are particular static methods that we can overload to provide algorithms for conversion between types.
We can implement an implicit conversion operator allowing the runtime to perform automatic conversion or we can implement an explicit conversion operator telling the runtime to throw an exception if client code doesn’t contain an explicit casting.
Let’s see implicit conversion operators at work adding the following two methods to the class: Format.
/// <summary>
/// converts a DateTimeFormat value into a Format instance
/// </summary>
public static implicit operator Format(DateTimeFormat arg)
{
	return new Format(arg);
}

/// <summary>
/// converts a Format instance into a string
/// </summary>
public static implicit operator string(Format arg)
{
	return arg.Convert();
}

The first one provides implicit conversion of DateTimeFormat values in Format instances.
This allows us to set clock.DisplayedFormat by simply providing the enumeration value:

clock.DisplayedFormat = DateTimeFormat.Date;

The second one provides implicit conversion of a Format instance into its string representation, the actual format string (i.e.: “dd/MM/yyy”).
This allows us to retrieve the desired actual format string by simply getting clock.DisplayedFormat:



string format = clock.DisplayedFormat;

→ top of paragraph
→ top of post

6. Conclusion

Eventually, to achieve the desired result, we only need two things.
We only need to implement the helper class and to replace the clock.Property returned type and it will work like a charm!

→ go to top

7. Advantages of this solution

Following are advantages of this solution:

  • Simple to implement (it is more difficult to explain than to code)
  • No changes to client code needed
  • Separation of concerns (relying on Format class to convert enum values and not on the enumeration itself by means of a custom converter)
  • Works with existing enumerations too

→ go to top

8. Full source code

Clock Code:

/*************
* Clock code:*
**************/

using System;

namespace FormatProvider
{
  public enum DateTimeFormat
  {
    Date,
    Time
  }

  /// <summary>
  /// displays date or time
  /// </summary>
  public class Clock
  {
    #region nested class

    /// <summary>
    /// encapsulate a DateTimeFormat and its
    /// string representation
    /// </summary>
    public class Format
    {
      #region const
      const string
              Date = "dd/MM/yyyy",
              Time = "HH:mm:ss";
      #endregion
      #region fields

      DateTimeFormat m_CalendarFormat;

      #endregion
      #region .CTOR
      /// <summary>
      /// each Format instance represents a different value
      /// of DateTimeFormat enumeration
      /// </summary>
      /// <param name="calendarFormat">DateTimeFormat value
      /// </param>
      public Format(DateTimeFormat calendarFormat)
      {
          m_CalendarFormat = calendarFormat;
      } 

      #endregion
      #region methods

      /// <summary>
      /// provides implicit conversion of DateTimeFormat
      /// values in Format instances
      /// </summary>
      /// <param name="arg">DateTimeFormat value</param>
      /// <returns>Format instance corresponding to the
      /// DateTimeFormat value provided</returns>
      public static implicit operator Format(
                                   DateTimeFormat arg)
      {
          return new Format(arg);
      }

      /// <summary>
      /// provides implicit conversion of a Format instance
      /// in its string representation, i.e. the actual
      /// format string</summary>
      /// <param name="arg">a Format instance to convert in
      /// string representation</param>
      /// <returns>returns the actual format string
      /// represented by the current Format instance
      /// (i.e.: "dd/MM/yyy")</returns>
      public static implicit operator string(Format arg)
      {
          return arg.Convert();
      }

      /// <summary>
      /// converts current Format instance
      /// in its string representation, i.e. the actual
      /// format string</summary>
      /// <returns>actual format string represented by the
      /// current Format instance (i.e.: "dd/MM/yyy")
      /// </returns>
      public override string ToString()
      {
          return Convert();
      }

      /// <summary>
      /// converts enum values in actual datetime formats
      /// </summary>
      /// <returns>returns valid datetime format</returns>
      private string Convert()
      {
          string format = null;
          switch (m_CalendarFormat)
          {
              case DateTimeFormat.Date:
                  format = Date;
                  break;
              case DateTimeFormat.Time:
                  format = Time;
                  break;
              default:
                  throw new Exception(
                               "Format not initialized!!");
          }
          return format;
      } 

      #endregion
    } 

    #endregion
    #region fields

    private DateTime m_CurrentDateTime = DateTime.Now;
    // defaults to time
    private Format m_Format = DateTimeFormat.Time;

    #endregion
    #region properties

    /// <summary>
    /// format used to display current datetime
    /// </summary>
    public Format DisplayedFormat
    {
        get
        {
            return m_Format;
        }
        set
        {
            m_Format = value;
        }
    } 

    #endregion
    #region methods

    /// <summary>
    /// provide string representation of current datetime
    /// depending on the format set.
    /// </summary>
    /// <returns>returns current date or time</returns>
    public override string ToString()
    {
        return m_CurrentDateTime.ToString(DisplayedFormat);
    } 

    #endregion
  }
}

→ top of paragraph
→ top of post

Client Code:

/***************
* Client code: *
****************/

using System;
using FormatProvider;

namespace ClientCode
{
    class Program
    {
        static void Main(string[] args)
        {
            Clock clock = new Clock();
            // clock.DisplayedFormat accepts enumeration
            // values (i.e.: 'DateTimeFormat.Date')
            clock.DisplayedFormat = DateTimeFormat.Date;
            Console.WriteLine(clock);

            // clock.DisplayedFormat returns the actual
            // format string (i.e.: 'dd/MM/yyyy')
            string format = clock.DisplayedFormat;
            Console.WriteLine(format);

            // output:
            // 08/01/2008
            // dd/MM/yyyy
        }
    }
}

→ top of paragraph
→ top of post

Advertisements

Actions

Information

2 responses

24 01 2008
dbilli

Ciao Matteo, scusami se leggo solo ora l’articolo ma sono stato parecchio impegnato, infatti ho postato pochissimi articoli anche nel mio blog. Ho letto il tuo articolo, sicuramente è interessante il modo in cui hai usato l’enumerativo. Tuttavia io trovo più comodo l’utilizzo di alcuni metodi nativamente presenti nell’oggetto DateTime che permettono di avere direttamente la data, l’ora o altro nella stringa rappresentativa, tra l’altro supportando automaticamente le diversità di localizzazione (in america con “yyyy/mm/dd” e in italia “dd/mm/yyyy” per intenderci). Buon lavoro comunque.

A presto!

3 11 2010
Silvio

Very nice job imo.
I know this post is _old_ but this is a really clever solution.
Enums in C# are very limited: wrapping them in a custom value object (http://c2.com/cgi/wiki?ValueObject) overcomes the limitations. The added complexity of a new type in the program is very (very) worth the cost because you centralize handling of the enum.
Btw, I would think of a factory method and private ctor for the value object and would not nest its class…
Learned something, thank you very much!

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: