https://github.com/toreaurstadboss/BulkOperationsEntityFramework
NorwegianPluralizationService.cs
using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure.Pluralization;
using System.Diagnostics;
using System.Linq;
namespace BulkOperationsEntityFramework.Lib.Services
{
/// <summary>
/// Sources for the pluralization rules for Norwegian language:
/// https://toppnorsk.com/2018/11/18/flertall-hovedregler/
/// </summary>
public class NorwegianPluralizationService : IPluralizationService
{
public static List<string> PluralizedWords = new List<string>();
public string Pluralize(string word)
{
if (PluralizedWords.Contains(word, StringComparer.OrdinalIgnoreCase))
{
return word; // Return the already pluralized word
}
//#if DEBUG
// Debugger.Break();
// Debugger.Launch(); // Uncomment this line to break into the debugger when this method is called, for example when database migrations are made with EF Code First
//#endif
word = NormalizeWord(word);
string pluralizedWord;
if (_specialCases.ContainsKey(word))
{
pluralizedWord = _specialCases[word];
PluralizedWords.Add(pluralizedWord);
return pluralizedWord;
}
if (_wordsChangingVowelToÆ.Contains(word, StringComparer.OrdinalIgnoreCase))
{
if (word.Equals("Håndkle", StringComparison.OrdinalIgnoreCase))
{
pluralizedWord = "Håndklær";
PluralizedWords.Add(pluralizedWord);
return pluralizedWord;
}
pluralizedWord = word.Replace("å", "æ").Replace("e", "æ") + "r";
PluralizedWords.Add(pluralizedWord);
return pluralizedWord;
}
if (_wordsForUnits.Contains(word, StringComparer.OrdinalIgnoreCase))
{
pluralizedWord = word;
PluralizedWords.Add(pluralizedWord);
return pluralizedWord;
}
if (_wordsForRelatives.Contains(word, StringComparer.OrdinalIgnoreCase))
{
switch (word.ToLower())
{
case "far": pluralizedWord = "Fedre"; break;
case "mor": pluralizedWord = "Mødre"; break;
case "datter": pluralizedWord = "Døtre"; break;
case "søster": pluralizedWord = "Søstre"; break;
case "fetter": pluralizedWord = "Fettere"; break;
case "onkel": pluralizedWord = "Onkler"; break;
case "svigerbror": pluralizedWord = "Svigerbrødre"; break;
case "svigerfar": pluralizedWord = "Svigerfedre"; break;
case "svigersøster": pluralizedWord = "Svigersøstre"; break;
case "svigermor": pluralizedWord = "Svigermødre"; break;
case "bror": pluralizedWord = "Brødre"; break;
default: pluralizedWord = word; break;
}
PluralizedWords.Add(pluralizedWord);
return pluralizedWord;
}
if (_wordsNeutralGenderEndingWithEumOrIum.Contains(word, StringComparer.OrdinalIgnoreCase))
{
if (word.EndsWith("eum"))
pluralizedWord = word.Substring(0, word.Length - 3) + "eer";
else if (word.EndsWith("ium"))
pluralizedWord = word.Substring(0, word.Length - 3) + "ier";
else
pluralizedWord = word;
PluralizedWords.Add(pluralizedWord);
return pluralizedWord;
}
if (_wordsNoPluralizationForNeutralGenderOneSyllable.Contains(word, StringComparer.OrdinalIgnoreCase))
{
pluralizedWord = word;
PluralizedWords.Add(pluralizedWord);
return pluralizedWord;
}
if (_wordChangingVowelsInPluralFemale.Contains(word, StringComparer.OrdinalIgnoreCase))
{
pluralizedWord = NormalizeWord(word.ToLower().Replace("å", "e").Replace("a", "e") + "er");
PluralizedWords.Add(pluralizedWord);
return pluralizedWord;
}
if (_wordsChangingVowelsInPluralMale.Contains(word, StringComparer.OrdinalIgnoreCase))
{
string rewrittenWord = NormalizeWord(word.Replace("o", "ø"));
if (rewrittenWord.Equals("føt", StringComparison.OrdinalIgnoreCase))
pluralizedWord = rewrittenWord + "ter";
else if (rewrittenWord.EndsWith("e"))
pluralizedWord = rewrittenWord + "r";
else if (!rewrittenWord.EndsWith("er"))
pluralizedWord = rewrittenWord + "er";
else
pluralizedWord = rewrittenWord;
PluralizedWords.Add(pluralizedWord);
return pluralizedWord;
}
if (_nonEndingWordsInPlural.Contains(word, StringComparer.OrdinalIgnoreCase))
{
pluralizedWord = word;
PluralizedWords.Add(pluralizedWord);
return pluralizedWord;
}
// General rules
if (word.EndsWith("er"))
pluralizedWord = word.Substring(0, word.Length - 2) + "ere";
else if (word.EndsWith("el"))
pluralizedWord = word.Substring(0, word.Length - 2) + "ler";
else if (word.EndsWith("e"))
pluralizedWord = word + "r";
else if (word.EndsWith("en"))
pluralizedWord = word + "er";
else
pluralizedWord = word + "er";
PluralizedWords.Add(pluralizedWord);
return pluralizedWord;
}
public string Singularize(string word)
{
word = NormalizeWord(word);
// Reverse special cases
var specialSingular = _specialCases.FirstOrDefault(kvp => kvp.Value.Equals(word, StringComparison.OrdinalIgnoreCase));
if (!specialSingular.Equals(default(KeyValuePair<string, string>)))
return specialSingular.Key;
// Words that are the same in singular and plural
if (_nonEndingWordsInPlural.Contains(word, StringComparer.OrdinalIgnoreCase) ||
_wordsNoPluralizationForNeutralGenderOneSyllable.Contains(word, StringComparer.OrdinalIgnoreCase) ||
_wordsForUnits.Contains(word, StringComparer.OrdinalIgnoreCase))
return word;
// Irregulars and vowel changes (expand as needed)
if (word.Equals("Bøker", StringComparison.OrdinalIgnoreCase)) return "Bok";
if (word.Equals("Føtter", StringComparison.OrdinalIgnoreCase)) return "Fot";
if (word.Equals("Brødre", StringComparison.OrdinalIgnoreCase)) return "Bror";
if (word.Equals("Menn", StringComparison.OrdinalIgnoreCase)) return "Mann";
if (word.Equals("Kvinner", StringComparison.OrdinalIgnoreCase)) return "Kvinne";
if (word.Equals("Gutter", StringComparison.OrdinalIgnoreCase)) return "Gutt";
if (word.Equals("Netter", StringComparison.OrdinalIgnoreCase)) return "Natt";
if (word.Equals("Tær", StringComparison.OrdinalIgnoreCase)) return "Tå";
if (word.Equals("Tenner", StringComparison.OrdinalIgnoreCase)) return "Tann";
if (word.Equals("Trær", StringComparison.OrdinalIgnoreCase)) return "Tre";
if (word.Equals("Knær", StringComparison.OrdinalIgnoreCase)) return "Kne";
if (word.Equals("Bønder", StringComparison.OrdinalIgnoreCase)) return "Bonde";
if (word.Equals("Hender", StringComparison.OrdinalIgnoreCase)) return "Hand";
if (word.Equals("Døtre", StringComparison.OrdinalIgnoreCase)) return "Datter";
if (word.Equals("Fedre", StringComparison.OrdinalIgnoreCase)) return "Far";
if (word.Equals("Mødre", StringComparison.OrdinalIgnoreCase)) return "Mor";
if (word.Equals("Søstre", StringComparison.OrdinalIgnoreCase)) return "Søster";
if (word.Equals("Øyne", StringComparison.OrdinalIgnoreCase)) return "Øye";
// "ler" ending (from "el")
if (word.EndsWith("ler"))
{
return word.Substring(0, word.Length - 2);
}
if (word.EndsWith("ter"))
{
return word.Substring(0, word.Length - 1);
}
// "ere" ending (from "er" ending in singular, e.g. "Lærere" -> "Lærer")
if (word.EndsWith("ere"))
return word.Substring(0, word.Length - 1);
// "er" ending (general case, e.g. "Biler" -> "Bil", "Stoler" -> "Stol", "Jenter" -> "Jente")
if (word.EndsWith("er"))
return word.Substring(0, word.Length - 2);
// "r" ending (from "e" ending in singular, e.g. "Jenter" -> "Jente" already handled above)
if (word.EndsWith("r"))
{
var possibleSingular = word.Substring(0, word.Length - 1);
return possibleSingular;
}
// Default: return as is
return word;
}
/// <summary>
/// Make the world normalized, i.e. first letter upper case and rest lower case letters, the word is trimmed.
/// Not considering using invariant culture here, as this is a Norwegian pluralization service.
/// </summary>
/// <remarks>In case an empty word (null or empty) is passed in, just return the word.
/// Edge case: In case just One non-empty letter was passed in, make the word also uppercase.</remarks>
private string NormalizeWord(string word)
{
word = word?.Trim();
if (string.IsNullOrEmpty(word) || word.Trim().Length <= 1) {
return word?.ToUpper();
}
return word.Substring(0, 1).ToUpper() + word.Trim().ToLower().Substring(1);
}
private string[] _nonEndingWordsInPlural = new string[] {
"mus", "sko", "ski", "feil", "ting" }; // Add more non-ending words in plural as needed
private string[] _wordsChangingVowelsInPluralMale = new string[]
{
"bonde", "fot", "bok", "bot", "rot"
};
private Dictionary<string, string> _specialCases = new Dictionary<string, string>
{
{ "Mann", "Menn" } , // 'mann' => 'menn'
{ "Barn", "Barn" }, // 'barn' => 'barn' (no pluralization)
{ "Øye", "Øyne" }, // 'øye' => 'øyne' (plural form of 'eye') //consider adding more special cases here in case all the other pluralization rules do not cover the given word
};
private string[] _wordsChangingVowelToÆ = new string[]
{
"Håndkle", "Kne", "Tre", "Tå"
};
private string[] _wordsForUnits = new string[]
{
"meter", "centimeter", "millimeter", "kilometer", "gram", "kilogram", "tonn", "liter", "desiliter", "centiliter", "dollar", "lire",
"pesetas", "euro", "yen", "franc", "pund", "rupee", "ringgit", "peso", "real", "won", "yuan"
};
private string[] _wordChangingVowelsInPluralFemale = new string[]
{
"and", "hand", "hånd", "natt", "stang", "strand", "tang", "tann"
};
private string[] _wordsForRelatives = new string[]
{
"far", "mor", "datter", "fetter", "onkel", "bror", "svigerbror", "svigerfar", "svigermor", "svigersøster", "søster"
};
private string[] _wordsNoPluralizationForNeutralGenderOneSyllable = new string[]
{
"hus", "fjell", "blad"
};
private string[] _wordsNeutralGenderEndingWithEumOrIum = new string[]
{
"museum", "Jubileum", "kjemikalium"
};
}
}
The following DbConfiguration set up for the DbContext sets up the pluralization service to use for Entity Framework.
ApplicationDbConfiguration.cs
using BulkOperationsEntityFramework.Lib.Services;
using System;
using System.Data.Entity;
using System.Data.Entity.SqlServer;
namespace BulkOperationsEntityFramework
{
public class ApplicationDbConfiguration : DbConfiguration
{
public ApplicationDbConfiguration()
{
SetPluralizationService(new NorwegianPluralizationService()); //Set up the NorwegianPluralizationService as the Pluralizer
//more code etc..
}
}
}
I have also created a Schema attribute to control schema names of tables convention previously in the solution, so the Norwegian pluralizer is also being used there.
SchemaConvention.cs
using BulkOperationsEntityFramework.Attributes;
using BulkOperationsEntityFramework.Lib.Services;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Reflection;
namespace BulkOperationsEntityFramework.Conventions
{
public class SchemaConvention : Convention
{
public SchemaConvention()
{
var pluralizer = new NorwegianPluralizationService();
Types().Configure(c =>
{
var schemaAttr = c.ClrType.GetCustomAttribute<SchemaAttribute>(false);
var tableName = pluralizer.Pluralize(c.ClrType.Name);
if (schemaAttr != null && !string.IsNullOrEmpty(schemaAttr.SchemaName))
{
c.ToTable(tableName, schemaAttr.SchemaName ?? "dbo");
}
else
{
c.ToTable(tableName);
}
});
}
}
}
The DbContext will use the IPluralizationService. Consider first this example DbContext :
ApplicationDbContext
using BulkOperationsEntityFramework.Conventions;
using BulkOperationsEntityFramework.Models;
using BulkOperationsEntityFramework.Test;
using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure.Interception;
using System.Linq;
namespace BulkOperationsEntityFramework
{
[DbConfigurationType(typeof(ApplicationDbConfiguration))]
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbConnection connection) : base(connection, false)
{
}
public ApplicationDbContext() : base("name=App")
{
}
public virtual DbSet Bruker { get; set; }
public DbSet ArkivertBruker { get; set; }
public DbSet ArkivertGjest { get; set; }
public DbSet Sesjon { get; set; }
public DbSet Jubileum { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Add(new SchemaConvention());
//more code etc
}
}
}
The following test cases checks how good the pluralizer works.
ApplicationDbContextTests.cs
using Bogus;
using BulkOperationsEntityFramework.Lib.Services;
using BulkOperationsEntityFramework.Models;
using FluentAssertions;
using Moq;
using Newtonsoft.Json;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure.Interception;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace BulkOperationsEntityFramework.Test
{
[TestFixture]
public class ApplicationDbContextTests
{
[Test]
[TestCaseSource(nameof(NorwegianPluralizationCases))]
public void CanUsePluralizationService(string word, string expected)
{
var norwegianPluralizationService = new NorwegianPluralizationService();
string pluralizedWord = norwegianPluralizationService.Pluralize(word);
pluralizedWord.Should().Be(expected, "Norwegian Pluralization service should return the correct plural form of the word.");
}
[Test, TestCaseSource(nameof(NorwegianSingularizationCases))]
public void NorwegianPluralizationService_CanSingularize(string plural, string expectedSingular)
{
var norwegianPluralizationService = new NorwegianPluralizationService();
var actual = norwegianPluralizationService.Singularize(plural);
Assert.That(actual, Is.EqualTo(expectedSingular), $"Expected singular of '{plural}' to be '{expectedSingular}', but got '{actual}'.");
}
public static IEnumerable<TestCaseData> NorwegianPluralizationCases
{
get
{
yield return new TestCaseData("Bil", "Biler");
yield return new TestCaseData("Bok", "Bøker");
yield return new TestCaseData("Hund", "Hunder");
yield return new TestCaseData("Stol", "Stoler");
yield return new TestCaseData("Jente", "Jenter");
yield return new TestCaseData("Gutt", "Gutter");
yield return new TestCaseData("Lærer", "Lærere");
yield return new TestCaseData("Barn", "Barn");
yield return new TestCaseData("Fjell", "Fjell");
yield return new TestCaseData("Sko", "Sko");
yield return new TestCaseData("Ting", "Ting");
yield return new TestCaseData("Mann", "Menn");
yield return new TestCaseData("Kvinne", "Kvinner");
yield return new TestCaseData("Bror", "Brødre");
yield return new TestCaseData("Far", "Fedre");
yield return new TestCaseData("Mor", "Mødre");
yield return new TestCaseData("Datter", "Døtre");
yield return new TestCaseData("Søster", "Søstre");
yield return new TestCaseData("Øye", "Øyne");
yield return new TestCaseData("Hand", "Hender");
yield return new TestCaseData("Fot", "Føtter");
yield return new TestCaseData("Tå", "Tær");
yield return new TestCaseData("Tann", "Tenner");
yield return new TestCaseData("Natt", "Netter");
yield return new TestCaseData("Tre", "Trær");
yield return new TestCaseData("Kne", "Knær");
yield return new TestCaseData("Bonde", "Bønder");
// _nonEndingWordsInPlural
yield return new TestCaseData("Mus", "Mus");
yield return new TestCaseData("Ski", "Ski");
yield return new TestCaseData("Feil", "Feil");
// _wordsChangingVowelsInPluralMale
yield return new TestCaseData("Bot", "Bøter");
yield return new TestCaseData("Rot", "Røter");
// _wordsChangingVowelToÆ
yield return new TestCaseData("Håndkle", "Håndklær");
yield return new TestCaseData("Kne", "Knær");
// _wordsForUnits (should not pluralize)
yield return new TestCaseData("Meter", "Meter");
yield return new TestCaseData("Gram", "Gram");
yield return new TestCaseData("Dollar", "Dollar");
// _wordChangingVowelsInPluralFemale
yield return new TestCaseData("And", "Ender");
yield return new TestCaseData("Hånd", "Hender");
yield return new TestCaseData("Stang", "Stenger");
yield return new TestCaseData("Strand", "Strender");
yield return new TestCaseData("Tang", "Tenger");
yield return new TestCaseData("Tann", "Tenner");
// _wordsForRelatives (some already covered, but add missing)
yield return new TestCaseData("Fetter", "Fettere");
yield return new TestCaseData("Onkel", "Onkler");
yield return new TestCaseData("Svigerbror", "Svigerbrødre");
yield return new TestCaseData("Svigerfar", "Svigerfedre");
yield return new TestCaseData("Svigermor", "Svigermødre");
yield return new TestCaseData("Svigersøster", "Svigersøstre");
// _wordsNoPluralizationForNeutralGenderOneSyllable
yield return new TestCaseData("Hus", "Hus");
yield return new TestCaseData("Blad", "Blad");
// _wordsNeutralGenderEndingWithEumOrIum
yield return new TestCaseData("Museum", "Museer");
yield return new TestCaseData("Jubileum", "Jubileer");
yield return new TestCaseData("Kjemikalium", "Kjemikalier");
}
}
public static IEnumerable<TestCaseData> NorwegianSingularizationCases
{
get
{
yield return new TestCaseData("Biler", "Bil");
yield return new TestCaseData("Bøker", "Bok");
yield return new TestCaseData("Hunder", "Hund");
yield return new TestCaseData("Stoler", "Stol");
yield return new TestCaseData("Jenter", "Jente");
yield return new TestCaseData("Gutter", "Gutt");
yield return new TestCaseData("Lærere", "Lærer");
yield return new TestCaseData("Barn", "Barn");
yield return new TestCaseData("Fjell", "Fjell");
yield return new TestCaseData("Sko", "Sko");
yield return new TestCaseData("Ting", "Ting");
yield return new TestCaseData("Menn", "Mann");
yield return new TestCaseData("Kvinner", "Kvinne");
yield return new TestCaseData("Brødre", "Bror");
yield return new TestCaseData("Fedre", "Far");
yield return new TestCaseData("Mødre", "Mor");
yield return new TestCaseData("Døtre", "Datter");
yield return new TestCaseData("Søstre", "Søster");
yield return new TestCaseData("Øyne", "Øye");
yield return new TestCaseData("Hender", "Hand");
yield return new TestCaseData("Føtter", "Fot");
yield return new TestCaseData("Tær", "Tå");
yield return new TestCaseData("Tenner", "Tann");
yield return new TestCaseData("Netter", "Natt");
yield return new TestCaseData("Trær", "Tre");
yield return new TestCaseData("Knær", "Kne");
yield return new TestCaseData("Bønder", "Bonde");
}
}
}
}