Is there an easy way to create ordinals in C#?

2019-01-01 08:08发布

问题:

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?

回答1:

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 static string AddOrdinal(int num)
{
    if( num <= 0 ) return num.ToString();

    switch(num % 100)
    {
        case 11:
        case 12:
        case 13:
            return num + \"th\";
    }

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

}

Update: Technically Ordinals don\'t exist for <= 0, so I\'ve updated the code above. Also removed the redundant ToString() methods.

Also note, this is not internationalised. I\'ve no idea what ordinals look like in other languages.



回答2:

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.



回答3:

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());
    }


回答4:

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 % 100) == 11 || (number % 100) == 12 || (number % 100) == 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 :-)

Edited for 1011 -- others have fixed this already, just want to make sure others don\'t grab this incorrect version.



回答5:

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;
        }
    }


回答6:

Simple, clean, quick

    private static string GetOrdinalSuffix(int num)
    {
        if (num.ToString().EndsWith(\"11\")) return \"th\";
        if (num.ToString().EndsWith(\"12\")) return \"th\";
        if (num.ToString().EndsWith(\"13\")) return \"th\";
        if (num.ToString().EndsWith(\"1\")) return \"st\";
        if (num.ToString().EndsWith(\"2\")) return \"nd\";
        if (num.ToString().EndsWith(\"3\")) return \"rd\";
        return \"th\";
    }

Or better yet, as an extension method

public static class IntegerExtensions
{
    public static string DisplayWithSuffix(this int num)
    {
        if (num.ToString().EndsWith(\"11\")) return num.ToString() + \"th\";
        if (num.ToString().EndsWith(\"12\")) return num.ToString() + \"th\";
        if (num.ToString().EndsWith(\"13\")) return num.ToString() + \"th\";
        if (num.ToString().EndsWith(\"1\")) return num.ToString() + \"st\";
        if (num.ToString().EndsWith(\"2\")) return num.ToString() + \"nd\";
        if (num.ToString().EndsWith(\"3\")) return num.ToString() + \"rd\";
        return num.ToString() + \"th\";
    }
}

Now you can just call

int a = 1;
a.DisplayWithSuffix(); 

or even as direct as

1.DisplayWithSuffix();


回答7:

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.



回答8:

Similar to Ryan\'s solution, but even more basic, I just use a plain array and use the day to look up the correct ordinal:

private string[] ordinals = new string[] {\"\",\"st\",\"nd\",\"rd\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"st\",\"nd\",\"rd\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"th\",\"st\" };
DateTime D = DateTime.Now;
String date = \"Today\'s day is: \"+ D.Day.ToString() + ordinals[D.Day];

I have not had the need, but I would assume you could use a multidimensional array if you wanted to have multiple language support.

From what I can remember from my Uni days, this method requires minimal effort from the server.



回答9:

        private static string GetOrd(int num) => $\"{num}{(!(Range(11, 3).Any(n => n == num % 100) ^ Range(1, 3).All(n => n != num % 10)) ? new[] { \"ˢᵗ\", \"ⁿᵈ\", \"ʳᵈ\" }[num % 10 - 1] : \"ᵗʰ\")}\";

If anyone looking for one liner :p



回答10:

I use this extension class:

public static class Int32Extensions
{
    public static string ToOrdinal(this int i)
    {
        return (i + \"th\")
            .Replace(\"1th\", \"1st\")
            .Replace(\"2th\", \"2nd\")
            .Replace(\"3th\", \"3rd\");
    }
}


回答11:

Requested \"less redundancy\" version of samjudson\'s answer...

public static string AddOrdinal(int number)
{
    if (number <= 0) return number.ToString();

    string GetIndicator(int num)
    {
        switch (num % 100)
        {
            case 11:
            case 12:
            case 13:
                return \"th\";
        }

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

    return number + GetIndicator(number);
}


回答12:

FWIW, for MS-SQL, this expression will do the job. Keep the first WHEN (WHEN num % 100 IN (11, 12, 13) THEN \'th\') as the first one in the list, as this relies upon being tried before the others.

CASE
  WHEN num % 100 IN (11, 12, 13) THEN \'th\' -- must be tried first
  WHEN num % 10 = 1 THEN \'st\'
  WHEN num % 10 = 2 THEN \'nd\'
  WHEN num % 10 = 3 THEN \'rd\'
  ELSE \'th\'
END AS Ordinal

For Excel :

=MID(\"thstndrdth\",MIN(9,2*RIGHT(A1)*(MOD(A1-11,100)>2)+1),2)

The expression (MOD(A1-11,100)>2) is TRUE (1) for all numbers except any ending in 11,12,13 (FALSE = 0). So 2 * RIGHT(A1) * (MOD(A1-11,100)>2) +1) ends up as 1 for 11/12/13, otherwise :
1 evaluates to 3
2 to 5,
3 to 7
others : 9
- and the required 2 characters are selected from \"thstndrdth\" starting from that position.

If you really want to convert that fairly directly to SQL, this worked for me for a handful of test values :

DECLARE @n as int
SET @n=13
SELECT SubString(  \'thstndrdth\'
                 , (SELECT MIN(value) FROM
                     (SELECT 9 as value UNION
                      SELECT 1+ (2* (ABS(@n) % 10)  *  CASE WHEN ((ABS(@n)+89) % 100)>2 THEN 1 ELSE 0 END)
                     ) AS Mins
                   )
                 , 2
                )


回答13:

EDIT: As YM_Industries points out in the comment, samjudson\'s answer DOES work for numbers over 1000, nickf\'s comment seems to have gone, and I can\'t remember what the problem I saw was. Left this answer here for the comparison timings.

An awful lot of these don\'t work for numbers > 999, as nickf pointed out in a comment (EDIT: now missing).

Here is a version based off a modified version of samjudson\'s accepted answer that does.

public static String GetOrdinal(int i)
{
    String res = \"\";

    if (i > 0)
    {
        int j = (i - ((i / 100) * 100));

        if ((j == 11) || (j == 12) || (j == 13))
            res = \"th\";
        else
        {
            int k = i % 10;

            if (k == 1)
                res = \"st\";
            else if (k == 2)
                res = \"nd\";
            else if (k == 3)
                res = \"rd\";
            else
                res = \"th\";
        }
    }

    return i.ToString() + res;
}

Also Shahzad Qureshi\'s answer using string manipulation works fine, however it does have a performance penalty. For generating a lot of these, a LINQPad example program makes the string version 6-7 times slower than this integer one (although you\'d have to be generating a lot to notice).

LINQPad example:

void Main()
{
    \"Examples:\".Dump();

    foreach(int i in new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 22, 113, 122, 201, 202, 211, 212, 2013, 1000003, 10000013 })
        Stuff.GetOrdinal(i).Dump();

    String s;

    System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();

    for(int iter = 0; iter < 100000; iter++)
        foreach(int i in new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 22, 113, 122, 201, 202, 211, 212, 2013, 1000003, 1000013 })
            s = Stuff.GetOrdinal(i);

    \"Integer manipulation\".Dump();
    sw.Elapsed.Dump();

    sw.Restart();

    for(int iter = 0; iter < 100000; iter++)
        foreach(int i in new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 22, 113, 122, 201, 202, 211, 212, 2013, 1000003, 1000013 })
            s = (i.ToString() + Stuff.GetOrdinalSuffix(i));

    \"String manipulation\".Dump();
    sw.Elapsed.Dump();
}

public class Stuff
{
        // Use integer manipulation
        public static String GetOrdinal(int i)
        {
                String res = \"\";

                if (i > 0)
                {
                        int j = (i - ((i / 100) * 100));

                        if ((j == 11) || (j == 12) || (j == 13))
                                res = \"th\";
                        else
                        {
                                int k = i % 10;

                                if (k == 1)
                                        res = \"st\";
                                else if (k == 2)
                                        res = \"nd\";
                                else if (k == 3)
                                        res = \"rd\";
                                else
                                        res = \"th\";
                        }
                }

                return i.ToString() + res;
        }

        // Use string manipulation
        public static string GetOrdinalSuffix(int num)
        {
                if (num.ToString().EndsWith(\"11\")) return \"th\";
                if (num.ToString().EndsWith(\"12\")) return \"th\";
                if (num.ToString().EndsWith(\"13\")) return \"th\";
                if (num.ToString().EndsWith(\"1\")) return \"st\";
                if (num.ToString().EndsWith(\"2\")) return \"nd\";
                if (num.ToString().EndsWith(\"3\")) return \"rd\";
                return \"th\";
        }
}


回答14:

public static string OrdinalSuffix(int ordinal)
{
    //Because negatives won\'t work with modular division as expected:
    var abs = Math.Abs(ordinal); 

    var lastdigit = abs % 10; 

    return 
        //Catch 60% of cases (to infinity) in the first conditional:
        lastdigit > 3 || lastdigit == 0 || (abs % 100) - lastdigit == 10 ? \"th\" 
            : lastdigit == 1 ? \"st\" 
            : lastdigit == 2 ? \"nd\" 
            : \"rd\";
}


回答15:

Based off the other answers:

public static string Ordinal(int n)
{   
    int     r = n % 100,     m = n % 10;

    return (r<4 || r>20) && (m>0 && m<4) ? n+\"  stndrd\".Substring(m*2,2) : n+\"th\";                                              
}


回答16:

Here is the DateTime Extension class. Copy, Paste & Enjoy

public static class DateTimeExtensions {

    public static string ToStringWithOrdinal(this DateTime d)
    {
        var result = \"\";
        bool bReturn = false;            

        switch (d.Day % 100)
        {
            case 11:
            case 12:
            case 13:
                result = d.ToString(\"dd\'th\' MMMM yyyy\");
                bReturn = true;
                break;
        }

        if (!bReturn)
        {
            switch (d.Day % 10)
            {
                case 1:
                    result = d.ToString(\"dd\'st\' MMMM yyyy\");
                    break;
                case 2:
                    result = d.ToString(\"dd\'nd\' MMMM yyyy\");
                    break;
                case 3:
                    result = d.ToString(\"dd\'rd\' MMMM yyyy\");
                    break;
                default:
                    result = d.ToString(\"dd\'th\' MMMM yyyy\");
                    break;
            }

        }

        if (result.StartsWith(\"0\")) result = result.Substring(1);
        return result;
    }
}

Result :

9th October 2014



回答17:

Another alternative that I used based on all the other suggestions, but requires no special casing:

    public static string DateSuffix(int day)
    {
        if (day == 11 | day == 12 | day == 13) return \"th\";
        Math.DivRem(day, 10, out day);
        switch (day)
        {
            case 1:
                return \"st\";
            case 2:
                return \"nd\";
            case 3:
                return \"rd\";
            default:
                return \"th\";
        }
    }


标签: c# .net ordinals