Skip to content
Open
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
13 changes: 13 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,19 @@ 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 was removed in version 2.0.

#### ToPossessive
`ToPossessive` converts word to its possesive form.

By default it appends "'" to words ending with 's' and "'s" for all other words.

This rule may differ between different versions of english, in which case user can override the default for e.g. adding "'s" even for words ending with 's' -

```
"Jess".ToPossessive() => "Jess'"
"Jess".ToPossessive(PossessiveSuffixOverride.APOSTROPHE_S) => "Jess's"
```



## Adding Words

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,8 @@ namespace Humanizer
public static string? Pluralize(this string? word, bool inputIsKnownToBeSingular = true) { }
public static string Singularize(this string word, bool inputIsKnownToBePlural = true, bool skipSimpleWords = false) { }
public static string Titleize(this string input) { }
[return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull("word")]
public static string? ToPossessive(this string? word, Humanizer.PossesiveSuffixOverride? suffix = default) { }
public static string Underscore(this string input) { }
}
public enum LetterCasing
Expand Down Expand Up @@ -1805,6 +1807,12 @@ namespace Humanizer
Plural = 1,
CouldBeEither = 2,
}
public enum PossesiveSuffixOverride
Copy link
Member

Choose a reason for hiding this comment

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

Please use PascalCase for the enumeration values.

