diff --git a/readme.md b/readme.md index 6359380af..297fd71aa 100644 --- a/readme.md +++ b/readme.md @@ -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 diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt index 9a3753c46..4ccf60334 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt @@ -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 @@ -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) { } @@ -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 { diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt index bb3920e7f..80d983e16 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt @@ -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 @@ -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) { } @@ -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 { diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt index 44794d215..a30274ffa 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt @@ -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 @@ -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) { } @@ -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 { diff --git a/src/Humanizer.Tests/InflectorTests.cs b/src/Humanizer.Tests/InflectorTests.cs index 3518355ec..a20c7a533 100644 --- a/src/Humanizer.Tests/InflectorTests.cs +++ b/src/Humanizer.Tests/InflectorTests.cs @@ -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 { @@ -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 diff --git a/src/Humanizer/Inflections/PossesiveSuffixOverride.cs b/src/Humanizer/Inflections/PossesiveSuffixOverride.cs new file mode 100644 index 000000000..2ef712bca --- /dev/null +++ b/src/Humanizer/Inflections/PossesiveSuffixOverride.cs @@ -0,0 +1,8 @@ +namespace Humanizer; + +public enum PossesiveSuffixOverride +{ + S_ONLY, + APOSTROPHE_ONLY, + APOSTROPHE_S +} diff --git a/src/Humanizer/Inflections/Vocabulary.cs b/src/Humanizer/Inflections/Vocabulary.cs index c818a864f..b8018a8fc 100644 --- a/src/Humanizer/Inflections/Vocabulary.cs +++ b/src/Humanizer/Inflections/Vocabulary.cs @@ -1,3 +1,5 @@ +using Humanizer; + namespace Humanizer; /// @@ -16,6 +18,11 @@ internal Vocabulary() readonly HashSet uncountables = new(StringComparer.CurrentCultureIgnoreCase); readonly Regex letterS = new("^([sS])[sS]*$"); + readonly Dictionary possessiveExceptionMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "it", 's' }, + }; + /// /// Adds a word to the vocabulary which cannot easily be pluralized/singularized by RegEx, e.g. "person" and "people". /// @@ -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 rules, string? word, bool skipFirstRule) { if (word == null) diff --git a/src/Humanizer/InflectorExtensions.cs b/src/Humanizer/InflectorExtensions.cs index b129372e8..afe67d499 100644 --- a/src/Humanizer/InflectorExtensions.cs +++ b/src/Humanizer/InflectorExtensions.cs @@ -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 @@ -100,4 +102,14 @@ public static string Hyphenate(this string underscoredWord) => public static string Kebaberize(this string input) => Underscore(input) .Dasherize(); + + /// + /// 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. + /// + /// Word to convert to possessive + /// Custom possessive suffix that user may want to override with + [return: NotNullIfNotNull(nameof(word))] + public static string? ToPossessive(this string? word, PossesiveSuffixOverride? suffix = null) => + Vocabularies.Default.ToPossessive(word, suffix); } \ No newline at end of file