|
|
@ -1,8 +1,12 @@ |
|
|
|
using System.Collections.Generic; |
|
|
|
using System; |
|
|
|
|
|
|
|
using System.Collections.Generic; |
|
|
|
|
|
|
|
using System.Diagnostics.CodeAnalysis; |
|
|
|
using System.Text.RegularExpressions; |
|
|
|
using System.Text.RegularExpressions; |
|
|
|
|
|
|
|
|
|
|
|
namespace Humanizer.Inflections |
|
|
|
namespace Humanizer.Inflections; |
|
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
#nullable enable |
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// <summary>
|
|
|
|
/// A container for exceptions to simple pluralization/singularization rules.
|
|
|
|
/// A container for exceptions to simple pluralization/singularization rules.
|
|
|
|
/// Vocabularies.Default contains an extensive list of rules for US English.
|
|
|
|
/// Vocabularies.Default contains an extensive list of rules for US English.
|
|
|
@ -14,9 +18,10 @@ namespace Humanizer.Inflections |
|
|
|
{ |
|
|
|
{ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private readonly List<Rule> _plurals = new List<Rule>(); |
|
|
|
readonly List<Rule> plurals = []; |
|
|
|
private readonly List<Rule> _singulars = new List<Rule>(); |
|
|
|
readonly List<Rule> singulars = []; |
|
|
|
private readonly List<string> _uncountables = new List<string>(); |
|
|
|
readonly HashSet<string> uncountables = new(StringComparer.CurrentCultureIgnoreCase); |
|
|
|
|
|
|
|
readonly Regex letterS = new("^([sS])[sS]*$"); |
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// <summary>
|
|
|
|
/// Adds a word to the vocabulary which cannot easily be pluralized/singularized by RegEx, e.g. "person" and "people".
|
|
|
|
/// Adds a word to the vocabulary which cannot easily be pluralized/singularized by RegEx, e.g. "person" and "people".
|
|
|
@ -28,8 +33,10 @@ namespace Humanizer.Inflections |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (matchEnding) |
|
|
|
if (matchEnding) |
|
|
|
{ |
|
|
|
{ |
|
|
|
AddPlural("(" + singular[0] + ")" + singular.Substring(1) + "$", "$1" + plural.Substring(1)); |
|
|
|
var singularSubstring = singular.Substring(1); |
|
|
|
AddSingular("(" + plural[0] + ")" + plural.Substring(1) + "$", "$1" + singular.Substring(1)); |
|
|
|
var pluralSubString = plural.Substring(1); |
|
|
|
|
|
|
|
AddPlural($"({singular[0]}){singularSubstring}$", $"$1{pluralSubString}"); |
|
|
|
|
|
|
|
AddSingular($"({plural[0]}){pluralSubString}$", $"$1{singularSubstring}"); |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
else |
|
|
|
{ |
|
|
|
{ |
|
|
@ -42,54 +49,63 @@ namespace Humanizer.Inflections |
|
|
|
/// Adds an uncountable word to the vocabulary, e.g. "fish". Will be ignored when plurality is changed.
|
|
|
|
/// Adds an uncountable word to the vocabulary, e.g. "fish". Will be ignored when plurality is changed.
|
|
|
|
/// </summary>
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="word">Word to be added to the list of uncountables.</param>
|
|
|
|
/// <param name="word">Word to be added to the list of uncountables.</param>
|
|
|
|
public void AddUncountable(string word) |
|
|
|
public void AddUncountable(string word) => |
|
|
|
{ |
|
|
|
uncountables.Add(word); |
|
|
|
_uncountables.Add(word.ToLower()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// <summary>
|
|
|
|
/// Adds a rule to the vocabulary that does not follow trivial rules for pluralization, e.g. "bus" -> "buses"
|
|
|
|
/// Adds a rule to the vocabulary that does not follow trivial rules for pluralization, e.g. "bus" -> "buses"
|
|
|
|
/// </summary>
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="rule">RegEx to be matched, case insensitive, e.g. "(bus)es$"</param>
|
|
|
|
/// <param name="rule">RegEx to be matched, case insensitive, e.g. "(bus)es$"</param>
|
|
|
|
/// <param name="replacement">RegEx replacement e.g. "$1"</param>
|
|
|
|
/// <param name="replacement">RegEx replacement e.g. "$1"</param>
|
|
|
|
public void AddPlural(string rule, string replacement) |
|
|
|
public void AddPlural(string rule, string replacement) => |
|
|
|
{ |
|
|
|
plurals.Add(new(rule, replacement)); |
|
|
|
_plurals.Add(new Rule(rule, replacement)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// <summary>
|
|
|
|
/// Adds a rule to the vocabulary that does not follow trivial rules for singularization, e.g. "vertices/indices -> "vertex/index"
|
|
|
|
/// Adds a rule to the vocabulary that does not follow trivial rules for singularization, e.g. "vertices/indices -> "vertex/index"
|
|
|
|
/// </summary>
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="rule">RegEx to be matched, case insensitive, e.g. ""(vert|ind)ices$""</param>
|
|
|
|
/// <param name="rule">RegEx to be matched, case insensitive, e.g. ""(vert|ind)ices$""</param>
|
|
|
|
/// <param name="replacement">RegEx replacement e.g. "$1ex"</param>
|
|
|
|
/// <param name="replacement">RegEx replacement e.g. "$1ex"</param>
|
|
|
|
public void AddSingular(string rule, string replacement) |
|
|
|
public void AddSingular(string rule, string replacement) => |
|
|
|
{ |
|
|
|
singulars.Add(new(rule, replacement)); |
|
|
|
_singulars.Add(new Rule(rule, replacement)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// <summary>
|
|
|
|
/// Pluralizes the provided input considering irregular words
|
|
|
|
/// Pluralizes the provided input considering irregular words
|
|
|
|
/// </summary>
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="word">Word to be pluralized</param>
|
|
|
|
/// <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>
|
|
|
|
/// <param name="inputIsKnownToBeSingular">Normally you call Pluralize on singular words; but if you're unsure call it with false</param>
|
|
|
|
/// <returns></returns>
|
|
|
|
[return: NotNullIfNotNull(nameof(word))] |
|
|
|
public string Pluralize(string word, bool inputIsKnownToBeSingular = true) |
|
|
|
public string? Pluralize(string? word, bool inputIsKnownToBeSingular = true) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (word == null) |
|
|
|
{ |
|
|
|
{ |
|
|
|
var result = ApplyRules(_plurals, word, false); |
|
|
|
return null; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var s = LetterS(word); |
|
|
|
|
|
|
|
if (s != null) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return s + "s"; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var result = ApplyRules(plurals, word, false); |
|
|
|
|
|
|
|
|
|
|
|
if (inputIsKnownToBeSingular) |
|
|
|
if (inputIsKnownToBeSingular) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return result ?? word; |
|
|
|
return result ?? word; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var asSingular = ApplyRules(_singulars, word, false); |
|
|
|
var asSingular = ApplyRules(singulars, word, false); |
|
|
|
var asSingularAsPlural = ApplyRules(_plurals, asSingular, false); |
|
|
|
var asSingularAsPlural = ApplyRules(plurals, asSingular, false); |
|
|
|
if (asSingular != null && asSingular != word && asSingular + "s" != word && asSingularAsPlural == word && result != word) |
|
|
|
if (asSingular != null && |
|
|
|
|
|
|
|
asSingular != word && |
|
|
|
|
|
|
|
asSingular + "s" != word && |
|
|
|
|
|
|
|
asSingularAsPlural == word && |
|
|
|
|
|
|
|
result != word) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return word; |
|
|
|
return word; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
return result!; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// <summary>
|
|
|
@ -98,10 +114,20 @@ namespace Humanizer.Inflections |
|
|
|
/// <param name="word">Word to be singularized</param>
|
|
|
|
/// <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>
|
|
|
|
/// <param name="inputIsKnownToBePlural">Normally you call Singularize on plural words; but if you're unsure call it with false</param>
|
|
|
|
/// <param name="skipSimpleWords">Skip singularizing single words that have an 's' on the end</param>
|
|
|
|
/// <param name="skipSimpleWords">Skip singularizing single words that have an 's' on the end</param>
|
|
|
|
/// <returns></returns>
|
|
|
|
[return: NotNullIfNotNull(nameof(word))] |
|
|
|
public string Singularize(string word, bool inputIsKnownToBePlural = true, bool skipSimpleWords = false) |
|
|
|
public string? Singularize(string? word, bool inputIsKnownToBePlural = true, bool skipSimpleWords = false) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (word == null) |
|
|
|
{ |
|
|
|
{ |
|
|
|
var result = ApplyRules(_singulars, word, skipSimpleWords); |
|
|
|
return null; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
var s = LetterS(word); |
|
|
|
|
|
|
|
if (s != null) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return s; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var result = ApplyRules(singulars, word, skipSimpleWords); |
|
|
|
|
|
|
|
|
|
|
|
if (inputIsKnownToBePlural) |
|
|
|
if (inputIsKnownToBePlural) |
|
|
|
{ |
|
|
|
{ |
|
|
@ -109,17 +135,24 @@ namespace Humanizer.Inflections |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// the Plurality is unknown so we should check all possibilities
|
|
|
|
// the Plurality is unknown so we should check all possibilities
|
|
|
|
var asPlural = ApplyRules(_plurals, word, false); |
|
|
|
var asPlural = ApplyRules(plurals, word, false); |
|
|
|
var asPluralAsSingular = ApplyRules(_singulars, asPlural, false); |
|
|
|
if (asPlural == word || |
|
|
|
if (asPlural != word && word + "s" != asPlural && asPluralAsSingular == word && result != word) |
|
|
|
word + "s" == asPlural) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return word; |
|
|
|
return result ?? word; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var asPluralAsSingular = ApplyRules(singulars, asPlural, false); |
|
|
|
|
|
|
|
if (asPluralAsSingular != word || |
|
|
|
|
|
|
|
result == word) |
|
|
|
|
|
|
|
{ |
|
|
|
return result ?? word; |
|
|
|
return result ?? word; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private string ApplyRules(IList<Rule> rules, string word, bool skipFirstRule) |
|
|
|
return word; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
string? ApplyRules(IList<Rule> rules, string? word, bool skipFirstRule) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (word == null) |
|
|
|
if (word == null) |
|
|
|
{ |
|
|
|
{ |
|
|
@ -145,34 +178,43 @@ namespace Humanizer.Inflections |
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return result; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private bool IsUncountable(string word) |
|
|
|
if (result == null) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return _uncountables.Contains(word.ToLower()); |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private class Rule |
|
|
|
return MatchUpperCase(word, result); |
|
|
|
{ |
|
|
|
} |
|
|
|
private readonly Regex _regex; |
|
|
|
|
|
|
|
private readonly string _replacement; |
|
|
|
bool IsUncountable(string word) => |
|
|
|
|
|
|
|
uncountables.Contains(word); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static string MatchUpperCase(string word, string replacement) => |
|
|
|
|
|
|
|
char.IsUpper(word[0]) && |
|
|
|
|
|
|
|
char.IsLower(replacement[0]) ? StringHumanizeExtensions.Concat(char.ToUpper(replacement[0]), replacement.AsSpan(1)) : replacement; |
|
|
|
|
|
|
|
|
|
|
|
public Rule(string pattern, string replacement) |
|
|
|
/// <summary>
|
|
|
|
|
|
|
|
/// If the word is the letter s, singular or plural, return the letter s singular
|
|
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
|
|
string? LetterS(string word) |
|
|
|
{ |
|
|
|
{ |
|
|
|
_regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); |
|
|
|
var s = letterS.Match(word); |
|
|
|
_replacement = replacement; |
|
|
|
return s.Groups.Count > 1 ? s.Groups[1].Value : null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public string Apply(string word) |
|
|
|
class Rule(string pattern, string replacement) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (!_regex.IsMatch(word)) |
|
|
|
readonly Regex regex = new(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public string? Apply(string word) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (!regex.IsMatch(word)) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return null; |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return _regex.Replace(word, _replacement); |
|
|
|
return regex.Replace(word, replacement); |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |