From be06967426575cc2976252ee9e7b3b06a1446956 Mon Sep 17 00:00:00 2001 From: Jesse Klaasse Date: Mon, 22 Sep 2025 15:20:57 +0200 Subject: [PATCH 1/6] Add Dutch extensions for generating Burgerservicenummer (BSN) and corresponding tests --- README.md | 2 + .../ExtensionTests/DutchExtensionTests.cs | 141 ++++++++++++++++++ .../Netherlands/ExtensionsForNetherlands.cs | 80 ++++++++++ 3 files changed, 223 insertions(+) create mode 100644 Source/Bogus.Tests/ExtensionTests/DutchExtensionTests.cs create mode 100644 Source/Bogus/Extensions/Netherlands/ExtensionsForNetherlands.cs diff --git a/README.md b/README.md index 90c7b1cf..ae4e0fe4 100644 --- a/README.md +++ b/README.md @@ -500,6 +500,8 @@ In the examples above, all three alternative styles of using **Bogus** produce t * **`using Bogus.Extensions.Italy;`** * `Bogus.Person.CodiceFiscale()` - Codice Fiscale * `Bogus.DataSets.Finance.CodiceFiscale()` - Codice Fiscale +* **`using Bogus.Extensions.Netherlands;`** + * `Bogus.Person.Bsn()` - Burgerservicenummer (BSN) * **`using Bogus.Extensions.Norway;`** * `Bogus.Person.Fodselsnummer()` - Norwegian national identity number * **`using Bogus.Extensions.Poland;`** diff --git a/Source/Bogus.Tests/ExtensionTests/DutchExtensionTests.cs b/Source/Bogus.Tests/ExtensionTests/DutchExtensionTests.cs new file mode 100644 index 00000000..d7fe7e7e --- /dev/null +++ b/Source/Bogus.Tests/ExtensionTests/DutchExtensionTests.cs @@ -0,0 +1,141 @@ +using Bogus.Extensions.Netherlands; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Bogus.Tests.ExtensionTests; + +public class DutchExtensionTests : SeededTest +{ + private readonly Faker faker; + private readonly ITestOutputHelper console; + + public DutchExtensionTests(ITestOutputHelper console) + { + faker = new Faker(); + this.console = console; + } + + [Fact] + public void should_generate_valid_bsn() + { + // Arrange + var person = faker.Person; + + // Act + var bsn = person.Bsn(); + + // Assert + bsn.Should().NotBeNullOrEmpty(); + bsn.Should().HaveLength(9); + bsn.Should().MatchRegex(@"^\d{9}$"); + + // Validate the BSN using 11-proof + IsValidBsn(bsn).Should().BeTrue($"BSN {bsn} should be valid according to 11-proof"); + + console.WriteLine($"Generated BSN: {bsn}"); + } + + [Fact] + public void should_generate_different_bsns_for_different_people() + { + // Arrange + var person1 = faker.Person; + var person2 = faker.Person; + + // Act + var bsn1 = person1.Bsn(); + var bsn2 = person2.Bsn(); + + // Assert + bsn1.Should().NotBe(bsn2, "Different people should have different BSNs"); + + console.WriteLine($"Person 1 BSN: {bsn1}"); + console.WriteLine($"Person 2 BSN: {bsn2}"); + } + + [Fact] + public void should_generate_multiple_valid_bsns() + { + // Arrange & Act + var bsns = new string[100]; + for (int i = 0; i < 100; i++) + { + var person = faker.Person; + bsns[i] = person.Bsn(); + } + + // Assert + foreach (var bsn in bsns) + { + bsn.Should().HaveLength(9); + bsn.Should().MatchRegex(@"^\d{9}$"); + IsValidBsn(bsn).Should().BeTrue($"BSN {bsn} should be valid"); + } + + console.WriteLine($"Generated {bsns.Length} valid BSNs"); + } + + [Theory] + [InlineData("123456782")] // Known valid BSN + [InlineData("111222333")] // Valid test BSN + public void should_validate_known_valid_bsns(string validBsn) + { + // Act & Assert + IsValidBsn(validBsn).Should().BeTrue($"BSN {validBsn} should be valid"); + } + + [Theory] + [InlineData("123456789")] // Invalid BSN (fails 11-proof) + [InlineData("111111111")] // Invalid BSN (all ones) + public void should_invalidate_known_invalid_bsns(string invalidBsn) + { + // Act & Assert + IsValidBsn(invalidBsn).Should().BeFalse($"BSN {invalidBsn} should be invalid"); + } + + [Fact] + public void should_handle_edge_cases_gracefully() + { + // Test with many different people to ensure robustness + for (int i = 0; i < 1000; i++) + { + var person = faker.Person; + var bsn = person.Bsn(); + + bsn.Should().NotBeNullOrEmpty(); + bsn.Should().HaveLength(9); + IsValidBsn(bsn).Should().BeTrue($"Generated BSN {bsn} should always be valid"); + } + } + + /// + /// Validates a BSN using the 11-proof algorithm + /// + /// The BSN to validate + /// True if the BSN is valid, false otherwise + private static bool IsValidBsn(string bsn) + { + if (string.IsNullOrEmpty(bsn) || bsn.Length != 9) + return false; + + if (!long.TryParse(bsn, out _)) + return false; + + // Calculate the 11-proof + int sum = 0; + for (int i = 0; i < 8; i++) + { + int digit = int.Parse(bsn[i].ToString()); + int weight = 9 - i; // Weights: 9, 8, 7, 6, 5, 4, 3, 2 + sum += digit * weight; + } + + int checkDigit = int.Parse(bsn[8].ToString()); + int calculatedCheckDigit = sum % 11; + + // BSN is valid if calculated check digit equals the actual check digit + // and the check digit is not 10 or 11 (which would be invalid) + return calculatedCheckDigit == checkDigit && calculatedCheckDigit < 10; + } +} diff --git a/Source/Bogus/Extensions/Netherlands/ExtensionsForNetherlands.cs b/Source/Bogus/Extensions/Netherlands/ExtensionsForNetherlands.cs new file mode 100644 index 00000000..6c408315 --- /dev/null +++ b/Source/Bogus/Extensions/Netherlands/ExtensionsForNetherlands.cs @@ -0,0 +1,80 @@ +using Bogus.DataSets; + +namespace Bogus.Extensions.Netherlands; + +/// +/// API extensions specific for a geographical location. +/// +public static class ExtensionsForNetherlands +{ + /// + /// Burgerservicenummer (BSN) + /// + /// + /// See also: https://nl.wikipedia.org/wiki/Burgerservicenummer + /// + public static string Bsn(this Person p) + { + // Generate a random 8-digit base number + var baseNumber = p.Random.Number(10000000, 99999999); + + // Calculate the check digit using the 11-proof + var checkDigit = CalculateBsnCheckDigit(baseNumber); + + // If check digit is valid (not 10 or 11), return the 9-digit BSN + if (checkDigit < 10) + { + return $"{baseNumber}{checkDigit}"; + } + + // If check digit is invalid, try again with a different base number + // This is a simple retry mechanism to ensure we get a valid BSN + return GenerateValidBsn(p); + } + + private static string GenerateValidBsn(Person p) + { + int attempts = 0; + const int maxAttempts = 1000; // Prevent infinite loops + + while (attempts < maxAttempts) + { + var baseNumber = p.Random.Number(10000000, 99999999); + var checkDigit = CalculateBsnCheckDigit(baseNumber); + + if (checkDigit < 10) + { + return $"{baseNumber}{checkDigit}"; + } + + attempts++; + } + + // Fallback: generate a known valid BSN pattern + // Use a base that we know will produce a valid check digit + return "123456782"; // This is a valid BSN for testing purposes + } + + private static int CalculateBsnCheckDigit(int baseNumber) + { + // Convert to string to easily access individual digits + var digits = baseNumber.ToString(); + + // BSN 11-proof calculation + // Multiply each digit by its weight (9, 8, 7, 6, 5, 4, 3, 2) + // and sum them up + int sum = 0; + for (int i = 0; i < 8; i++) + { + int digit = int.Parse(digits[i].ToString()); + int weight = 9 - i; // Weights: 9, 8, 7, 6, 5, 4, 3, 2 + sum += digit * weight; + } + + // The check digit is the remainder when sum is divided by 11 + var checkDigit = sum % 11; + + // If remainder is 10 or 11, the BSN is invalid + return checkDigit; + } +} \ No newline at end of file From 7bab0144552d814d17e2ec5990cb7301ae762b75 Mon Sep 17 00:00:00 2001 From: Jesse Klaasse Date: Mon, 22 Sep 2025 15:57:40 +0200 Subject: [PATCH 2/6] Add Dutch vehicle registration plate generation and tests --- README.md | 3 +- .../ExtensionTests/DutchExtensionTests.cs | 2 +- .../DutchVehicleExtensionTests.cs | 326 ++++++++++++++++++ .../VehicleExtensionsForNetherlands.cs | 264 ++++++++++++++ 4 files changed, 593 insertions(+), 2 deletions(-) create mode 100644 Source/Bogus.Tests/ExtensionTests/DutchVehicleExtensionTests.cs create mode 100644 Source/Bogus/Extensions/Netherlands/VehicleExtensionsForNetherlands.cs diff --git a/README.md b/README.md index ae4e0fe4..fb7654cb 100644 --- a/README.md +++ b/README.md @@ -501,7 +501,8 @@ In the examples above, all three alternative styles of using **Bogus** produce t * `Bogus.Person.CodiceFiscale()` - Codice Fiscale * `Bogus.DataSets.Finance.CodiceFiscale()` - Codice Fiscale * **`using Bogus.Extensions.Netherlands;`** - * `Bogus.Person.Bsn()` - Burgerservicenummer (BSN) + * `Bogus.Person.Bsn()` - Dutch Burgerservicenummer (BSN) + * `Bogus.Vehicle.NlRegistrationPlate()` - Dutch Vehicle Registration Plate (kentekenplaat) * **`using Bogus.Extensions.Norway;`** * `Bogus.Person.Fodselsnummer()` - Norwegian national identity number * **`using Bogus.Extensions.Poland;`** diff --git a/Source/Bogus.Tests/ExtensionTests/DutchExtensionTests.cs b/Source/Bogus.Tests/ExtensionTests/DutchExtensionTests.cs index d7fe7e7e..e5d5a16b 100644 --- a/Source/Bogus.Tests/ExtensionTests/DutchExtensionTests.cs +++ b/Source/Bogus.Tests/ExtensionTests/DutchExtensionTests.cs @@ -12,7 +12,7 @@ public class DutchExtensionTests : SeededTest public DutchExtensionTests(ITestOutputHelper console) { - faker = new Faker(); + faker = new Faker("nl"); this.console = console; } diff --git a/Source/Bogus.Tests/ExtensionTests/DutchVehicleExtensionTests.cs b/Source/Bogus.Tests/ExtensionTests/DutchVehicleExtensionTests.cs new file mode 100644 index 00000000..7dedae59 --- /dev/null +++ b/Source/Bogus.Tests/ExtensionTests/DutchVehicleExtensionTests.cs @@ -0,0 +1,326 @@ +using System; +using Bogus.Extensions.Netherlands; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Bogus.Tests.ExtensionTests; + +public class DutchVehicleExtensionTests : SeededTest +{ + private readonly Faker faker; + private readonly ITestOutputHelper console; + + public DutchVehicleExtensionTests(ITestOutputHelper console) + { + faker = new Faker("nl"); + this.console = console; + } + + [Fact] + public void should_generate_valid_dutch_registration_plate() + { + // Arrange + var vehicle = faker.Vehicle; + var dateFrom = new DateTime(2020, 1, 1); + var dateTo = new DateTime(2024, 12, 31); + + // Act + var kenteken = vehicle.NlRegistrationPlate(dateFrom, dateTo); + + // Assert + kenteken.Should().NotBeNullOrEmpty(); + kenteken.Should().MatchRegex(@"^[A-Z0-9]+-[A-Z0-9]+-[A-Z0-9]+$", "Dutch kenteken should follow the pattern with dashes"); + + console.WriteLine($"Generated kenteken: {kenteken}"); + } + + [Theory] + [InlineData("1951-01-01", "1964-12-31")] // Serie 1: XX-99-99 + [InlineData("1965-01-01", "1972-12-31")] // Serie 2: 99-99-XX + [InlineData("1973-01-01", "1977-12-31")] // Serie 3: 99-XX-99 + [InlineData("1978-07-01", "1990-12-31")] // Serie 4: XX-99-XX + [InlineData("1991-01-01", "1998-12-31")] // Serie 5: XX-XX-99 + [InlineData("1999-01-01", "2004-10-28")] // Serie 6: 99-XX-XX + [InlineData("2004-10-29", "2013-03-04")] // Serie 7: 99-XXX-9 + [InlineData("2013-03-05", "2015-03-29")] // Serie 8: 9-XXX-99 + [InlineData("2015-03-30", "2019-08-18")] // Serie 9: XX-999-X + [InlineData("2019-08-19", "2024-01-07")] // Serie 10: X-999-XX + [InlineData("2024-01-08", "2024-06-03")] // Serie 12: X-99-XXX (starts before Serie 11) + [InlineData("2024-06-04", "2024-12-31")] // Serie 11: XXX-99-X + public void should_generate_correct_format_for_date_ranges(string fromDate, string toDate) + { + // Arrange + var vehicle = faker.Vehicle; + var dateFrom = DateTime.Parse(fromDate); + var dateTo = DateTime.Parse(toDate); + + // Act + var kenteken = vehicle.NlRegistrationPlate(dateFrom, dateTo); + + // Assert + kenteken.Should().NotBeNullOrEmpty(); + ValidateKentekenFormatByDate(kenteken, dateFrom, dateTo); + + console.WriteLine($"Date range: {fromDate} to {toDate}, Generated kenteken: {kenteken}"); + } + + [Fact] + public void should_generate_serie_1_format_for_1950s() + { + // Arrange + var vehicle = faker.Vehicle; + var dateFrom = new DateTime(1951, 1, 1); + var dateTo = new DateTime(1964, 12, 31); + + // Act + var kenteken = vehicle.NlRegistrationPlate(dateFrom, dateTo); + + // Assert + kenteken.Should().MatchRegex(@"^[A-Z]{2}-\d{2}-\d{2}$", "Serie 1 should follow XX-99-99 format"); + + console.WriteLine($"Serie 1 kenteken: {kenteken}"); + } + + [Fact] + public void should_generate_serie_7_format_for_2000s() + { + // Arrange + var vehicle = faker.Vehicle; + var dateFrom = new DateTime(2004, 10, 29); + var dateTo = new DateTime(2013, 3, 4); + + // Act + var kenteken = vehicle.NlRegistrationPlate(dateFrom, dateTo); + + // Assert + kenteken.Should().MatchRegex(@"^\d{2}-[A-Z]{3}-\d{1}$", "Serie 7 should follow 99-XXX-9 format"); + + console.WriteLine($"Serie 7 kenteken: {kenteken}"); + } + + [Fact] + public void should_generate_serie_8_format_for_2010s() + { + // Arrange + var vehicle = faker.Vehicle; + var dateFrom = new DateTime(2013, 3, 5); + var dateTo = new DateTime(2015, 3, 29); + + // Act + var kenteken = vehicle.NlRegistrationPlate(dateFrom, dateTo); + + // Assert + kenteken.Should().MatchRegex(@"^\d{1}-[A-Z]{3}-\d{2}$", "Serie 8 should follow 9-XXX-99 format"); + + // Serie 8 for personenauto's should start with specific letters + var firstLetter = kenteken.Split('-')[1][0]; + new[] { 'K', 'S', 'T', 'X', 'Z' }.Should().Contain(firstLetter, "Serie 8 personenauto should start with K, S, T, X, or Z"); + + console.WriteLine($"Serie 8 kenteken: {kenteken}"); + } + + [Fact] + public void should_generate_current_format_for_recent_dates() + { + // Arrange + var vehicle = faker.Vehicle; + var dateFrom = new DateTime(2024, 6, 4); + var dateTo = new DateTime(2025, 12, 31); + + // Act + var kenteken = vehicle.NlRegistrationPlate(dateFrom, dateTo); + + // Assert + kenteken.Should().MatchRegex(@"^[A-Z]{3}-\d{2}-[A-Z]{1}$", "Current format should follow XXX-99-X format"); + + console.WriteLine($"Current kenteken: {kenteken}"); + } + + [Fact] + public void should_avoid_forbidden_combinations() + { + // Arrange + var vehicle = faker.Vehicle; + var dateFrom = new DateTime(2020, 1, 1); + var dateTo = new DateTime(2024, 12, 31); + + // Act & Assert - Generate multiple kentekens to test forbidden combinations + for (int i = 0; i < 100; i++) + { + var kenteken = vehicle.NlRegistrationPlate(dateFrom, dateTo); + + // Check that forbidden combinations are not present + kenteken.Should().NotContain("SA"); + kenteken.Should().NotContain("SD"); + kenteken.Should().NotContain("SS"); + kenteken.Should().NotContain("GVD"); + kenteken.Should().NotContain("KKK"); + kenteken.Should().NotContain("LPF"); + kenteken.Should().NotContain("NSB"); + kenteken.Should().NotContain("PKK"); + kenteken.Should().NotContain("PSV"); + kenteken.Should().NotContain("PVV"); + kenteken.Should().NotContain("TBS"); + kenteken.Should().NotContain("BBB"); + } + } + + [Fact] + public void should_generate_different_kentekens_for_different_vehicles() + { + // Arrange + var vehicle1 = faker.Vehicle; + var vehicle2 = faker.Vehicle; + var dateFrom = new DateTime(2020, 1, 1); + var dateTo = new DateTime(2024, 12, 31); + + // Act + var kenteken1 = vehicle1.NlRegistrationPlate(dateFrom, dateTo); + var kenteken2 = vehicle2.NlRegistrationPlate(dateFrom, dateTo); + + // Assert + kenteken1.Should().NotBe(kenteken2, "Different vehicles should have different kentekens"); + + console.WriteLine($"Vehicle 1 kenteken: {kenteken1}"); + console.WriteLine($"Vehicle 2 kenteken: {kenteken2}"); + } + + [Fact] + public void should_handle_date_swap_correctly() + { + // Arrange + var vehicle = faker.Vehicle; + var laterDate = new DateTime(2024, 12, 31); + var earlierDate = new DateTime(2020, 1, 1); + + // Act - Pass dates in wrong order + var kenteken = vehicle.NlRegistrationPlate(laterDate, earlierDate); + + // Assert + kenteken.Should().NotBeNullOrEmpty("Method should handle swapped dates gracefully"); + + console.WriteLine($"Kenteken with swapped dates: {kenteken}"); + } + + [Theory] + [InlineData("1950-12-31")] // Before earliest registration + [InlineData("2031-01-01")] // After latest registration + public void should_throw_exception_for_invalid_date_ranges(string invalidDate) + { + // Arrange + var vehicle = faker.Vehicle; + var validDate = new DateTime(2020, 1, 1); + var invalidDateTime = DateTime.Parse(invalidDate); + + // Act & Assert + Assert.Throws(() => vehicle.NlRegistrationPlate(invalidDateTime, validDate)); + Assert.Throws(() => vehicle.NlRegistrationPlate(validDate, invalidDateTime)); + } + + [Fact] + public void should_generate_valid_kentekens_consistently() + { + // Arrange + var vehicle = faker.Vehicle; + var dateFrom = new DateTime(2020, 1, 1); + var dateTo = new DateTime(2024, 12, 31); + + // Act & Assert - Generate multiple kentekens to ensure consistency + for (int i = 0; i < 100; i++) + { + var kenteken = vehicle.NlRegistrationPlate(dateFrom, dateTo); + + kenteken.Should().NotBeNullOrEmpty(); + kenteken.Should().MatchRegex(@"^[A-Z0-9]+-[A-Z0-9]+-[A-Z0-9]+$"); + kenteken.Split('-').Should().HaveCount(3, "Kenteken should have exactly 3 parts separated by dashes"); + } + } + + [Fact] + public void should_only_use_consonants_in_modern_formats() + { + // Arrange + var vehicle = faker.Vehicle; + var dateFrom = new DateTime(2004, 10, 29); // Serie 7 onwards + var dateTo = new DateTime(2024, 12, 31); + + // Act & Assert + for (int i = 0; i < 50; i++) + { + var kenteken = vehicle.NlRegistrationPlate(dateFrom, dateTo); + var parts = kenteken.Split('-'); + + foreach (var part in parts) + { + foreach (var character in part) + { + if (char.IsLetter(character)) + { + // Should not contain vowels (A, E, I, O, U) or forbidden letters + character.Should().NotBe('A'); + character.Should().NotBe('E'); + character.Should().NotBe('I'); + character.Should().NotBe('O'); + character.Should().NotBe('U'); + } + } + } + } + } + + private void ValidateKentekenFormatByDate(string kenteken, DateTime dateFrom, DateTime dateTo) + { + // Use the middle date of the range to determine expected format + var midDate = dateFrom.AddDays((dateTo - dateFrom).TotalDays / 2); + + if (midDate >= new DateTime(2024, 6, 4)) + { + kenteken.Should().MatchRegex(@"^[A-Z]{3}-\d{2}-[A-Z]{1}$", "Serie 11 format"); + } + else if (midDate >= new DateTime(2024, 1, 8)) + { + kenteken.Should().MatchRegex(@"^[A-Z]{1}-\d{2}-[A-Z]{3}$", "Serie 12 format"); + } + else if (midDate >= new DateTime(2019, 8, 19)) + { + kenteken.Should().MatchRegex(@"^[A-Z]{1}-\d{3}-[A-Z]{2}$", "Serie 10 format"); + } + else if (midDate >= new DateTime(2015, 3, 30)) + { + kenteken.Should().MatchRegex(@"^[A-Z]{2}-\d{3}-[A-Z]{1}$", "Serie 9 format"); + } + else if (midDate >= new DateTime(2013, 3, 5)) + { + kenteken.Should().MatchRegex(@"^\d{1}-[A-Z]{3}-\d{2}$", "Serie 8 format"); + } + else if (midDate >= new DateTime(2004, 10, 29)) + { + kenteken.Should().MatchRegex(@"^\d{2}-[A-Z]{3}-\d{1}$", "Serie 7 format"); + } + else if (midDate >= new DateTime(1999, 1, 1)) + { + kenteken.Should().MatchRegex(@"^\d{2}-[A-Z]{2}-[A-Z]{2}$", "Serie 6 format"); + } + else if (midDate >= new DateTime(1991, 1, 1)) + { + kenteken.Should().MatchRegex(@"^[A-Z]{2}-[A-Z]{2}-\d{2}$", "Serie 5 format"); + } + else if (midDate >= new DateTime(1978, 7, 1)) + { + kenteken.Should().MatchRegex(@"^[A-Z]{2}-\d{2}-[A-Z]{2}$", "Serie 4 format"); + } + else if (midDate >= new DateTime(1973, 1, 1)) + { + kenteken.Should().MatchRegex(@"^\d{2}-[A-Z]{2}-\d{2}$", "Serie 3 format"); + } + else if (midDate >= new DateTime(1965, 1, 1)) + { + kenteken.Should().MatchRegex(@"^\d{2}-\d{2}-[A-Z]{2}$", "Serie 2 format"); + } + else + { + kenteken.Should().MatchRegex(@"^[A-Z]{2}-\d{2}-\d{2}$", "Serie 1 format"); + } + } +} diff --git a/Source/Bogus/Extensions/Netherlands/VehicleExtensionsForNetherlands.cs b/Source/Bogus/Extensions/Netherlands/VehicleExtensionsForNetherlands.cs new file mode 100644 index 00000000..415839a7 --- /dev/null +++ b/Source/Bogus/Extensions/Netherlands/VehicleExtensionsForNetherlands.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Bogus.DataSets; + +namespace Bogus.Extensions.Netherlands; + +/// +/// API extensions specific for a geographical location. +/// +public static class VehicleExtensionsForNetherlands +{ + // Dutch kenteken series with their date ranges and formats + private static readonly DateTime Serie1Start = new(1951, 1, 1); + private static readonly DateTime Serie2Start = new(1965, 1, 1); + private static readonly DateTime Serie3Start = new(1973, 1, 1); + private static readonly DateTime Serie4Start = new(1978, 7, 1); + private static readonly DateTime Serie5Start = new(1991, 1, 1); + private static readonly DateTime Serie6Start = new(1999, 1, 1); + private static readonly DateTime Serie7Start = new(2004, 10, 29); + private static readonly DateTime Serie8Start = new(2013, 3, 5); + private static readonly DateTime Serie9Start = new(2015, 3, 30); + private static readonly DateTime Serie10Start = new(2019, 8, 19); + private static readonly DateTime Serie11Start = new(2024, 6, 4); + private static readonly DateTime Serie12Start = new(2024, 1, 8); + + private static readonly DateTime EarliestRegistration = Serie1Start; + private static readonly DateTime LatestRegistration = new(2030, 12, 31); + + // Letters used in Dutch kentekens (excluding forbidden combinations) + private static readonly char[] ConsonantLetters = "BCDFGHJKLMNPRSTVWXZ".ToCharArray(); + private static readonly char[] AllLettersExceptKY = "BCDFGHJLMNPRSTVWXZ".ToCharArray(); // For serie 5 + private static readonly char[] AllLettersWithK = "BCDFGHJKLMNPRSTVWXZ".ToCharArray(); // For serie 6+ + + // Forbidden combinations that should be avoided + private static readonly HashSet ForbiddenCombinations = new() + { + "SA", "SD", "SS", "KL", "GVD", "KKK", "LPF", "NSB", "PKK", "PSV", "PVV", "TBS", "BBB" + }; + + /// + /// Dutch Vehicle Registration Plate (Kenteken) + /// + /// Object to extend. + /// The start of the range of registration dates. + /// The end of the range of registration dates. + /// A string containing a Dutch registration plate. + /// + /// This is based on the information in the Wikipedia article on + /// Dutch license plates (Nederlands kenteken). + /// https://nl.wikipedia.org/wiki/Nederlands_kenteken + /// Supports multiple kenteken series from 1951 to present. + /// + public static string NlRegistrationPlate(this Vehicle vehicle, DateTime dateFrom, DateTime dateTo) + { + DateTime registrationDate = GenerateRegistrationDate(vehicle, dateFrom, dateTo); + return GenerateKenteken(vehicle, registrationDate); + } + + private static string GenerateKenteken(Vehicle vehicle, DateTime registrationDate) + { + // Determine which serie to use based on registration date + // Note: Serie 11 starts later than Serie 12, so check it first + if (registrationDate >= Serie11Start) + return GenerateSerie11(vehicle); // XXX-99-X + else if (registrationDate >= Serie12Start) + return GenerateSerie12(vehicle); // X-99-XXX + else if (registrationDate >= Serie10Start) + return GenerateSerie10(vehicle); // X-999-XX + else if (registrationDate >= Serie9Start) + return GenerateSerie9(vehicle); // XX-999-X + else if (registrationDate >= Serie8Start) + return GenerateSerie8(vehicle); // 9-XXX-99 + else if (registrationDate >= Serie7Start) + return GenerateSerie7(vehicle); // 99-XXX-9 + else if (registrationDate >= Serie6Start) + return GenerateSerie6(vehicle); // 99-XX-XX + else if (registrationDate >= Serie5Start) + return GenerateSerie5(vehicle); // XX-XX-99 + else if (registrationDate >= Serie4Start) + return GenerateSerie4(vehicle); // XX-99-XX + else if (registrationDate >= Serie3Start) + return GenerateSerie3(vehicle); // 99-XX-99 + else if (registrationDate >= Serie2Start) + return GenerateSerie2(vehicle); // 99-99-XX + else + return GenerateSerie1(vehicle); // XX-99-99 + } + + private static string GenerateSerie1(Vehicle vehicle) + { + // XX-99-99 format, started with ND-00-01 + string firstLetter = vehicle.Random.ArrayElement(new[] { "N", "P", "R", "S", "T", "V", "X", "Z" }); + string secondLetter = vehicle.Random.ArrayElement(new[] { "D", "G", "K", "P", "T", "X" }); // Personenauto letters + string numbers1 = vehicle.Random.Int(0, 99).ToString("D2"); + string numbers2 = vehicle.Random.Int(1, 99).ToString("D2"); + return $"{firstLetter}{secondLetter}-{numbers1}-{numbers2}"; + } + + private static string GenerateSerie2(Vehicle vehicle) + { + // 99-99-XX format + string numbers1 = vehicle.Random.Int(10, 99).ToString("D2"); + string numbers2 = vehicle.Random.Int(1, 99).ToString("D2"); + string letters = GenerateRandomLetterPair(vehicle, ConsonantLetters); + return $"{numbers1}-{numbers2}-{letters}"; + } + + private static string GenerateSerie3(Vehicle vehicle) + { + // 99-XX-99 format + string numbers1 = vehicle.Random.Int(10, 99).ToString("D2"); + string letters = GenerateRandomLetterPair(vehicle, ConsonantLetters); + string numbers2 = vehicle.Random.Int(1, 99).ToString("D2"); + return $"{numbers1}-{letters}-{numbers2}"; + } + + private static string GenerateSerie4(Vehicle vehicle) + { + // XX-99-XX format + string letters1 = GenerateRandomLetterPair(vehicle, ConsonantLetters); + string numbers = vehicle.Random.Int(1, 99).ToString("D2"); + string letters2 = GenerateRandomLetterPair(vehicle, ConsonantLetters); + return $"{letters1}-{numbers}-{letters2}"; + } + + private static string GenerateSerie5(Vehicle vehicle) + { + // XX-XX-99 format (no K or Y) + string letters1 = GenerateRandomLetterPair(vehicle, AllLettersExceptKY); + string letters2 = GenerateRandomLetterPair(vehicle, AllLettersExceptKY); + string numbers = vehicle.Random.Int(1, 99).ToString("D2"); + return $"{letters1}-{letters2}-{numbers}"; + } + + private static string GenerateSerie6(Vehicle vehicle) + { + // 99-XX-XX format (K is back except as first letter) + string numbers = vehicle.Random.Int(1, 99).ToString("D2"); + string letters1 = GenerateRandomLetterPair(vehicle, AllLettersWithK); + string letters2 = GenerateRandomLetterPair(vehicle, AllLettersWithK); + return $"{numbers}-{letters1}-{letters2}"; + } + + private static string GenerateSerie7(Vehicle vehicle) + { + // 99-XXX-9 format + string numbers1 = vehicle.Random.Int(0, 99).ToString("D2"); + string letters = GenerateRandomLetterTriple(vehicle, ConsonantLetters); + string numbers2 = vehicle.Random.Int(1, 9).ToString(); + return $"{numbers1}-{letters}-{numbers2}"; + } + + private static string GenerateSerie8(Vehicle vehicle) + { + // 9-XXX-99 format (personenauto's: K, S, T, X, Z) + string[] validFirstLetters = { "K", "S", "T", "X", "Z" }; + string firstLetter = vehicle.Random.ArrayElement(validFirstLetters); + string remainingLetters = GenerateRandomLetterPair(vehicle, ConsonantLetters); + string numbers1 = vehicle.Random.Int(1, 9).ToString(); + string numbers2 = vehicle.Random.Int(0, 99).ToString("D2"); + return $"{numbers1}-{firstLetter}{remainingLetters}-{numbers2}"; + } + + private static string GenerateSerie9(Vehicle vehicle) + { + // XX-999-X format + string letters1 = GenerateRandomLetterPair(vehicle, ConsonantLetters); + string numbers = vehicle.Random.Int(1, 999).ToString("D3"); + string letter2 = vehicle.Random.ArrayElement(ConsonantLetters).ToString(); + return $"{letters1}-{numbers}-{letter2}"; + } + + private static string GenerateSerie10(Vehicle vehicle) + { + // X-999-XX format + string letter1 = vehicle.Random.ArrayElement(ConsonantLetters).ToString(); + string numbers = vehicle.Random.Int(1, 999).ToString("D3"); + string letters2 = GenerateRandomLetterPair(vehicle, ConsonantLetters); + return $"{letter1}-{numbers}-{letters2}"; + } + + private static string GenerateSerie11(Vehicle vehicle) + { + // XXX-99-X format + string letters1 = GenerateRandomLetterTriple(vehicle, ConsonantLetters); + string numbers = vehicle.Random.Int(1, 99).ToString("D2"); + string letter2 = vehicle.Random.ArrayElement(ConsonantLetters).ToString(); + return $"{letters1}-{numbers}-{letter2}"; + } + + private static string GenerateSerie12(Vehicle vehicle) + { + // X-99-XXX format + string letter1 = vehicle.Random.ArrayElement(ConsonantLetters).ToString(); + string numbers = vehicle.Random.Int(1, 99).ToString("D2"); + string letters2 = GenerateRandomLetterTriple(vehicle, ConsonantLetters); + return $"{letter1}-{numbers}-{letters2}"; + } + + private static string GenerateRandomLetterPair(Vehicle vehicle, char[] availableLetters) + { + string result; + int attempts = 0; + do + { + char letter1 = vehicle.Random.ArrayElement(availableLetters); + char letter2 = vehicle.Random.ArrayElement(availableLetters); + result = $"{letter1}{letter2}"; + attempts++; + } while (ForbiddenCombinations.Contains(result) && attempts < 100); + + return result; + } + + private static string GenerateRandomLetterTriple(Vehicle vehicle, char[] availableLetters) + { + string result; + int attempts = 0; + do + { + char letter1 = vehicle.Random.ArrayElement(availableLetters); + char letter2 = vehicle.Random.ArrayElement(availableLetters); + char letter3 = vehicle.Random.ArrayElement(availableLetters); + result = $"{letter1}{letter2}{letter3}"; + attempts++; + } while (ContainsForbiddenSubstring(result) && attempts < 100); + + return result; + } + + private static bool ContainsForbiddenSubstring(string letters) + { + foreach (string forbidden in ForbiddenCombinations) + { + if (letters.Contains(forbidden)) + return true; + } + return false; + } + + private static DateTime GenerateRegistrationDate(Vehicle vehicle, DateTime dateFrom, DateTime dateTo) + { + if (dateFrom < EarliestRegistration || dateFrom > LatestRegistration) + throw new ArgumentOutOfRangeException(nameof(dateFrom), $"Can only accept registration dates between {EarliestRegistration:yyyy-MM-dd} and {LatestRegistration:yyyy-MM-dd}."); + if (dateTo < EarliestRegistration || dateTo > LatestRegistration) + throw new ArgumentOutOfRangeException(nameof(dateTo), $"Can only accept registration dates between {EarliestRegistration:yyyy-MM-dd} and {LatestRegistration:yyyy-MM-dd}."); + + // Swap the values if they're the wrong way around + if (dateFrom > dateTo) + { + DateTime valueHolder = dateFrom; + dateFrom = dateTo; + dateTo = valueHolder; + } + + dateFrom = dateFrom.Date; + dateTo = dateTo.Date; + int duration = (int)(dateTo - dateFrom).TotalDays; + int offset = vehicle.Random.Int(0, duration); + DateTime registrationDate = dateFrom.AddDays(offset); + return registrationDate; + } +} \ No newline at end of file From 1a1b3d578fef5bdd089f8184bac2a6aabd8da710 Mon Sep 17 00:00:00 2001 From: Jesse Klaasse Date: Fri, 24 Oct 2025 13:46:46 +0200 Subject: [PATCH 3/6] Enhance Dutch postcode to prevent illegal codes --- Source/Bogus/data/nl.locale.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/Bogus/data/nl.locale.json b/Source/Bogus/data/nl.locale.json index 4c19f499..97a265fc 100644 --- a/Source/Bogus/data/nl.locale.json +++ b/Source/Bogus/data/nl.locale.json @@ -2857,7 +2857,15 @@ "#{street_name} #{building_number}" ], "postcode": [ - "#### ??" + "1### ??", + "2### ??", + "3### ??", + "4### ??", + "5### ??", + "6### ??", + "7### ??", + "8### ??", + "9### ??" ], "state": [ "Noord-Holland", From 09442b90f908a4ccbfd2f8f2738907170cf97821 Mon Sep 17 00:00:00 2001 From: Jesse Klaasse Date: Fri, 24 Oct 2025 13:49:55 +0200 Subject: [PATCH 4/6] Remove Dutch BSN references (will add this later, after reconsidering how this should work) --- README.md | 1 - .../ExtensionTests/DutchExtensionTests.cs | 141 ------------------ .../Netherlands/ExtensionsForNetherlands.cs | 80 ---------- 3 files changed, 222 deletions(-) delete mode 100644 Source/Bogus.Tests/ExtensionTests/DutchExtensionTests.cs delete mode 100644 Source/Bogus/Extensions/Netherlands/ExtensionsForNetherlands.cs diff --git a/README.md b/README.md index fb7654cb..7c19b3ba 100644 --- a/README.md +++ b/README.md @@ -501,7 +501,6 @@ In the examples above, all three alternative styles of using **Bogus** produce t * `Bogus.Person.CodiceFiscale()` - Codice Fiscale * `Bogus.DataSets.Finance.CodiceFiscale()` - Codice Fiscale * **`using Bogus.Extensions.Netherlands;`** - * `Bogus.Person.Bsn()` - Dutch Burgerservicenummer (BSN) * `Bogus.Vehicle.NlRegistrationPlate()` - Dutch Vehicle Registration Plate (kentekenplaat) * **`using Bogus.Extensions.Norway;`** * `Bogus.Person.Fodselsnummer()` - Norwegian national identity number diff --git a/Source/Bogus.Tests/ExtensionTests/DutchExtensionTests.cs b/Source/Bogus.Tests/ExtensionTests/DutchExtensionTests.cs deleted file mode 100644 index e5d5a16b..00000000 --- a/Source/Bogus.Tests/ExtensionTests/DutchExtensionTests.cs +++ /dev/null @@ -1,141 +0,0 @@ -using Bogus.Extensions.Netherlands; -using FluentAssertions; -using Xunit; -using Xunit.Abstractions; - -namespace Bogus.Tests.ExtensionTests; - -public class DutchExtensionTests : SeededTest -{ - private readonly Faker faker; - private readonly ITestOutputHelper console; - - public DutchExtensionTests(ITestOutputHelper console) - { - faker = new Faker("nl"); - this.console = console; - } - - [Fact] - public void should_generate_valid_bsn() - { - // Arrange - var person = faker.Person; - - // Act - var bsn = person.Bsn(); - - // Assert - bsn.Should().NotBeNullOrEmpty(); - bsn.Should().HaveLength(9); - bsn.Should().MatchRegex(@"^\d{9}$"); - - // Validate the BSN using 11-proof - IsValidBsn(bsn).Should().BeTrue($"BSN {bsn} should be valid according to 11-proof"); - - console.WriteLine($"Generated BSN: {bsn}"); - } - - [Fact] - public void should_generate_different_bsns_for_different_people() - { - // Arrange - var person1 = faker.Person; - var person2 = faker.Person; - - // Act - var bsn1 = person1.Bsn(); - var bsn2 = person2.Bsn(); - - // Assert - bsn1.Should().NotBe(bsn2, "Different people should have different BSNs"); - - console.WriteLine($"Person 1 BSN: {bsn1}"); - console.WriteLine($"Person 2 BSN: {bsn2}"); - } - - [Fact] - public void should_generate_multiple_valid_bsns() - { - // Arrange & Act - var bsns = new string[100]; - for (int i = 0; i < 100; i++) - { - var person = faker.Person; - bsns[i] = person.Bsn(); - } - - // Assert - foreach (var bsn in bsns) - { - bsn.Should().HaveLength(9); - bsn.Should().MatchRegex(@"^\d{9}$"); - IsValidBsn(bsn).Should().BeTrue($"BSN {bsn} should be valid"); - } - - console.WriteLine($"Generated {bsns.Length} valid BSNs"); - } - - [Theory] - [InlineData("123456782")] // Known valid BSN - [InlineData("111222333")] // Valid test BSN - public void should_validate_known_valid_bsns(string validBsn) - { - // Act & Assert - IsValidBsn(validBsn).Should().BeTrue($"BSN {validBsn} should be valid"); - } - - [Theory] - [InlineData("123456789")] // Invalid BSN (fails 11-proof) - [InlineData("111111111")] // Invalid BSN (all ones) - public void should_invalidate_known_invalid_bsns(string invalidBsn) - { - // Act & Assert - IsValidBsn(invalidBsn).Should().BeFalse($"BSN {invalidBsn} should be invalid"); - } - - [Fact] - public void should_handle_edge_cases_gracefully() - { - // Test with many different people to ensure robustness - for (int i = 0; i < 1000; i++) - { - var person = faker.Person; - var bsn = person.Bsn(); - - bsn.Should().NotBeNullOrEmpty(); - bsn.Should().HaveLength(9); - IsValidBsn(bsn).Should().BeTrue($"Generated BSN {bsn} should always be valid"); - } - } - - /// - /// Validates a BSN using the 11-proof algorithm - /// - /// The BSN to validate - /// True if the BSN is valid, false otherwise - private static bool IsValidBsn(string bsn) - { - if (string.IsNullOrEmpty(bsn) || bsn.Length != 9) - return false; - - if (!long.TryParse(bsn, out _)) - return false; - - // Calculate the 11-proof - int sum = 0; - for (int i = 0; i < 8; i++) - { - int digit = int.Parse(bsn[i].ToString()); - int weight = 9 - i; // Weights: 9, 8, 7, 6, 5, 4, 3, 2 - sum += digit * weight; - } - - int checkDigit = int.Parse(bsn[8].ToString()); - int calculatedCheckDigit = sum % 11; - - // BSN is valid if calculated check digit equals the actual check digit - // and the check digit is not 10 or 11 (which would be invalid) - return calculatedCheckDigit == checkDigit && calculatedCheckDigit < 10; - } -} diff --git a/Source/Bogus/Extensions/Netherlands/ExtensionsForNetherlands.cs b/Source/Bogus/Extensions/Netherlands/ExtensionsForNetherlands.cs deleted file mode 100644 index 6c408315..00000000 --- a/Source/Bogus/Extensions/Netherlands/ExtensionsForNetherlands.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Bogus.DataSets; - -namespace Bogus.Extensions.Netherlands; - -/// -/// API extensions specific for a geographical location. -/// -public static class ExtensionsForNetherlands -{ - /// - /// Burgerservicenummer (BSN) - /// - /// - /// See also: https://nl.wikipedia.org/wiki/Burgerservicenummer - /// - public static string Bsn(this Person p) - { - // Generate a random 8-digit base number - var baseNumber = p.Random.Number(10000000, 99999999); - - // Calculate the check digit using the 11-proof - var checkDigit = CalculateBsnCheckDigit(baseNumber); - - // If check digit is valid (not 10 or 11), return the 9-digit BSN - if (checkDigit < 10) - { - return $"{baseNumber}{checkDigit}"; - } - - // If check digit is invalid, try again with a different base number - // This is a simple retry mechanism to ensure we get a valid BSN - return GenerateValidBsn(p); - } - - private static string GenerateValidBsn(Person p) - { - int attempts = 0; - const int maxAttempts = 1000; // Prevent infinite loops - - while (attempts < maxAttempts) - { - var baseNumber = p.Random.Number(10000000, 99999999); - var checkDigit = CalculateBsnCheckDigit(baseNumber); - - if (checkDigit < 10) - { - return $"{baseNumber}{checkDigit}"; - } - - attempts++; - } - - // Fallback: generate a known valid BSN pattern - // Use a base that we know will produce a valid check digit - return "123456782"; // This is a valid BSN for testing purposes - } - - private static int CalculateBsnCheckDigit(int baseNumber) - { - // Convert to string to easily access individual digits - var digits = baseNumber.ToString(); - - // BSN 11-proof calculation - // Multiply each digit by its weight (9, 8, 7, 6, 5, 4, 3, 2) - // and sum them up - int sum = 0; - for (int i = 0; i < 8; i++) - { - int digit = int.Parse(digits[i].ToString()); - int weight = 9 - i; // Weights: 9, 8, 7, 6, 5, 4, 3, 2 - sum += digit * weight; - } - - // The check digit is the remainder when sum is divided by 11 - var checkDigit = sum % 11; - - // If remainder is 10 or 11, the BSN is invalid - return checkDigit; - } -} \ No newline at end of file From 94b6914fc5d1d538b0df6791b033fedcf0e2229c Mon Sep 17 00:00:00 2001 From: Jesse Klaasse Date: Fri, 24 Oct 2025 13:52:45 +0200 Subject: [PATCH 5/6] Update Dutch phone number formats for improved accuracy --- Source/Bogus/data/nl.locale.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/Bogus/data/nl.locale.json b/Source/Bogus/data/nl.locale.json index 97a265fc..1c521546 100644 --- a/Source/Bogus/data/nl.locale.json +++ b/Source/Bogus/data/nl.locale.json @@ -4737,9 +4737,11 @@ }, "phone_number": { "formats": [ - "(####) ######", - "##########", + "0###-######", + "0##-#######", + "0#########", "06########", + "06-########", "06 #### ####" ] } From c19153a665f8cef269481b895b7f8af12ac68963 Mon Sep 17 00:00:00 2001 From: Jesse Klaasse Date: Fri, 24 Oct 2025 15:16:35 +0200 Subject: [PATCH 6/6] Update Dutch postcode and phone number formats for enhanced validation --- Source/Bogus/data/nl.locale.schema.verified.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Bogus/data/nl.locale.schema.verified.txt b/Source/Bogus/data/nl.locale.schema.verified.txt index 8674e5f5..c82da10a 100644 --- a/Source/Bogus/data/nl.locale.schema.verified.txt +++ b/Source/Bogus/data/nl.locale.schema.verified.txt @@ -6,7 +6,7 @@ city_suffix: [Array String; 47], country: [Array String; 254], default_country: [Array String; 1], - postcode: [Array String; 1], + postcode: [Array String; 9], secondary_address: [Array String; 4], state: [Array String; 12], street_address: [Array String; 1], @@ -63,7 +63,7 @@ tussenvoegsel: [Array String; 7] }, phone_number: { - formats: [Array String; 4] + formats: [Array String; 6] }, title: Dutch } \ No newline at end of file