Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Break out Vocabulary/DefaultVocabulary to make Inflector more extensible. #408

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Humanizer meets all your .NET needs for manipulating and displaying strings, enu
- [Inflector methods](#inflector-methods)
- [Pluralize](#pluralize)
- [Singularize](#singularize)
- [Adding Words](#adding-words)
- [ToQuantity](#toquantity)
- [Ordinalize](#ordinalize)
- [Titleize](#titleize)
Expand Down Expand Up @@ -459,6 +460,25 @@ Normally you would call `Singularize` on a plural word but if you're unsure abou

The overload of `Singularize` with `plurality` argument is obsolete and will be removed in next major release.

##<a id="adding-words">Adding Words</a>
Sometimes, you may need to add a rule from the singularization/pluralization vocabulary (the examples below are already in the `DefaultVocabluary` used by `Inflector`):

```C#
// Adds a word to the vocabulary which cannot easily be pluralized/singularized by RegEx:
Vocabularies.Default.AddIrregular("person", "people");

// Adds an uncountable word to the vocabulary. Will be ignored when plurality is changed:
Vocabularies.Default.AddUncountable("fish");

// Adds a rule to the vocabulary that does not follow trivial rules for pluralization, e.g. "bus" -> "buses"
Vocabularies.Default.AddPlural("bus", "buses");

// Adds a rule to the vocabulary that does not follow trivial rules for singularization
// (will match both "vertices" -> "vertex" and "indices" -> "index"):
Vocabularies.Default.AddSingular("(vert|ind)ices$", "$1ex");

```

####<a id="toquantity">ToQuantity</a>
Many times you want to call `Singularize` and `Pluralize` to prefix a word with a number; e.g. "2 requests", "3 men". `ToQuantity` prefixes the provided word with the number and accordingly pluralizes or singularizes the word:

Expand Down
1 change: 1 addition & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
###In Development

[Commits](https://github.com/MehdiK/Humanizer/compare/v1.35.0...master)
- [#408](https://github.com/MehdiK/Humanizer/pull/408): Added support for adding/removing rules from singular/pluralization by adding `Vocabulary` class and `Vocabularies.Default`.

###v1.35.0 - 2015-03-29
- [#399](https://github.com/MehdiK/Humanizer/pull/399): Added support for humanizing DateTimeOffset
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,23 @@ public class In
public System.DateTime TheYear(int year) { }
}

public class Vocabularies
{
public Vocabularies() { }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please remove this default constructor from public surface?
This class isn't intended to have any instance, right?

public Humanizer.Inflections.Vocabulary Default { get; }
}

public class Vocabulary
{
public Vocabulary() { }
public void AddIrregular(string singular, string plural) { }
public void AddPlural(string rule, string replacement) { }
public void AddSingular(string rule, string replacement) { }
public void AddUncountable(string word) { }
public string Pluralize(string word, bool inputIsKnownToBeSingular) { }
public string Singularize(string word, bool inputIsKnownToBePlural) { }
}

public class InflectorExtensions
{
public string Camelize(string input) { }
Expand Down
4 changes: 3 additions & 1 deletion src/Humanizer.Tests/Humanizer.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Folder Include="Inflections\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
Expand Down
4 changes: 3 additions & 1 deletion src/Humanizer.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_IFELSE_BRACES_STYLE/@EntryValue">ONLY_FOR_MULTILINE</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
3 changes: 3 additions & 0 deletions src/Humanizer/Humanizer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
<Compile Include="DateTimeHumanizeStrategy\DefaultDateTimeOffsetHumanizeStrategy.cs" />
<Compile Include="DateTimeHumanizeStrategy\IDateTimeOffsetHumanizeStrategy.cs" />
<Compile Include="DateTimeHumanizeStrategy\PrecisionDateTimeOffsetHumanizeStrategy.cs" />
<Compile Include="Inflections\Vocabularies.cs" />
<Compile Include="Inflections\Vocabulary.cs" />
<Compile Include="Localisation\CollectionFormatters\DefaultCollectionFormatter.cs" />
<Compile Include="Localisation\CollectionFormatters\ICollectionFormatter.cs" />
<Compile Include="Localisation\CollectionFormatters\OxfordStyleCollectionFormatter.cs" />
Expand Down Expand Up @@ -112,6 +114,7 @@
<Compile Include="Localisation\Ordinalizers\ItalianOrdinalizer.cs" />
<Compile Include="Localisation\Tense.cs" />
<Compile Include="Localisation\NumberToWords\SpanishNumberToWordsConverter.cs" />
<Compile Include="Plurality.cs" />
<Compile Include="RegexOptionsUtil.cs" />
<Compile Include="TimeSpanHumanizeExtensions.cs" />
<Compile Include="FluentDate\In.SomeTimeFrom.cs">
Expand Down
104 changes: 104 additions & 0 deletions src/Humanizer/Inflections/Vocabularies.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
namespace Humanizer.Inflections
{
/// <summary>
/// Container for registered Vocabularies. At present, only a single vocabulary is supported: Default.
/// </summary>
public class Vocabularies
{
private static Vocabulary _default;

/// <summary>
/// The default vocabulary used for singular/plural irregularities.
/// Rules can be added to this vocabulary and will be picked up by called to Singularize() and Pluralize().
/// At this time, multiple vocabularies and removing existing rules are not supported.
/// </summary>
public static Vocabulary Default
{
get
{
if (_default == null)
BuildDefault();

return _default;
}
}

private static void BuildDefault()
{
_default = new Vocabulary();

_default.AddPlural("$", "s");
_default.AddPlural("s$", "s");
_default.AddPlural("(ax|test)is$", "$1es");
_default.AddPlural("(octop|vir|alumn|fung)us$", "$1i");
_default.AddPlural("(alias|status)$", "$1es");
_default.AddPlural("(bu)s$", "$1ses");
_default.AddPlural("(buffal|tomat|volcan)o$", "$1oes");
_default.AddPlural("([ti])um$", "$1a");
_default.AddPlural("sis$", "ses");
_default.AddPlural("(?:([^f])fe|([lr])f)$", "$1$2ves");
_default.AddPlural("(hive)$", "$1s");
_default.AddPlural("([^aeiouy]|qu)y$", "$1ies");
_default.AddPlural("(x|ch|ss|sh)$", "$1es");
_default.AddPlural("(matr|vert|ind)ix|ex$", "$1ices");
_default.AddPlural("([m|l])ouse$", "$1ice");
_default.AddPlural("^(ox)$", "$1en");
_default.AddPlural("(quiz)$", "$1zes");
_default.AddPlural("(campus)$", "$1es");
_default.AddPlural("^is$", "are");

_default.AddSingular("s$", "");
_default.AddSingular("(n)ews$", "$1ews");
_default.AddSingular("([ti])a$", "$1um");
_default.AddSingular("((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$", "$1$2sis");
_default.AddSingular("(^analy)ses$", "$1sis");
_default.AddSingular("([^f])ves$", "$1fe");
_default.AddSingular("(hive)s$", "$1");
_default.AddSingular("(tive)s$", "$1");
_default.AddSingular("([lr])ves$", "$1f");
_default.AddSingular("([^aeiouy]|qu)ies$", "$1y");
_default.AddSingular("(s)eries$", "$1eries");
_default.AddSingular("(m)ovies$", "$1ovie");
_default.AddSingular("(x|ch|ss|sh)es$", "$1");
_default.AddSingular("([m|l])ice$", "$1ouse");
_default.AddSingular("(bus)es$", "$1");
_default.AddSingular("(o)es$", "$1");
_default.AddSingular("(shoe)s$", "$1");
_default.AddSingular("(cris|ax|test)es$", "$1is");
_default.AddSingular("(octop|vir|alumn|fung)i$", "$1us");
_default.AddSingular("(alias|status)es$", "$1");
_default.AddSingular("^(ox)en", "$1");
_default.AddSingular("(vert|ind)ices$", "$1ex");
_default.AddSingular("(matr)ices$", "$1ix");
_default.AddSingular("(quiz)zes$", "$1");
_default.AddSingular("(campus)es$", "$1");
_default.AddSingular("^are$", "is");

_default.AddIrregular("person", "people");
_default.AddIrregular("man", "men");
_default.AddIrregular("child", "children");
_default.AddIrregular("sex", "sexes");
_default.AddIrregular("move", "moves");
_default.AddIrregular("goose", "geese");
_default.AddIrregular("alumna", "alumnae");
_default.AddIrregular("criterion", "criteria");
_default.AddIrregular("wave", "waves");

_default.AddUncountable("equipment");
_default.AddUncountable("information");
_default.AddUncountable("rice");
_default.AddUncountable("money");
_default.AddUncountable("species");
_default.AddUncountable("series");
_default.AddUncountable("fish");
_default.AddUncountable("sheep");
_default.AddUncountable("deer");
_default.AddUncountable("aircraft");
_default.AddUncountable("oz");
_default.AddUncountable("tsp");
_default.AddUncountable("tbsp");
_default.AddUncountable("ml");
_default.AddUncountable("l");
}
}
}
147 changes: 147 additions & 0 deletions src/Humanizer/Inflections/Vocabulary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace Humanizer.Inflections
{
/// <summary>
/// A container for exceptions to simple pluralization/singularization rules.
/// Vocabularies.Default contains an extensive list of rules for US English.
/// At this time, multiple vocabularies and removing existing rules are not supported.
/// </summary>
public class Vocabulary
{
private readonly List<Rule> _plurals = new List<Rule>();
private readonly List<Rule> _singulars = new List<Rule>();
private readonly List<string> _uncountables = new List<string>();

/// <summary>
/// Adds a word to the vocabulary which cannot easily be pluralized/singularized by RegEx, e.g. "person" and "people".
/// </summary>
/// <param name="singular">The singular form of the irregular word, e.g. "person".</param>
/// <param name="plural">The plural form of the irregular word, e.g. "people".</param>
public void AddIrregular(string singular, string plural)
{
AddPlural("(" + singular[0] + ")" + singular.Substring(1) + "$", "$1" + plural.Substring(1));
AddSingular("(" + plural[0] + ")" + plural.Substring(1) + "$", "$1" + singular.Substring(1));
}

/// <summary>
/// Adds an uncountable word to the vocabulary, e.g. "fish". Will be ignored when plurality is changed.
/// </summary>
/// <param name="word">Word to be added to the list of uncountables.</param>
public void AddUncountable(string word)
{
_uncountables.Add(word.ToLower());
}

/// <summary>
/// Adds a rule to the vocabulary that does not follow trivial rules for pluralization, e.g. "bus" -> "buses"
/// </summary>
/// <param name="rule">RegEx to be matched, case insensitive, e.g. "(bus)es$"</param>
/// <param name="replacement">RegEx replacement e.g. "$1"</param>
public void AddPlural(string rule, string replacement)
{
_plurals.Add(new Rule(rule, replacement));
}

/// <summary>
/// Adds a rule to the vocabulary that does not follow trivial rules for singularization, e.g. "vertices/indices -> "vertex/index"
/// </summary>
/// <param name="rule">RegEx to be matched, case insensitive, e.g. ""(vert|ind)ices$""</param>
/// <param name="replacement">RegEx replacement e.g. "$1ex"</param>
public void AddSingular(string rule, string replacement)
{
_singulars.Add(new Rule(rule, replacement));
}

/// <summary>
/// Pluralizes the provided input considering irregular words
/// </summary>
/// <param name="word">Word to be pluralized</param>
/// <param name="inputIsKnownToBeSingular">Normally you call Pluralize on singular words; but if you're unsure call it with false</param>
/// <returns></returns>
public string Pluralize(string word, bool inputIsKnownToBeSingular = true)
{
var result = ApplyRules(_plurals, word);

if (inputIsKnownToBeSingular)
return result;

var asSingular = ApplyRules(_singulars, word);
var asSingularAsPlural = ApplyRules(_plurals, asSingular);
if (asSingular != null && asSingular != word && asSingular + "s" != word && asSingularAsPlural == word && result != word)
return word;

return result;
}

/// <summary>
/// Singularizes the provided input considering irregular words
/// </summary>
/// <param name="word">Word to be singularized</param>
/// <param name="inputIsKnownToBePlural">Normally you call Singularize on plural words; but if you're unsure call it with false</param>
/// <returns></returns>
public string Singularize(string word, bool inputIsKnownToBePlural = true)
{

var result = ApplyRules(_singulars, word);

if (inputIsKnownToBePlural)
return result;

// the Plurality is unknown so we should check all possibilities
var asPlural = ApplyRules(_plurals, word);
var asPluralAsSingular = ApplyRules(_singulars, asPlural);
if (asPlural != word && word + "s" != asPlural && asPluralAsSingular == word && result != word)
return word;

return result ?? word;
}

private string ApplyRules(IList<Rule> rules, string word)
{
if (word == null)
return null;

if (IsUncountable(word))
return word;

var result = word;
for (int i = rules.Count - 1; i >= 0; i--)
{
if ((result = rules[i].Apply(word)) != null)
{
break;
}
}
return result;
}

private bool IsUncountable(string word)
{
return _uncountables.Contains(word.ToLower());
}

private class Rule
{
private readonly Regex _regex;
private readonly string _replacement;

public Rule(string pattern, string replacement)
{
_regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptionsUtil.Compiled);
_replacement = replacement;
}

public string Apply(string word)
{
if (!_regex.IsMatch(word))
{
return null;
}

return _regex.Replace(word, _replacement);
}
}
}
}
Loading