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 3 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
243 changes: 125 additions & 118 deletions src/Humanizer.Tests/InflectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,43 +23,44 @@

using System.Collections;
using System.Collections.Generic;
using Humanizer.Inflections;
using Xunit;
using Xunit.Extensions;

namespace Humanizer.Tests
{
public class InflectorTests
public class InflectorTests
{
public readonly IList<object[]> PluralTestData = new List<object[]>();

[Theory]
[ClassData(typeof(PluralTestSource))]
[ClassData(typeof(InflectorTestSource))]
public void Pluralize(string singular, string plural)
{
Assert.Equal(plural, singular.Pluralize());
Assert.Equal(plural, Vocabularies.Default.Pluralize(singular));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why did you changed the actual test implementation?
Those are inflector extension method tests, they should test them, as they did before.

Copy link
Author

Choose a reason for hiding this comment

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

Vocabularies.Default now contains that big list of words, so the big list of words are tested against Vocabularies.Default. Inflector now just delegates a call to Vocabularies.Default. (If it were possible to verify that method call cleanly, I'd do that, but testing for equality here is pretty close.)

My original implementation allowed the user to pass a Vocabulary to Pluralize(), etc, (e.g. a dictionary in another language) but I took this out before submitting the PR as it was overkill for the immediate problem.

I'll be happy to get the tests however they need to be, but I just wanted to make sure my reasoning was clear. Please let me know how to proceed.

Copy link
Collaborator

Choose a reason for hiding this comment

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

..., but testing for equality here is pretty close.

Not really, the test now verifies only that Vocabularies.Default can process mouse/mice. It's relation to Inflector is gone, as you changed the test. You can change the implementation of the Inflector to anything and this test wouldn't break.

}

[Theory]
[ClassData(typeof(PluralTestSource))]
[ClassData(typeof(InflectorTestSource))]
public void PluralizeWordsWithUnknownPlurality(string singular, string plural)
{
Assert.Equal(plural, plural.Pluralize(false));
Assert.Equal(plural, singular.Pluralize(false));
Assert.Equal(plural, Vocabularies.Default.Pluralize(plural, false));
Assert.Equal(plural, Vocabularies.Default.Pluralize(singular, false));
}

[Theory]
[ClassData(typeof(PluralTestSource))]
[ClassData(typeof(InflectorTestSource))]
public void Singularize(string singular, string plural)
{
Assert.Equal(singular, plural.Singularize());
Assert.Equal(singular, Vocabularies.Default.Singularize(plural));
}

[Theory]
[ClassData(typeof(PluralTestSource))]
[ClassData(typeof(InflectorTestSource))]
public void SingularizeWordsWithUnknownSingularity(string singular, string plural)
{
Assert.Equal(singular, singular.Singularize(false));
Assert.Equal(singular, plural.Singularize(false));
Assert.Equal(singular, Vocabularies.Default.Singularize(singular, false));
Assert.Equal(singular, Vocabularies.Default.Singularize(plural, false));
}

//Uppercases individual words and removes some characters
Expand Down Expand Up @@ -136,122 +137,128 @@ public void Underscore(string input, string expectedOuput)
}
}

class PluralTestSource : IEnumerable<object[]>
class InflectorTestSource : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[] {"search", "searches"};
yield return new object[] {"switch", "switches"};
yield return new object[] {"fix", "fixes"};
yield return new object[] {"box", "boxes"};
yield return new object[] {"process", "processes"};
yield return new object[] {"address", "addresses"};
yield return new object[] {"case", "cases"};
yield return new object[] {"stack", "stacks"};
yield return new object[] {"wish", "wishes"};
yield return new object[] {"fish", "fish"};

yield return new object[] {"category", "categories"};
yield return new object[] {"query", "queries"};
yield return new object[] {"ability", "abilities"};
yield return new object[] {"agency", "agencies"};
yield return new object[] {"movie", "movies"};

yield return new object[] {"archive", "archives"};

yield return new object[] {"index", "indices"};

yield return new object[] {"wife", "wives"};
yield return new object[] {"safe", "saves"};
yield return new object[] {"half", "halves"};

yield return new object[] {"move", "moves"};

yield return new object[] {"salesperson", "salespeople"};
yield return new object[] {"person", "people"};

yield return new object[] {"spokesman", "spokesmen"};
yield return new object[] {"man", "men"};
yield return new object[] {"woman", "women"};

yield return new object[] {"basis", "bases"};
yield return new object[] {"diagnosis", "diagnoses"};

yield return new object[] {"datum", "data"};
yield return new object[] {"medium", "media"};
yield return new object[] {"analysis", "analyses"};

yield return new object[] {"node_child", "node_children"};
yield return new object[] {"child", "children"};

yield return new object[] {"experience", "experiences"};
yield return new object[] {"day", "days"};

yield return new object[] {"comment", "comments"};
yield return new object[] {"foobar", "foobars"};
yield return new object[] {"newsletter", "newsletters"};

yield return new object[] {"old_news", "old_news"};
yield return new object[] {"news", "news"};

yield return new object[] {"series", "series"};
yield return new object[] {"species", "species"};

yield return new object[] {"quiz", "quizzes"};

yield return new object[] {"perspective", "perspectives"};

yield return new object[] {"ox", "oxen"};
yield return new object[] {"photo", "photos"};
yield return new object[] {"buffalo", "buffaloes"};
yield return new object[] {"tomato", "tomatoes"};
yield return new object[] {"dwarf", "dwarves"};
yield return new object[] {"elf", "elves"};
yield return new object[] {"information", "information"};
yield return new object[] {"equipment", "equipment"};
yield return new object[] {"bus", "buses"};
yield return new object[] {"status", "statuses"};
yield return new object[] {"status_code", "status_codes"};
yield return new object[] {"mouse", "mice"};

yield return new object[] {"louse", "lice"};
yield return new object[] {"house", "houses"};
yield return new object[] {"octopus", "octopi"};
yield return new object[] {"virus", "viri"};
yield return new object[] {"alias", "aliases"};
yield return new object[] {"portfolio", "portfolios"};
yield return new object[] {"criterion", "criteria"};

yield return new object[] {"vertex", "vertices"};
yield return new object[] {"matrix", "matrices"};

yield return new object[] {"axis", "axes"};
yield return new object[] {"testis", "testes"};
yield return new object[] {"crisis", "crises"};

yield return new object[] {"rice", "rice"};
yield return new object[] {"shoe", "shoes"};

yield return new object[] {"horse", "horses"};
yield return new object[] {"prize", "prizes"};
yield return new object[] {"edge", "edges"};
yield return new object[] { "search", "searches" };
yield return new object[] { "switch", "switches" };
yield return new object[] { "fix", "fixes" };
yield return new object[] { "box", "boxes" };
yield return new object[] { "process", "processes" };
yield return new object[] { "address", "addresses" };
yield return new object[] { "case", "cases" };
yield return new object[] { "stack", "stacks" };
yield return new object[] { "wish", "wishes" };
yield return new object[] { "fish", "fish" };

yield return new object[] { "category", "categories" };
yield return new object[] { "query", "queries" };
yield return new object[] { "ability", "abilities" };
yield return new object[] { "agency", "agencies" };
yield return new object[] { "movie", "movies" };

yield return new object[] { "archive", "archives" };

yield return new object[] { "index", "indices" };

yield return new object[] { "wife", "wives" };
yield return new object[] { "safe", "saves" };
yield return new object[] { "half", "halves" };

yield return new object[] { "move", "moves" };

yield return new object[] { "salesperson", "salespeople" };
yield return new object[] { "person", "people" };

yield return new object[] { "spokesman", "spokesmen" };
yield return new object[] { "man", "men" };
yield return new object[] { "woman", "women" };

yield return new object[] { "basis", "bases" };
yield return new object[] { "diagnosis", "diagnoses" };

yield return new object[] { "datum", "data" };
yield return new object[] { "medium", "media" };
yield return new object[] { "analysis", "analyses" };

yield return new object[] { "node_child", "node_children" };
yield return new object[] { "child", "children" };

yield return new object[] { "experience", "experiences" };
yield return new object[] { "day", "days" };

yield return new object[] { "comment", "comments" };
yield return new object[] { "foobar", "foobars" };
yield return new object[] { "newsletter", "newsletters" };

yield return new object[] { "old_news", "old_news" };
yield return new object[] { "news", "news" };

yield return new object[] { "series", "series" };
yield return new object[] { "species", "species" };

yield return new object[] { "quiz", "quizzes" };

yield return new object[] { "perspective", "perspectives" };

yield return new object[] { "ox", "oxen" };
yield return new object[] { "photo", "photos" };
yield return new object[] { "buffalo", "buffaloes" };
yield return new object[] { "tomato", "tomatoes" };
yield return new object[] { "dwarf", "dwarves" };
yield return new object[] { "elf", "elves" };
yield return new object[] { "information", "information" };
yield return new object[] { "equipment", "equipment" };
yield return new object[] { "bus", "buses" };
yield return new object[] { "status", "statuses" };
yield return new object[] { "status_code", "status_codes" };
yield return new object[] { "mouse", "mice" };

yield return new object[] { "louse", "lice" };
yield return new object[] { "house", "houses" };
yield return new object[] { "octopus", "octopi" };
yield return new object[] { "virus", "viri" };
yield return new object[] { "alias", "aliases" };
yield return new object[] { "portfolio", "portfolios" };
yield return new object[] { "criterion", "criteria" };

yield return new object[] { "vertex", "vertices" };
yield return new object[] { "matrix", "matrices" };

yield return new object[] { "axis", "axes" };
yield return new object[] { "testis", "testes" };
yield return new object[] { "crisis", "crises" };

yield return new object[] { "rice", "rice" };
yield return new object[] { "shoe", "shoes" };

yield return new object[] { "horse", "horses" };
yield return new object[] { "prize", "prizes" };
yield return new object[] { "edge", "edges" };

/* Tests added by Bas Jansen */
yield return new object[] {"goose", "geese"};
yield return new object[] {"deer", "deer"};
yield return new object[] {"sheep", "sheep"};
yield return new object[] {"wolf", "wolves"};
yield return new object[] {"volcano", "volcanoes"};
yield return new object[] {"aircraft", "aircraft"};
yield return new object[] {"alumna", "alumnae"};
yield return new object[] {"alumnus", "alumni"};
yield return new object[] {"fungus", "fungi"};
yield return new object[] { "goose", "geese" };
yield return new object[] { "deer", "deer" };
yield return new object[] { "sheep", "sheep" };
yield return new object[] { "wolf", "wolves" };
yield return new object[] { "volcano", "volcanoes" };
yield return new object[] { "aircraft", "aircraft" };
yield return new object[] { "alumna", "alumnae" };
yield return new object[] { "alumnus", "alumni" };
yield return new object[] { "fungus", "fungi" };

yield return new object[] {"wave","waves"};
yield return new object[] { "wave", "waves" };

yield return new object[] {"campus", "campuses"};
yield return new object[] { "campus", "campuses" };

yield return new object[] { "is", "are" };

// Units of measurement:
yield return new object[] { "oz", "oz" };
yield return new object[] { "tsp", "tsp" };
yield return new object[] { "ml", "ml" };
yield return new object[] { "l", "l" };
}

IEnumerator IEnumerable.GetEnumerator()
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
Loading