tags:

views:

3029

answers:

7

Is there an easy way in C# to create Ordinals for a number? For example:

  • 1 returns 1st
  • 2 returns 2nd
  • 3 returns 3rd
  • ...etc

Can this be done through String.Format() or are there any functions available to do this?

+8  A: 

You'll have to roll your own. From the top of my head:

public static string Ordinal(this int number)
{
  var work = number.ToString();
  if (number == 11 || number == 12 || number == 13)
    return work + "th";
  switch (number % 10)
  {
    case 1: work += "st"; break;
    case 2: work += "nd"; break;
    case 3: work += "rd"; break;
    default: work += "th"; break;
  }
  return work;
}

You can then do

Console.WriteLine(432.Ordinal());

Edited for 11/12/13 exceptions. I DID say from the top of my head :-)

Stu
This function doesn't work: Ordinal(1011) -> 1011st
nickf
+5  A: 

How's this?

http://csharpaspnet.blogspot.com/2007/01/ordinal-numbers-in-c-like-1-as-1st-3-as.html

@Stu - love the elegance of making an extension method out of this.

Ian Nelson
+49  A: 

This page gives you a complete listing of all custom numerical formatting rules:

http://msdn.microsoft.com/en-us/library/0c899ak8.aspx

As you can see, there is nothing in there about ordinals, so it can't be done using String.Format. However its not really that hard to write a function to do it.

public string AddOrdinal(int num)
{
 switch(num % 100)
 {
  case 11:
  case 12:
  case 13:
   return num.ToString() + "th";
 }

 switch(num % 10)
 {
  case 1:
   return num.ToString() + "st";
  case 2:
   return num.ToString() + "nd";
  case 3:
   return num.ToString() + "rd";
  default:
   return num.ToString() + "th";
 }

}
samjudson
Assert.AreEqual("0", AddOrdinal(0));See http://www.wisegeek.com/what-is-an-ordinal-number.htm
Si
Using an extention method (or whatever it's called -- see @Stu's answer) would work great here. @Si, Adding that condition would be very easy if it is required.
strager
If I made it an extension method I would call it "ToOrdinalString".
samjudson
+3  A: 

I rather liked elements from both Stu's and samjudson's solutions and worked them together into what I think is a usable combo:

   public static string
   Ordinal (this int number)
   {
      const string  TH = "th";
      var           s = number.ToString ();

      number %= 100;

      if ((number >= 11) && (number <= 13))
      {
         return s + TH;
      }

      switch (number % 10)
      {
      case 1:
         return s + "st";
      case 2:
         return s + "nd";
      case 3:
         return s + "rd";
      default:
         return s + TH;
      }
   }
Jesse C. Slicer
what's the rationale behind using a constant for "th"?
nickf
because it's used twice in the code. Just utilizing the age-old wisdom that you shouldn't repeat yourself :) In this case, the .NET runtime should only create one copy of the string while with two "th"s in the code, there'd be two strings created and referenced in memory.
Jesse C. Slicer
and also, if the value of TH ever changes, you'll be set.
Eclipse
@Jesse - You get my +1, but I don't believe .NET handles strings this way, see http://www.yoda.arachsys.com/csharp/strings.html#interning, my reading of that is each reference to the "th" literal would reference the same bit of memory. But I agree about DRY :)
Si
@Si - Rereading my last response, I'm reversing what I said and agree with your assessment. .NET is pretty darn smart when it comes to string handling and you have to go out of your way to make it work badly.
Jesse C. Slicer
+4  A: 

While I haven't benchmarked this yet, you should be able to get better performance by avoiding all the conditional case statements.

This is java, but a port to C# is trivial:

public class NumberUtil {
  final static String[] ORDINAL_SUFFIXES = {
    "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"
  };

  public static String ordinalSuffix(int value) {
    int n = Math.abs(value);
    int lastTwoDigits = n % 100;
    int lastDigit = n % 10;
    int index = (lastTwoDigits >= 11 && lastTwoDigits <= 13) ? 0 : lastDigit;
    return ORDINAL_SUFFIXES[index];
  }

  public static String toOrdinal(int n) {
    return new StringBuffer().append(n).append(ordinalSuffix(n)).toString();
  }
}

Note, the reduction of conditionals and the use of the array lookup should speed up performance if generating a lot of ordinals in a tight loop. However, I also concede that this isn't as readable as the case statement solution.

