开发者

Clever way to append 's' for plural form in .Net (syntactic sugar)

I want to be able to type something like:

Console.WriteLine("You have {0:life/lives} left.", player.Lives);

instead of

Console.WriteLine("You have {0} {1} left.", player.Lives, player.Lives == 1 ? "life" : "lives");

so that for player.Lives == 1 the output would be: You have 1 life left.

for player.Lives != 1 : You have 5 lives left.

or

Console.WriteLine("{0:day[s]} till doomsday.", tillDoomsdayTimeSpan);

Some systems have that built-in. How close can I get to that notation in C#?

EDIT: Yes, I am specifically looking for syntactic sugar, and not a method to determine what singular/p开发者_如何学Golural forms are.


You may checkout the PluralizationService class which is part of the .NET 4.0 framework:

string lives = "life";
if (player.Lives != 1)
{
    lives = PluralizationService
        .CreateService(new CultureInfo("en-US"))
        .Pluralize(lives);
}
Console.WriteLine("You have {0} {1} left", player.Lives, lives);

It is worth noting that only English is supported for the moment. Warning, this don't work on the Net Framework 4.0 Client Profile!

You could also write an extension method:

public static string Pluralize(this string value, int count)
{
    if (count == 1)
    {
        return value;
    }
    return PluralizationService
        .CreateService(new CultureInfo("en-US"))
        .Pluralize(value);
}

And then:

Console.WriteLine(
    "You have {0} {1} left", player.Lives, "life".Pluralize(player.Lives)
);


You can create a custom formatter that does that:

public class PluralFormatProvider : IFormatProvider, ICustomFormatter {

  public object GetFormat(Type formatType) {
    return this;
  }


  public string Format(string format, object arg, IFormatProvider formatProvider) {
    string[] forms = format.Split(';');
    int value = (int)arg;
    int form = value == 1 ? 0 : 1;
    return value.ToString() + " " + forms[form];
  }

}

The Console.WriteLine method has no overload that takes a custom formatter, so you have to use String.Format:

Console.WriteLine(String.Format(
  new PluralFormatProvider(),
  "You have {0:life;lives} left, {1:apple;apples} and {2:eye;eyes}.",
  1, 0, 2)
);

Output:

You have 1 life left, 0 apples and 2 eyes.

Note: This is the bare minimum to make a formatter work, so it doesn't handle any other formats or data types. Ideally it would detect the format and data type, and pass the formatting on to a default formatter if there is some other formatting or data types in the string.


With the newfangled interpolated strings, I just use something like this:

// n is the number of connection attempts
Console.WriteLine($"Needed {n} attempt{(n!=1 ? "s" : "")} to connect...");

EDIT: just ran across this answer again--since I originally posted this, I've been using an extension method that makes it even easier. This example is ordered by peculiarity:

static class Extensions {
    /// <summary>
    /// Pluralize: takes a word, inserts a number in front, and makes the word plural if the number is not exactly 1.
    /// </summary>
    /// <example>"{n.Pluralize("maid")} a-milking</example>
    /// <param name="word">The word to make plural</param>
    /// <param name="number">The number of objects</param>
    /// <param name="pluralSuffix">An optional suffix; "s" is the default.</param>
    /// <param name="singularSuffix">An optional suffix if the count is 1; "" is the default.</param>
    /// <returns>Formatted string: "number word[suffix]", pluralSuffix (default "s") only added if the number is not 1, otherwise singularSuffix (default "") added</returns>
    internal static string Pluralize(this int number, string word, string pluralSuffix = "s", string singularSuffix = "")
    {
        return $@"{number} {word}{(number != 1 ? pluralSuffix : singularSuffix)}";
    }
}

void Main()
{
    int lords = 0;
    int partridges = 1;
    int geese = 1;
    int ladies = 8;
    Console.WriteLine($@"Have {lords.Pluralize("lord")}, {partridges.Pluralize("partridge")}, {ladies.Pluralize("lad", "ies", "y")}, and {geese.Pluralize("", "geese", "goose")}");
    lords = 1;
    partridges = 2;
    geese = 6;
    ladies = 1;
    Console.WriteLine($@"Have {lords.Pluralize("lord")}, {partridges.Pluralize("partridge")}, {ladies.Pluralize("lad", "ies", "y")}, and {geese.Pluralize("", "geese", "goose")}");
}

(formats are the same). The output is:

Have 0 lords, 1 partridge, 8 ladies, and 1 goose
Have 1 lord, 2 partridges, 1 lady, and 6 geese


using @Darin Dimitrov solution, I would create an extention for string ....

public static Extentions
{
    public static string Pluralize(this string str,int n)
    {
        if ( n != 1 )
            return PluralizationService.CreateService(new CultureInfo("en-US"))
            .Pluralize(str);
        return str;
    }
}