{
S_ONLY = 0,
APOSTROPHE_ONLY = 1,
APOSTROPHE_S = 2,
}
public class PrecisionDateOnlyHumanizeStrategy : Humanizer.IDateOnlyHumanizeStrategy
{
public PrecisionDateOnlyHumanizeStrategy(double precision = 0.75) { }
Expand Down Expand Up @@ -1964,6 +1972,7 @@ namespace Humanizer
public string? Pluralize(string? word, bool inputIsKnownToBeSingular = true) { }
[return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull("word")]
public string? Singularize(string? word, bool inputIsKnownToBePlural = true, bool skipSimpleWords = false) { }
public string? ToPossessive(string? word, Humanizer.PossesiveSuffixOverride? possessiveSuffix) { }
}
public enum WordForm
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,8 @@ namespace Humanizer
public static string? Pluralize(this string? word, bool inputIsKnownToBeSingular = true) { }
public static string Singularize(this string word, bool inputIsKnownToBePlural = true, bool skipSimpleWords = false) { }
public static string Titleize(this string input) { }
[return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull("word")]
public static string? ToPossessive(this string? word, Humanizer.PossesiveSuffixOverride? suffix = default) { }
public static string Underscore(this string input) { }
}
public enum LetterCasing
Expand Down Expand Up @@ -1805,6 +1807,12 @@ namespace Humanizer
Plural = 1,
CouldBeEither = 2,
}
public enum PossesiveSuffixOverride
{
S_ONLY = 0,
APOSTROPHE_ONLY = 1,
APOSTROPHE_S = 2,
}
public class PrecisionDateOnlyHumanizeStrategy : Humanizer.IDateOnlyHumanizeStrategy
{
public PrecisionDateOnlyHumanizeStrategy(double precision = 0.75) { }
Expand Down Expand Up @@ -1964,6 +1972,7 @@ namespace Humanizer
public string? Pluralize(string? word, bool inputIsKnownToBeSingular = true) { }
[return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull("word")]
public string? Singularize(string? word, bool inputIsKnownToBePlural = true, bool skipSimpleWords = false) { }
public string? ToPossessive(string? word, Humanizer.PossesiveSuffixOverride? possessiveSuffix) { }
}
public enum WordForm
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,8 @@ namespace Humanizer
public static string? Pluralize(this string? word, bool inputIsKnownToBeSingular = true) { }
public static string Singularize(this string word, bool inputIsKnownToBePlural = true, bool skipSimpleWords = false) { }
public static string Titleize(this string input) { }
[return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull("word")]
public static string? ToPossessive(this string? word, Humanizer.PossesiveSuffixOverride? suffix = default) { }
public static string Underscore(this string input) { }
}
public enum LetterCasing
Expand Down Expand Up @@ -1156,6 +1158,12 @@ namespace Humanizer
Plural = 1,
CouldBeEither = 2,
}
public enum PossesiveSuffixOverride
{
S_ONLY = 0,
APOSTROPHE_ONLY = 1,
APOSTROPHE_S = 2,
}
public class PrecisionDateTimeHumanizeStrategy : Humanizer.IDateTimeHumanizeStrategy
{
public PrecisionDateTimeHumanizeStrategy(double precision = 0.75) { }
Expand Down Expand Up @@ -1301,6 +1309,7 @@ namespace Humanizer
public string? Pluralize(string? word, bool inputIsKnownToBeSingular = true) { }
[return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull("word")]
public string? Singularize(string? word, bool inputIsKnownToBePlural = true, bool skipSimpleWords = false) { }
public string? ToPossessive(string? word, Humanizer.PossesiveSuffixOverride? possessiveSuffix) { }
}
public enum WordForm
{
Expand Down
49 changes: 49 additions & 0 deletions src/Humanizer.Tests/InflectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
//CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System.Collections;
using Humanizer;

public class InflectorTests
{
Expand Down Expand Up @@ -148,6 +149,54 @@ public void Underscore(string input, string expectedOutput) =>
[InlineData("A VeryShortSENTENCE", "a-very-short-sentence")]
public void Kebaberize(string input, string expectedOutput) =>
Assert.Equal(expectedOutput, input.Kebaberize());



[Theory]
[InlineData("James", "James'")]
[InlineData("cat", "cat's")]
[InlineData("boss", "boss'")]
[InlineData("child", "child's")]
[InlineData("dogs", "dogs'")]
[InlineData("Chris", "Chris'")]
[InlineData("class", "class'")]
[InlineData("fox", "fox's")]
[InlineData("baby", "baby's")]
[InlineData("children", "children's")]
[InlineData("iT", "iTs")]
[InlineData("it", "its")]
[InlineData("IT", "ITs")]
[InlineData("It", "Its")]
[InlineData("", "")]
[InlineData(" ", " ")]
public void ToPossessive_DefaultSuffix_Works(string input, string expected) =>
Assert.Equal(expected, input.ToPossessive());

[Theory]
[InlineData("James", PossesiveSuffixOverride.APOSTROPHE_S, "James's")]
[InlineData("iT", PossesiveSuffixOverride.APOSTROPHE_S, "iT's")]
[InlineData("it", PossesiveSuffixOverride.APOSTROPHE_S, "it's")]
[InlineData("IT", PossesiveSuffixOverride.APOSTROPHE_S, "IT's")]
[InlineData("It", PossesiveSuffixOverride.APOSTROPHE_S, "It's")]
[InlineData("", PossesiveSuffixOverride.APOSTROPHE_S, "")]
[InlineData(" ", PossesiveSuffixOverride.APOSTROPHE_S, " ")]
[InlineData("Bit", PossesiveSuffixOverride.S_ONLY, "Bits")]
[InlineData("iT", PossesiveSuffixOverride.S_ONLY, "iTs")]
[InlineData("it", PossesiveSuffixOverride.S_ONLY, "its")]
[InlineData("IT", PossesiveSuffixOverride.S_ONLY, "ITs")]
[InlineData("It", PossesiveSuffixOverride.S_ONLY, "Its")]
[InlineData("", PossesiveSuffixOverride.S_ONLY, "")]
[InlineData(" ", PossesiveSuffixOverride.S_ONLY, " ")]
[InlineData("Bit", PossesiveSuffixOverride.APOSTROPHE_ONLY, "Bit'")]
[InlineData("iT", PossesiveSuffixOverride.APOSTROPHE_ONLY, "iT'")]
[InlineData("it", PossesiveSuffixOverride.APOSTROPHE_ONLY, "it'")]
[InlineData("IT", PossesiveSuffixOverride.APOSTROPHE_ONLY, "IT'")]
[InlineData("It", PossesiveSuffixOverride.APOSTROPHE_ONLY, "It'")]
[InlineData("", PossesiveSuffixOverride.APOSTROPHE_ONLY, "")]
[InlineData(" ", PossesiveSuffixOverride.APOSTROPHE_ONLY, " ")]
public void ToPossessive_WithExplicitSuffix_AppliesSuffix(string input, PossesiveSuffixOverride suffix, string expected) =>
// Assuming PossesiveSuffix is an enum with at least ApostropheS and Apostrophe
Assert.Equal(expected, input.ToPossessive(suffix));
}

class PluralTestSource : IEnumerable<object[]>
Expand Down
8 changes: 8 additions & 0 deletions src/Humanizer/Inflections/PossesiveSuffixOverride.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Humanizer;

public enum PossesiveSuffixOverride
{
S_ONLY,
APOSTROPHE_ONLY,
APOSTROPHE_S
}
37 changes: 37 additions & 0 deletions src/Humanizer/Inflections/Vocabulary.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Humanizer;

namespace Humanizer;

/// <summary>
Expand All @@ -16,6 +18,11 @@ internal Vocabulary()
readonly HashSet<string> uncountables = new(StringComparer.CurrentCultureIgnoreCase);
readonly Regex letterS = new("^([sS])[sS]*$");

readonly Dictionary<string, char> possessiveExceptionMap = new Dictionary<string, char>(StringComparer.OrdinalIgnoreCase)
{
{ "it", 's' },
};

/// <summary>
/// Adds a word to the vocabulary which cannot easily be pluralized/singularized by RegEx, e.g. "person" and "people".
/// </summary>
Expand Down Expand Up @@ -145,6 +152,36 @@ public void AddSingular(string rule, string replacement) =>
return word;
}

