From fa4d525c190875707f336f2fb00da50e1bc35d10 Mon Sep 17 00:00:00 2001 From: MunroRaymaker Date: Sun, 31 Dec 2023 23:34:55 +0000 Subject: [PATCH] Denmark extensions: Updates CPR number generator with checksum (#496) * Allow for control digit to be 0 and 1, which is valid. Adds new unit tests file Updates CPR number generator with optional checksum and adds unit tests. * File scope DanishExtensionTest.cs; Fix spelling error in Extension * Adds includeDash as parameter for formatting final result. * Use switch statement for better readability; and use actual date range in ArgumentOutOfRangeException message --------- Co-authored-by: Brian Chavez --- .../ExtensionTests/DanishExtentionTest.cs | 152 +++++++++++++++++ Source/Bogus.Tests/PersonTest.cs | 16 -- .../Denmark/ExtensionsForDenmark.cs | 160 +++++++++++++++++- 3 files changed, 308 insertions(+), 20 deletions(-) create mode 100644 Source/Bogus.Tests/ExtensionTests/DanishExtentionTest.cs diff --git a/Source/Bogus.Tests/ExtensionTests/DanishExtentionTest.cs b/Source/Bogus.Tests/ExtensionTests/DanishExtentionTest.cs new file mode 100644 index 00000000..e24eccf7 --- /dev/null +++ b/Source/Bogus.Tests/ExtensionTests/DanishExtentionTest.cs @@ -0,0 +1,152 @@ +using System; +using Bogus.DataSets; +using Bogus.Extensions.Denmark; +using FluentAssertions; +using Xunit; + +namespace Bogus.Tests.ExtensionTests; + +public class DanishExtensionTest : SeededTest +{ + private readonly Faker _faker; + + public DanishExtensionTest() + { + _faker = new Faker(); + } + + [Fact] + public void can_generate_cpr_number_for_denmark() + { + // Act + var obtained = _faker.Person.Cpr(validChecksum: false); + + obtained.Dump(); + + // Assert + obtained.Should().NotBeNullOrWhiteSpace(); + ShouldBeLegalDanishCprNumber(obtained); + ShouldBeCorrectGenderCode(_faker.Person.Gender, obtained); + } + + [Fact] + public void excludes_dash_cpr_number() + { + var result = _faker.Person.Cpr(includeDash: false); + result.Should().NotContain("-"); + } + + [Theory] + [InlineData("080165-0058", Name.Gender.Female)] + [InlineData("080165-0066", Name.Gender.Female)] + [InlineData("080165-0074", Name.Gender.Female)] + [InlineData("080165-0082", Name.Gender.Female)] + [InlineData("080165-0090", Name.Gender.Female)] + [InlineData("250665-3595", Name.Gender.Male)] + [InlineData("250665-3617", Name.Gender.Male)] + [InlineData("250665-3633", Name.Gender.Male)] + [InlineData("250665-3641", Name.Gender.Male)] + [InlineData("250665-3749", Name.Gender.Male)] + public void is_valid_danish_cpr_number(string candidate, Name.Gender gender) + { + ShouldBeCorrectGenderCode(gender, candidate); + ShouldBeLegalDanishCprNumber(candidate); + ShouldHaveCorrectChecksum(candidate); + } + + [Theory] + [InlineData("000000-0000", Name.Gender.Female)] + [InlineData("111111-1111", Name.Gender.Male)] + [InlineData("999999-9999", Name.Gender.Female)] + [InlineData("AAAAAA-AAAA", Name.Gender.Female)] + [InlineData("241212-1234", Name.Gender.Female)] + public void is_invalid_danish_cpr_number(string candidate, Name.Gender gender) + { + Action action = () => + { + ShouldBeCorrectGenderCode(gender, candidate); + ShouldBeLegalDanishCprNumber(candidate); + ShouldHaveCorrectChecksum(candidate); + }; + + action.Should().Throw(); + } + + [Theory] + [InlineData("080165", Name.Gender.Female)] + [InlineData("080166", Name.Gender.Female)] + [InlineData("080167", Name.Gender.Female)] + [InlineData("080168", Name.Gender.Female)] + [InlineData("080169", Name.Gender.Female)] + [InlineData("250665", Name.Gender.Male)] + [InlineData("250607", Name.Gender.Male)] + [InlineData("250608", Name.Gender.Male)] + [InlineData("250609", Name.Gender.Male)] + [InlineData("250610", Name.Gender.Male)] + public void can_generate_valid_danish_cpr_numbers(string birthDate, Name.Gender gender) + {; + int day = int.Parse(birthDate.Substring(0, 2)); + int month = int.Parse(birthDate.Substring(2, 2)); + int year = int.Parse(birthDate.Substring(4, 2)); + + year += year < DateTime.Now.Year % 100 ? 2000 : 1900; + + var bd = new DateTime(year, month, day); + + _faker.Person.DateOfBirth = bd; + _faker.Person.Gender = gender; + + var actual = _faker.Person.Cpr(true); + + ShouldBeCorrectGenderCode(gender, actual); + ShouldBeLegalDanishCprNumber(actual); + ShouldHaveCorrectChecksum(actual); + } + + private void ShouldHaveCorrectChecksum(string candidate) + { + var factors = new[] { 4, 3, 2, 7, 6, 5, 4, 3, 2, 1 }; + var digits = candidate.Replace("-", "").Substring(0, 10).ToCharArray(); + + int cs = 0; + for (int i = 0; i < 10; i++) + { + cs += (digits[i] - '0') * factors[i]; + } + + (cs % 11).Should().Be(0); + } + + private void ShouldBeLegalDanishCprNumber(string candidate) + { + var parts = candidate.Split('-'); + parts[0].Should().HaveLength(6); + parts[1].Should().HaveLength(4); + + // Check if the first 6 digits represent a valid date. + int day = int.Parse(parts[0].Substring(0, 2)); + int month = int.Parse(parts[0].Substring(2, 2)); + int year = int.Parse(parts[0].Substring(4, 2)); + + day.Should().BeInRange(1, 31); + month.Should().BeInRange(1, 12); + year.Should().BeInRange(0, 99); + } + + private void ShouldBeCorrectGenderCode(Name.Gender gender, string candidate) + { + var lastPart = int.Parse(candidate.Split('-')[1]); + + if (gender == Name.Gender.Female) + { + lastPart.Should() + .Match(x => x % 2 == 0); + } + + if (gender == Name.Gender.Male) + { + lastPart.Should() + .Match(x => x % 2 == 1); + } + } +} diff --git a/Source/Bogus.Tests/PersonTest.cs b/Source/Bogus.Tests/PersonTest.cs index c6551319..4b6479b1 100644 --- a/Source/Bogus.Tests/PersonTest.cs +++ b/Source/Bogus.Tests/PersonTest.cs @@ -4,7 +4,6 @@ using Bogus.DataSets; using Bogus.Extensions.Brazil; using Bogus.Extensions.Canada; -using Bogus.Extensions.Denmark; using Bogus.Extensions.Finland; using Bogus.Extensions.UnitedStates; using FluentAssertions; @@ -149,21 +148,6 @@ public void can_generate_numeric_cpf_for_brazil() obtained.Should().Equal(expect); } - [Fact] - public void can_generate_cpr_number_for_denmark() - { - var p = new Person(); - var obtained = p.Cpr(); - - obtained.Dump(); - - var a = obtained.Split('-')[0]; - var b = obtained.Split('-')[1]; - - a.Length.Should().Be(6); - b.Length.Should().Be(4); - } - [Fact] public void can_generate_henkilötunnus_for_finland() { diff --git a/Source/Bogus/Extensions/Denmark/ExtensionsForDenmark.cs b/Source/Bogus/Extensions/Denmark/ExtensionsForDenmark.cs index 2efcddd4..a15bc3af 100644 --- a/Source/Bogus/Extensions/Denmark/ExtensionsForDenmark.cs +++ b/Source/Bogus/Extensions/Denmark/ExtensionsForDenmark.cs @@ -1,4 +1,7 @@ -namespace Bogus.Extensions.Denmark; +using static Bogus.DataSets.Name; +using System; + +namespace Bogus.Extensions.Denmark; /// /// API extensions specific for a geographical location. @@ -8,18 +11,167 @@ public static class ExtensionsForDenmark /// /// Danish Personal Identification number /// - public static string Cpr(this Person p) + /// The holder. + /// + /// Indicates whether the generated CPR number should have a valid checksum or not. + /// + public static string Cpr(this Person p, bool validChecksum = true, bool includeDash = true) { const string Key = nameof(ExtensionsForDenmark) + "CPR"; - if( p.context.ContainsKey(Key) ) + if (p.context.ContainsKey(Key)) { return p.context[Key] as string; } + /* + DDMMYY-XXXX + | | | | + | | | | + | | | | + | | | |----> (X)Individual number + | | |-------> (Y)Year (last two digits) + | |---------> (M)Month + |-----------> (D)Day + + The individual number has to be even for women and odd for men. + + As of 2007 there is no longer a requirement for a checksum with a modulo algorithm. + + https://cpr.dk/cpr-systemet/opbygning-af-cpr-nummeret + + https://da.wikipedia.org/wiki/CPR-nummer + + https://www.cprgenerator.net/metode + */ + var r = p.Random; - var final = $"{p.DateOfBirth:ddMMyy}-{r.Replace("####")}"; + string birthDate = $"{p.DateOfBirth:ddMMyy}"; + string individualNumber; + string checksum; + bool hasValidChecksum; + + if (validChecksum) + { + do + { + individualNumber = GenerateIndividualThreeDigitNumber(r, p.DateOfBirth.Year); + hasValidChecksum = GenerateChecksum(birthDate, p.Gender, individualNumber, out checksum); + } while (!hasValidChecksum); + } + else + { + checksum = string.Empty; + individualNumber = GenerateIndividualFourDigitNumber(r, p.Gender, p.DateOfBirth.Year); + } + + string final; + if( includeDash ) { + final = $"{birthDate}-{individualNumber}{checksum}"; + } + else + { + final = $"{birthDate}{individualNumber}{checksum}"; + } p.context[Key] = final; return final; } + + private static string GenerateIndividualFourDigitNumber(Randomizer r, DataSets.Name.Gender gender, int year) + { + int from; + int to; + + switch( year ) + { + case >= 1858 and <= 1899: + from = 5000; + to = 8999; + break; + case >= 1900 and <= 1936: + from = 0; + to = 3999; + break; + case >= 1937 and <= 1999: + from = 0; + to = 4999; + break; + case >= 2000 and <= 2036: + from = 4000; + to = 9999; + break; + case >= 2037 and <= 2057: + from = 5000; + to = 9999; + break; + default: + throw new ArgumentOutOfRangeException(nameof(year), $"{nameof(year)} must be between 1858 and 2057."); + } + + int individualNumber = gender == DataSets.Name.Gender.Female ? r.Even(from, to) : r.Odd(from, to); + + return individualNumber.ToString("D4"); + } + + private static string GenerateIndividualThreeDigitNumber(Randomizer r, int year) + { + int from; + int to; + + switch( year ) + { + case >= 1858 and <= 1899: + from = 500; + to = 899; + break; + case >= 1900 and <= 1936: + from = 0; + to = 399; + break; + case >= 1937 and <= 1999: + from = 0; + to = 499; + break; + case >= 2000 and <= 2036: + from = 400; + to = 999; + break; + case >= 2037 and <= 2057: + from = 500; + to = 999; + break; + default: + throw new ArgumentOutOfRangeException(nameof(year), $"{nameof(year)} must be between 1858 and 2057."); + } + + int individualNumber = r.Int(from, to); + + return individualNumber.ToString("D3"); + } + + private static bool GenerateChecksum(string birthDate, DataSets.Name.Gender gender, string individualNumber, out string checksum) + { + var factors = new[] { 4, 3, 2, 7, 6, 5, 4, 3, 2 }; + var digits = (birthDate + individualNumber).ToCharArray(); + + int cs = 0; + for (int i = 0; i < 9; i++) + { + cs += (digits[i] - '0') * factors[i]; + } + + cs = 11 - (cs % 11); + + if (cs == 11) + { + cs = 0; + } + + checksum = $"{cs}"; + + if (gender == Gender.Female && cs % 2 != 0) return false; + if (gender == Gender.Male && cs % 2 == 0) return false; + + return cs < 10; + } } \ No newline at end of file