string.format("you have {0} {1} remaining",liveCount,"life".Pluralize());


string message = string.format("You have {0} left.", player.Lives == 1 ? "life" : "lives");

Of course this assumes that you have a finite number of values to pluralize.


I wrote an open-source library called SmartFormat that does exactly that! It's written in C# and is on GitHub: http://github.com/scottrippey/SmartFormat

Although it supports several languages, English "plural rules" are the default. Here's the syntax:

var output = Smart.Format("You have {0} {0:life:lives} left.", player.Lives);

It also supports "zero" quantity, and nested placeholders, so you could do:

var output = Smart.Format("You have {0:no lives:1 life:{0} lives} left.", player.Lives);


See the Inflector class that is part of Castle ActiveRecord. It is licensed under the Apache license.

It has a set of regular expression rules that define how words are pluralized. The version I have used has some errors in these rules though, e.g. it has a 'virus' → 'virii' rule.

I have three extension methods which wrap Inflector, the first of which may be right up your street:

    /// <summary>
    /// Pluralises the singular form word specified.
    /// </summary>
    /// <param name="this">The singular form.</param>
    /// <param name="count">The count.</param>
    /// <returns>The word, pluralised if necessary.</returns>
    public static string Pluralise(this string @this, long count)
    {
        return (count == 1) ? @this :
                              Pluralise(@this);
    }

    /// <summary>
    /// Pluralises the singular form word specified.
    /// </summary>
    /// <param name="this">The singular form word.</param>
    /// <returns>The plural form.</returns>
    public static string Pluralise(this string @this)
    {
        return Inflector.Pluralize(@this);
    }

    /// <summary>
    /// Singularises the plural form word.
    /// </summary>
    /// <param name="this">The plural form word.</param>
    /// <returns>Th singular form.</returns>
    public static string Singularise(this string @this)
    {
        return Inflector.Singularize(@this);
    }


For C# 6.0 onwards, you can use Interpolated Strings to do this tricks.

Example:

    Console.WriteLine("\n --- For REGULAR NOUNS --- \n");
    {
        int count1 = 1;
        Console.WriteLine($"I have {count1} apple{(count1 == 1 ? "" : "s")}.");
        int count2 = 5;
        Console.WriteLine($"I have {count2} apple{(count2 == 1 ? "" : "s")}.");
    }

    Console.WriteLine("\n --- For IRREGULAR NOUNS --- \n");
    {
        int count1 = 1;
        Console.WriteLine($"He has {count1} {(count1 == 1 ? "leaf" : "leaves")}.");
        int count2 = 5;
        Console.WriteLine($"He has {count2} {(count2 == 1 ? "leaf" : "leaves")}.");
    }

Output:

 --- For REGULAR NOUNS --- 

I have 1 apple.
I have 5 apples.

 --- For IRREGULAR NOUNS --- 

He has 1 leaf.
He has 5 leaves.

You can play around on my .NET Fiddle.
For more details, go to Interpolated String documentation.


I'm thinking the easiest way to do it is to create an Interface IPlural which has an method .ToString(int quantity) which returns the singular form when quantity == 1 an the plural form all other times.


I am using this extension method with .NET 4.6

public static string Pluralize(this string @string)
{
     if (string.IsNullOrEmpty(@string)) return string.Empty;

     var service = new EnglishPluralizationService();

     return service.Pluralize(@string);
}


I did a little bit of work with PluralizationService and came up with. I just made the PluralizationService static for performance and combined all.

Reference System.Data.Entity.Design

  using System.Data.Entity.Design.PluralizationServices;
  using System.Reflection;

  public static class Strings
  {
    private static PluralizationService pluralizationService = PluralizationService.CreateService(System.Globalization.CultureInfo.CurrentUICulture);
    public static string Pluralize(this MemberInfo memberInfo)//types, propertyinfos, ect
    {
      return Pluralize(memberInfo.Name.StripEnd());
    }

    public static string Pluralize(this string name)
    {
      return pluralizationService.Pluralize(name); // remove EF type suffix, if any
    }

    public static string StripEnd(this string name)
    {
      return name.Split('_')[0];
    }
  }


If your application is English on you can use all the solutions shown here. However if you plan to localize the application the plural enabled message must be done in a proper way. This means that you may need multiple patterns (from 1 to 6) depending on the language and the rule to choose what the pattern is used depends on the language.

For example in English you would have two patterns

"You have {0} live left" "You have {0} lives left"

Then you would have a Format function where you pass these two patterns with the liveAmount variable.

Format("You have {0} live left", "You have {0} lives left", liveAmount);

In a real application you would not hard code the string but would use resource strings.

Format would know what the active language is and if English it would use