Ryan McGeary
+21  A: 

Remember internationalisation!

The solutions here only work for English. Things get a lot more complex if you need to support other languages.

For example, in Spanish "1st" would be written as "1.o", "1.a", "1.os" or "1.as" depending on whether the thing you're counting is masculine, feminine or plural!

So if your software needs to support different languages, try to avoid ordinals.

roomaroo
Excellent point, and very easy to forget.
Electrons_Ahoy
How can 1st be plural?
Andomar
@ Andomar: "The first 2 readers" => in Italian (and Spanish too, I suppose) "first" is plural here. So you have singular masculine, singulare feminine, plural masculine, plural feminine; maybe some language has also a neutral case (distinguing things from men/animals)
Turro
That said, you don't have to avoid ordinals: include them in localization, once you know all the case you could face, or (make your customer) accept some limitations.
Turro
This explains why the .NET team steered clear of adding it to the DateTime formatters
Chris S
+4  A: 

My version of Jesse's version of Stu's and samjudson's versions :)

Included unit test to show that the accepted answer is incorrect when number < 1

    /// <summary>
    /// Get the ordinal value of positive integers.
    /// </summary>
    /// <remarks>
    /// Only works for english-based cultures.
    /// Code from: http://stackoverflow.com/questions/20156/is-there-a-quick-way-to-create-ordinals-in-c/31066#31066
    /// With help: http://www.wisegeek.com/what-is-an-ordinal-number.htm
    /// </remarks>
    /// <param name="number">The number.</param>
    /// <returns>Ordinal value of positive integers, or <see cref="int.ToString"/> if less than 1.</returns>
    public static string Ordinal(this int number)
    {
        const string TH = "th";
        string s = number.ToString();

        // Negative and zero have no ordinal representation
        if (number < 1) return s;

        number %= 100;
        if ((number >= 11) && (number <= 13))
        {
            return s + TH;
        }

        switch (number % 10)
        {
            case 1: return s + "st";
            case 2: return s + "nd";
            case 3: return s + "rd";
            default: return s + TH;
        }
    }

    [Test]
    public void Ordinal_ReturnsExpectedResults()
    {
        Assert.AreEqual("-1", (1-2).Ordinal());
        Assert.AreEqual("0", 0.Ordinal());
        Assert.AreEqual("1st", 1.Ordinal());
        Assert.AreEqual("2nd", 2.Ordinal());
        Assert.AreEqual("3rd", 3.Ordinal());
        Assert.AreEqual("4th", 4.Ordinal());
        Assert.AreEqual("5th", 5.Ordinal());
        Assert.AreEqual("6th", 6.Ordinal());
        Assert.AreEqual("7th", 7.Ordinal());
        Assert.AreEqual("8th", 8.Ordinal());
        Assert.AreEqual("9th", 9.Ordinal());
        Assert.AreEqual("10th", 10.Ordinal());
        Assert.AreEqual("11th", 11.Ordinal());
        Assert.AreEqual("12th", 12.Ordinal());
        Assert.AreEqual("13th", 13.Ordinal());
        Assert.AreEqual("14th", 14.Ordinal());
        Assert.AreEqual("20th", 20.Ordinal());
        Assert.AreEqual("21st", 21.Ordinal());
        Assert.AreEqual("22nd", 22.Ordinal());
        Assert.AreEqual("23rd", 23.Ordinal());
        Assert.AreEqual("24th", 24.Ordinal());
        Assert.AreEqual("100th", 100.Ordinal());
        Assert.AreEqual("101st", 101.Ordinal());
        Assert.AreEqual("102nd", 102.Ordinal());
        Assert.AreEqual("103rd", 103.Ordinal());
        Assert.AreEqual("104th", 104.Ordinal());
        Assert.AreEqual("110th", 110.Ordinal());
        Assert.AreEqual("111th", 111.Ordinal());
        Assert.AreEqual("112th", 112.Ordinal());
        Assert.AreEqual("113th", 113.Ordinal());
        Assert.AreEqual("114th", 114.Ordinal());
        Assert.AreEqual("120th", 120.Ordinal());
        Assert.AreEqual("121st", 121.Ordinal());
        Assert.AreEqual("122nd", 122.Ordinal());
        Assert.AreEqual("123rd", 123.Ordinal());
        Assert.AreEqual("124th", 124.Ordinal());
    }
Si