public string? ToPossessive(string? word, PossesiveSuffixOverride? possessiveSuffix)
{
if (word == null || string.IsNullOrWhiteSpace(word))
{
return word;
}

switch (possessiveSuffix)
{
case PossesiveSuffixOverride.S_ONLY:
return StringHumanizeExtensions.Concat(word.AsSpan(0, word.Length), 's');
case PossesiveSuffixOverride.APOSTROPHE_ONLY:
return StringHumanizeExtensions.Concat(word.AsSpan(0, word.Length), '\'');
case PossesiveSuffixOverride.APOSTROPHE_S:
return StringHumanizeExtensions.Concat(word.AsSpan(0, word.Length), "'s".AsSpan());
}

if (possessiveExceptionMap.TryGetValue(word, out var suffix))
{
return StringHumanizeExtensions.Concat(word.AsSpan(0, word.Length), suffix);
}

if (word[word.Length - 1] == 's')
{
return StringHumanizeExtensions.Concat(word.AsSpan(0, word.Length), '\'');
}

return StringHumanizeExtensions.Concat(word.AsSpan(0, word.Length), "'s".AsSpan());
}

string? ApplyRules(IList<Rule> rules, string? word, bool skipFirstRule)
{
if (word == null)
Expand Down
12 changes: 12 additions & 0 deletions src/Humanizer/InflectorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
//IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
//CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using Humanizer;

namespace Humanizer;

public static class InflectorExtensions
Expand Down Expand Up @@ -100,4 +102,14 @@ public static string Hyphenate(this string underscoredWord) =>
public static string Kebaberize(this string input) =>
Underscore(input)
.Dasherize();

/// <summary>
/// Converts the input to possessive form, e.g. "cat" -> "cat's", "dogs" -> "dogs'", "child" -> "child's"
/// Also supports custom possessive suffixes via the PossesiveSuffixOverride parameter.
/// </summary>
/// <param name="word">Word to convert to possessive</param>
/// <param name="suffix">Custom possessive suffix that user may want to override with</param>
[return: NotNullIfNotNull(nameof(word))]
public static string? ToPossessive(this string? word, PossesiveSuffixOverride? suffix = null) =>
Vocabularies.Default.ToPossessive(word, suffix);
}