if (count == 1)
  useSingularPattern
else
  usePluralPattern

To implement this yourself is a bit complex bu you don't have to. You can you an open source project I have made

https://github.com/jaska45/I18N

By using it you can easily get the string

var str = MultiPattern.Format("one;You have {0} live left;other;You have {0} lives left", liveAmount);

That's it. The library knows what pattern to use depending on the passed liveAmount paramter. The rules have been extracted from CLDR into library .cs file.

If you want to localize the application you just put the multi pattern string into .resx and let the translator to translate it. Depending on the target language the multi pattern string might contains 1, 2, 3, 4, 5 or 6 patterns.


A bit late to the party, but I wrote a library called MessageFormat.NET that handles this.

var str = @"You have {lives, plural, 
                     zero {no lives} 
                      one {one life} 
                    other {# lives}
            } left.";
var result = MessageFormatter.Format(str, new {
    lives = 1337
});

The whitespace in the string surrounding the text is not required, but merely for readability.

This is great when translating, as languages have different rules when it comes to pluralization.


Looking at how strings are typically written that account for simultaneous single and multiple values, e.g.

"Expected {0} file(s), but found {1} file(s)."

The approach I took was to keep the same string, but add some parsing to determine whether the (s) should be removed completely, or to keep the s inside the round brackets. For irregular words, a slash can separate the singular and plural form.

Other examples:

  • "Had {0:n2} child(ren)".Pluralize(1.0) => "Had 1.00 child"
  • "Had {0} cherry(ies)".Pluralize(2) => "Had 2 cherries"
  • "Had {0} calf/calves".Pluralize(1) => "Had 1 calf"
  • "Had {0} son(s)-in-law".Pluralize(2) => "Had 2 sons-in-law"
  • "Had {0} able seaman/seamen".Pluralize(1) => "Had 1 able seaman"
  • "Had {0} sheep, {1} goat(s)".Pluralize(1, 2) => "Had 1 sheep, 2 goats"
///<summary>
/// Examples:
/// "{0} file(s)".Pluralize(1); -> "1 file"
/// "{0} file(s)".Pluralize(2); -> "2 files"
///</summary>
public static String Pluralize(this String s, params Object[] counts) {
    String[] arr = s.Split(new [] { ' ' }, StringSplitOptions.None);
    for (int i = 0; i < arr.Length; i++) {
        String t = arr[i];
        if (t.Length == 0 || t[0] != '{' || t[t.Length - 1] != '}')
            continue;

        int w = 1;
        while (w < t.Length) {
            char c = t[w];
            if (c < '0' || c > '9')
                break;
            w++;
        }

        if (w == 1)
            continue;

        int n = int.Parse(t.Substring(1, w-1));
        if (n >= counts.Length)
            continue;

        Object o = counts[n];
        if (o == null)
            continue;

        bool isSingle = false;
        if (o is int)
            isSingle = 1 == (int) o;
        else if (o is double)
            isSingle = 1 == (double) o;
        else if (o is float)
            isSingle = 1 == (float) o;
        else if (o is decimal)
            isSingle = 1 == (decimal) o;
        else if (o is byte)
            isSingle = 1 == (byte) o;
        else if (o is sbyte)
            isSingle = 1 == (sbyte) o;
        else if (o is short)
            isSingle = 1 == (short) o;
        else if (o is ushort)
            isSingle = 1 == (ushort) o;
        else if (o is uint)
            isSingle = 1 == (uint) o;
        else if (o is long)
            isSingle = 1 == (long) o;
        else if (o is ulong)
            isSingle = 1 == (ulong) o;
        else
            continue;

        for (int j = i + 1; j < arr.Length && j < i + 4; j++) {
            String u = arr[j];
            if (u.IndexOf('{') >= 0)
                break; // couldn't find plural word and ran into next token

            int b1 = u.IndexOf('(');
            int b2 = u.IndexOf(')', b1 + 1);
            if (b1 >= 0 && b2 >= 0) {
                String u1 = u.Substring(0, b1);
                String u2 = u.Substring(b2+1);
                char last = (u1.Length > 0 ? u1[u1.Length - 1] : ' ');
                String v = (isSingle ? "" : u.Substring(b1+1, (b2 - b1) - 1));
                if ((last == 'y' || last == 'Y') && String.Compare(v, "ies", true) == 0)
                    u1 = u1.TrimEnd('y', 'Y');

                arr[j] = u1 + v + u2;
                break;
            }
            int s1 = u.IndexOf('/');
            if (s1 >= 0) {
                arr[j] = (isSingle ? u.Substring(0, s1) : u.Substring(s1 + 1));
                break;
            }
        }
    }

    s = String.Join(" ", arr);
    s = String.Format(s, counts);
    return s;
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