diff --git a/samples/ConsoleSample.CSharp/Program.cs b/samples/ConsoleSample.CSharp/Program.cs index eb9697e..45ea0cc 100644 --- a/samples/ConsoleSample.CSharp/Program.cs +++ b/samples/ConsoleSample.CSharp/Program.cs @@ -7,14 +7,14 @@ namespace ConsoleSample { class Program { - private static readonly string[] RawPersonalIdentityNumberSamples = + private static readonly string[] RawIndividualIdentityNumberSamples = { "990913+9801", "120211+9986", "990807-2391", "180101-2392", - "180101.2392", + "199008672397", "ABC", }; @@ -23,9 +23,9 @@ static void Main(string[] args) Console.WriteLine("Sample showing possible uses of SwedishPersonalIdentityNumber."); WriteSpace(); - foreach (var sample in RawPersonalIdentityNumberSamples) + foreach (var sample in RawIndividualIdentityNumberSamples) { - WritePersonalIdentityNumberInfo(sample); + WriteIndividualIdentityNumberInfo(sample); WriteSpace(); } @@ -44,51 +44,61 @@ static void Main(string[] args) Console.WriteLine("Here is a personal identity number that can be used for testing:"); Console.WriteLine("----------------------"); var randomPin = SwedishPersonalIdentityNumberTestData.GetRandom(); - WritePersonalIdentityNumberInfo(randomPin); + WriteIndividualIdentityNumberInfo(IndividualIdentityNumber.FromSwedishPersonalIdentityNumber(randomPin)); WriteSpace(); Console.WriteLine("What is your (Swedish) Personal Identity Number?"); var userRawPersonalIdentityNumber = Console.ReadLine(); - WritePersonalIdentityNumberInfo(userRawPersonalIdentityNumber); + WriteIndividualIdentityNumberInfo(userRawPersonalIdentityNumber); WriteSpace(); Console.ReadLine(); } - private static void WritePersonalIdentityNumberInfo(string rawPersonalIdentityNumber) + private static void WriteIndividualIdentityNumberInfo(string rawIndividualIdentityNumber) { - WriteHeader($"Input: {rawPersonalIdentityNumber}"); - if (SwedishPersonalIdentityNumber.TryParse(rawPersonalIdentityNumber, out var personalIdentityNumber)) + WriteHeader($"Input: {rawIndividualIdentityNumber}"); + if (IndividualIdentityNumber.TryParse(rawIndividualIdentityNumber, out var identityNumber)) { - WritePersonalIdentityNumberInfo(personalIdentityNumber); + WriteIndividualIdentityNumberInfo(identityNumber); } else { - Console.Error.WriteLine("Unable to parse the input as a SwedishPersonalIdentityNumber."); + Console.Error.WriteLine("Unable to parse the input as a IndividualIdentityNumber."); } } - private static void WritePersonalIdentityNumberInfo(SwedishPersonalIdentityNumber personalIdentityNumber) + private static void WriteIndividualIdentityNumberInfo(IndividualIdentityNumber identityNumber) { - Console.WriteLine("SwedishPersonalIdentityNumber"); - WriteKeyValueInfo(" .ToString()", personalIdentityNumber.ToString()); - WriteKeyValueInfo(" .To10DigitString()", personalIdentityNumber.To10DigitString()); - WriteKeyValueInfo(" .To12DigitString()", personalIdentityNumber.To12DigitString()); + if (identityNumber.IsSwedishPersonalIdentityNumber) + { + Console.WriteLine("SwedishPersonalIdentityNumber"); + } + else if (identityNumber.IsSwedishCoordinationNumber) + { + Console.WriteLine("SwedishCoordinationNumber"); + } + WriteKeyValueInfo(" .ToString()", identityNumber.ToString()); + WriteKeyValueInfo(" .To10DigitString()", identityNumber.To10DigitString()); + WriteKeyValueInfo(" .To12DigitString()", identityNumber.To12DigitString()); - WriteKeyValueInfo(" .Year", personalIdentityNumber.Year.ToString()); - WriteKeyValueInfo(" .Month", personalIdentityNumber.Month.ToString()); - WriteKeyValueInfo(" .Day", personalIdentityNumber.Day.ToString()); - WriteKeyValueInfo(" .BirthNumber", personalIdentityNumber.BirthNumber.ToString()); - WriteKeyValueInfo(" .Checksum", personalIdentityNumber.Checksum.ToString()); + WriteKeyValueInfo(" .Year", identityNumber.Year.ToString()); + WriteKeyValueInfo(" .Month", identityNumber.Month.ToString()); + WriteKeyValueInfo(" .Day", identityNumber.Day.ToString()); + WriteKeyValueInfo(" .BirthNumber", identityNumber.BirthNumber.ToString()); + WriteKeyValueInfo(" .Checksum", identityNumber.Checksum.ToString()); - WriteKeyValueInfo(" .GetDateOfBirthHint()", personalIdentityNumber.GetDateOfBirthHint().ToShortDateString()); - WriteKeyValueInfo(" .GetAgeHint()", personalIdentityNumber.GetAgeHint().ToString()); + WriteKeyValueInfo(" .GetDateOfBirthHint()", identityNumber.GetDateOfBirthHint().ToShortDateString()); + WriteKeyValueInfo(" .GetAgeHint()", identityNumber.GetAgeHint().ToString()); - WriteKeyValueInfo(" .GetGenderHint()", personalIdentityNumber.GetGenderHint().ToString()); + WriteKeyValueInfo(" .GetGenderHint()", identityNumber.GetGenderHint().ToString()); - // IsTestNumber is an extension method from the package ActiveLogin.Identity.Swedish.TestData - WriteKeyValueInfo(" .IsTestNumber()", personalIdentityNumber.IsTestNumber().ToString()); + if (identityNumber.IsSwedishPersonalIdentityNumber) + { + // IsTestNumber is an extension method from the package ActiveLogin.Identity.Swedish.TestData + WriteKeyValueInfo(" .IsTestNumber()", identityNumber.SwedishPersonalIdentityNumber.IsTestNumber().ToString()); + } } private static void WriteHeader(string header) diff --git a/samples/ConsoleSample.FSharp/Program.fs b/samples/ConsoleSample.FSharp/Program.fs index b1f5d20..c2bc6f7 100644 --- a/samples/ConsoleSample.FSharp/Program.fs +++ b/samples/ConsoleSample.FSharp/Program.fs @@ -4,55 +4,65 @@ open System open ActiveLogin.Identity.Swedish.FSharp open ActiveLogin.Identity.Swedish.FSharp.TestData -let sampleStrings = [ "990913+9801"; "120211+9986"; "990807-2391"; "180101-2392"; "180101.2392"; "ABC" ] +let sampleStrings = [ "990913+9801"; "120211+9986"; "990807-2391"; "180101-2392"; "180101.2392" ] +let sampleCoordinationNumbers = [ "199008672397" ] +let sampleInvalidNumbers = [ "ABC" ] -let parseAndPrintPersonalIdentityNumber str = +let parseAndPrintIndividualIdentityNumber str = let printHeader str = str |> printfn "Input: %s\n----------------------" - let print10DigitString pin = - pin - |> SwedishPersonalIdentityNumber.to10DigitString - |> printfn "SwedishPersonalIdentityNumber.to10DigitString: %A" + let print10DigitString num = + num + |> IndividualIdentityNumber.to10DigitString + |> printfn "IdentityNumber.to10DigitString: %A" - let print12DigitString pin = - pin - |> SwedishPersonalIdentityNumber.to12DigitString - |> printfn "SwedishPersonalIdentityNumber.to12DigitString: %s" + let print12DigitString num = + num + |> IndividualIdentityNumber.to12DigitString + |> printfn "IdentityNumber.to12DigitString: %s" - let printDateOfBirthHint pin = - let date = pin |> SwedishPersonalIdentityNumber.Hints.getDateOfBirthHint - date.ToShortDateString() |> printfn "SwedishPersonalIdentityNumber.Hints.getDateOfBirthHint: %s" + let printDateOfBirthHint num = + let date = num |> IndividualIdentityNumber.Hints.getDateOfBirthHint + date.ToShortDateString() |> printfn "IdentityNumber.Hints.getDateOfBirthHint: %s" - let printAgeHint pin = - pin - |> SwedishPersonalIdentityNumber.Hints.getAgeHintOnDate DateTime.UtcNow + let printAgeHint num = + num + |> IndividualIdentityNumber.Hints.getAgeHintOnDate DateTime.UtcNow |> Option.defaultValue 0 - |> printfn "SwedishPersonalIdentityNumber.Hints.getAgeHintOnDate: %i" + |> printfn "IdentityNumber.Hints.getAgeHintOnDate: %i" let printGenderHint pin = - let gender = pin |> SwedishPersonalIdentityNumber.Hints.getGenderHint - gender.ToString() |> printfn "SwedishPersonalIdentityNumber.Hints.getGenderHint: %s" + let gender = pin |> IndividualIdentityNumber.Hints.getGenderHint + gender.ToString() |> printfn "IdentityNumber.Hints.getGenderHint: %s" - let printIsTestNumber pin = + let printIsTestNumber num = // isTestNumber is an extension from the package ActiveLogin.Identity.Swedish.FSharp.TestData - pin - |> SwedishPersonalIdentityNumber.isTestNumber - |> printfn "SwedishPersonalIdentityNumber.isTestNumber: %b" + match num with + | Personal pin -> + pin + |> SwedishPersonalIdentityNumber.isTestNumber + |> printfn "SwedishPersonalIdentityNumber.isTestNumber: %b" + | Coordination _ -> printfn "Testnumber check is not implemented for coordination numbers" printHeader str - match SwedishPersonalIdentityNumber.parse str with - | Ok pin -> - printfn "SwedishPersonalIdentityNumber:" - printfn "%A" pin - print10DigitString pin - print12DigitString pin - printDateOfBirthHint pin - printAgeHint pin - printGenderHint pin - printIsTestNumber pin - | Error e -> printfn "%A: Unable to parse the input as a SwedishPersonalIdentityNumber." e + match IndividualIdentityNumber.parse str with + | Ok num -> + match num with + | Personal pin -> + printfn "SwedishPersonalIdentityNumber:" + printfn "%A" pin + | Coordination cNum -> + printfn "SwedishCoordinationNumber:" + printfn "%A" cNum + print10DigitString num + print12DigitString num + printDateOfBirthHint num + printAgeHint num + printGenderHint num + printIsTestNumber num + | Error e -> printfn "%A: Unable to parse the input as an IndividualIdentityNumber." e printf "\n\n" [] @@ -60,15 +70,15 @@ let main argv = printfn "Sample showing possible uses of SwedishPersonalIdentityNumber." printf "\n\n" - sampleStrings |> List.iter parseAndPrintPersonalIdentityNumber + sampleStrings @ sampleCoordinationNumbers @ sampleInvalidNumbers |> List.iter parseAndPrintIndividualIdentityNumber - printfn "Here is a valid 10 digit string that can be used for testing:\n----------------------" + printfn "Here is a valid 10 digit string personal identity number that can be used for testing:\n----------------------" SwedishPersonalIdentityNumberTestData.getRandom() |> SwedishPersonalIdentityNumber.to10DigitString |> printfn "%A" printf "\n\n" - printfn "Here is a valid 12 digit string that can be used for testing:\n----------------------" + printfn "Here is a valid 12 digit personal identity number string that can be used for testing:\n----------------------" SwedishPersonalIdentityNumberTestData.getRandom() |> SwedishPersonalIdentityNumber.to12DigitString |> printfn "%s" @@ -82,8 +92,8 @@ let main argv = |> printfn "Is it a test number? %b!" printf "\n\n" - printfn "What is your (Swedish) Personal Identity Number?" + printfn "What is your (Swedish) Identity Number?" let userInput = Console.ReadLine() - parseAndPrintPersonalIdentityNumber userInput + parseAndPrintIndividualIdentityNumber userInput Console.ReadLine() |> ignore 0 // return an integer exit code diff --git a/src/ActiveLogin.Identity.Swedish.TestData/ActiveLogin.Identity.Swedish.TestData.fsproj b/src/ActiveLogin.Identity.Swedish.TestData/ActiveLogin.Identity.Swedish.TestData.fsproj index 8b3a946..c527fed 100644 --- a/src/ActiveLogin.Identity.Swedish.TestData/ActiveLogin.Identity.Swedish.TestData.fsproj +++ b/src/ActiveLogin.Identity.Swedish.TestData/ActiveLogin.Identity.Swedish.TestData.fsproj @@ -47,6 +47,7 @@ + diff --git a/src/ActiveLogin.Identity.Swedish.TestData/AllCoordNums.fs b/src/ActiveLogin.Identity.Swedish.TestData/AllCoordNums.fs new file mode 100644 index 0000000..b0f5135 --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish.TestData/AllCoordNums.fs @@ -0,0 +1,1210 @@ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +module internal ActiveLogin.Identity.Swedish.TestData.AllCoordNums + let allCoordNums = + [| + (1914,01,68,239,6) + (1914,03,70,239,0) + (1914,03,74,239,6) + (1914,04,72,239,7) + (1914,04,76,239,3) + (1914,05,72,239,6) + (1914,06,65,238,6) + (1914,07,81,238,5) + (1914,08,64,239,3) + (1914,11,67,238,7) + (1914,12,80,239,7) + (1914,12,85,238,4) + (1915,02,69,238,5) + (1915,03,88,239,9) + (1915,04,65,238,7) + (1915,04,67,238,5) + (1915,06,81,238,5) + (1915,06,82,239,2) + (1915,08,82,239,0) + (1915,10,66,239,6) + (1915,10,79,238,3) + (1915,12,82,239,4) + (1916,01,61,238,3) + (1916,01,79,238,3) + (1916,03,87,238,1) + (1916,05,75,238,3) + (1916,05,86,239,8) + (1916,06,84,239,9) + (1916,07,75,238,1) + (1916,08,61,238,6) + (1916,11,61,238,1) + (1916,11,88,239,8) + (1916,12,61,238,0) + (1916,12,85,238,2) + (1917,01,66,239,5) + (1917,01,68,239,3) + (1917,01,73,238,8) + (1917,01,75,238,6) + (1917,02,85,238,3) + (1917,04,73,238,5) + (1917,04,84,239,0) + (1917,04,87,238,9) + (1917,05,64,239,3) + (1917,06,77,238,9) + (1917,06,82,239,0) + (1917,09,86,239,3) + (1918,01,66,239,4) + (1918,01,81,238,7) + (1918,01,86,239,0) + (1918,05,73,238,3) + (1918,08,73,238,0) + (1918,09,71,238,1) + (1918,10,80,239,5) + (1919,03,77,238,0) + (1919,05,62,239,3) + (1919,06,68,239,6) + (1919,06,72,239,0) + (1919,06,73,238,1) + (1919,07,86,239,3) + (1919,10,66,239,2) + (1919,11,61,238,8) + (1920,02,70,239,3) + (1920,04,79,238,4) + (1920,06,81,238,8) + (1920,07,65,238,7) + (1920,07,73,238,7) + (1920,07,75,238,5) + (1920,08,72,239,5) + (1920,08,76,239,1) + (1920,09,71,238,7) + (1921,01,85,238,8) + (1921,02,68,239,6) + (1921,03,62,239,1) + (1921,03,69,238,6) + (1921,05,82,239,5) + (1921,06,64,239,6) + (1921,08,61,238,9) + (1921,08,78,239,8) + (1921,08,82,239,2) + (1921,08,85,238,1) + (1921,08,86,239,8) + (1921,11,75,238,8) + (1921,12,79,238,3) + (1922,01,64,239,0) + (1922,01,82,239,8) + (1922,01,85,238,7) + (1922,02,75,238,8) + (1922,04,79,238,2) + (1922,07,88,239,6) + (1922,08,64,239,3) + (1922,08,69,238,0) + (1922,09,64,239,2) + (1922,10,64,239,9) + (1922,10,69,238,6) + (1922,10,76,239,5) + (1922,11,70,239,0) + (1922,11,71,238,1) + (1923,02,61,238,3) + (1923,02,62,239,0) + (1923,02,81,238,9) + (1923,05,88,239,7) + (1923,07,64,239,3) + (1923,07,67,238,2) + (1923,07,82,239,1) + (1923,07,83,238,2) + (1923,09,63,238,4) + (1924,01,88,239,0) + (1924,02,63,238,0) + (1924,03,62,239,8) + (1924,04,72,239,5) + (1924,06,74,239,1) + (1924,07,66,239,0) + (1924,09,66,239,8) + (1924,10,69,238,4) + (1924,10,84,239,3) + (1924,12,72,239,5) + (1925,01,72,239,7) + (1925,02,84,239,2) + (1925,05,65,238,4) + (1925,05,73,238,4) + (1925,05,84,239,9) + (1925,06,69,238,9) + (1925,06,83,238,1) + (1925,09,61,238,4) + (1925,09,73,238,0) + (1925,10,87,238,1) + (1926,01,76,239,2) + (1926,01,77,238,3) + (1926,04,68,239,9) + (1926,04,73,238,4) + (1926,06,77,238,8) + (1926,06,79,238,6) + (1926,09,84,239,4) + (1926,12,66,239,1) + (1926,12,69,238,0) + (1927,01,62,239,7) + (1927,04,67,238,1) + (1927,04,83,238,1) + (1927,04,85,238,9) + (1927,05,78,239,5) + (1927,06,82,239,8) + (1927,08,65,238,9) + (1927,08,69,238,5) + (1927,11,61,238,8) + (1927,11,66,239,1) + (1927,11,77,238,0) + (1927,11,88,239,5) + (1927,12,78,239,6) + (1928,01,66,239,2) + (1928,01,83,238,3) + (1928,04,63,238,4) + (1928,05,73,238,1) + (1928,06,81,238,0) + (1928,07,71,238,1) + (1928,07,72,239,8) + (1928,07,85,238,5) + (1928,08,73,238,8) + (1928,08,88,239,9) + (1928,10,75,238,2) + (1928,11,83,238,1) + (1928,12,61,238,6) + (1929,02,64,239,2) + (1929,02,74,239,0) + (1929,03,67,238,0) + (1929,05,62,239,1) + (1929,05,69,238,6) + (1929,06,87,238,3) + (1929,10,61,238,7) + (1929,10,70,239,4) + (1929,11,80,239,1) + (1929,12,75,238,9) + (1930,01,61,238,5) + (1930,01,72,239,0) + (1930,01,73,238,1) + (1930,01,77,238,7) + (1930,01,81,238,1) + (1930,01,84,239,6) + (1930,06,71,238,8) + (1930,06,83,238,4) + (1930,06,84,239,1) + (1930,06,85,238,2) + (1930,07,71,238,7) + (1930,07,79,238,9) + (1930,10,63,238,2) + (1930,10,64,239,9) + (1930,11,74,239,6) + (1931,01,80,239,9) + (1931,02,77,238,5) + (1931,03,88,239,9) + (1931,07,63,238,6) + (1931,07,85,238,0) + (1931,09,79,238,6) + (1932,01,72,239,8) + (1932,02,72,239,7) + (1932,02,76,239,3) + (1932,02,88,239,9) + (1932,03,63,238,9) + (1932,05,68,239,0) + (1932,05,76,239,0) + (1932,05,82,239,2) + (1932,06,70,239,5) + (1932,06,82,239,1) + (1932,07,61,238,7) + (1932,07,85,238,9) + (1932,08,83,238,0) + (1932,09,82,239,8) + (1932,09,86,239,4) + (1932,10,78,239,1) + (1932,10,83,238,6) + (1932,11,77,238,3) + (1933,01,83,238,6) + (1933,02,81,238,7) + (1933,06,65,238,3) + (1933,06,81,238,3) + (1933,06,87,238,7) + (1933,08,71,238,3) + (1933,09,66,239,7) + (1933,09,87,238,4) + (1933,10,82,239,4) + (1933,11,80,239,5) + (1934,02,61,238,0) + (1934,02,62,239,7) + (1934,03,63,238,7) + (1934,03,85,238,1) + (1934,04,81,238,4) + (1934,05,69,238,9) + (1934,07,67,238,9) + (1934,08,73,238,0) + (1934,08,82,239,7) + (1934,09,72,239,8) + (1934,10,84,239,1) + (1934,11,82,239,2) + (1935,01,75,238,4) + (1935,01,85,238,2) + (1935,03,67,238,2) + (1935,03,77,238,0) + (1935,05,74,239,9) + (1935,05,86,239,5) + (1935,08,73,238,9) + (1935,09,65,238,8) + (1935,10,66,239,2) + (1935,10,80,239,4) + (1935,10,86,239,8) + (1935,11,68,239,9) + (1935,12,63,238,5) + (1936,01,75,238,3) + (1936,02,63,238,6) + (1936,04,64,239,1) + (1936,04,80,239,1) + (1936,04,86,239,5) + (1936,05,73,238,1) + (1936,06,69,238,6) + (1936,07,67,238,7) + (1936,09,67,238,5) + (1936,12,87,238,6) + (1937,01,67,238,2) + (1937,03,68,239,7) + (1937,03,85,238,8) + (1937,05,66,239,7) + (1937,06,80,239,8) + (1937,07,75,238,6) + (1937,09,75,238,4) + (1937,11,63,238,4) + (1937,11,66,239,9) + (1937,11,67,238,0) + (1937,12,71,238,3) + (1938,01,83,238,1) + (1938,01,84,239,8) + (1938,02,71,238,4) + (1938,02,77,238,8) + (1938,03,66,239,8) + (1938,03,67,238,9) + (1938,03,79,238,5) + (1938,05,79,238,3) + (1938,06,65,238,8) + (1938,07,67,238,5) + (1938,08,62,239,7) + (1938,08,77,238,2) + (1938,09,76,239,0) + (1938,10,78,239,5) + (1938,10,81,238,2) + (1938,10,88,239,3) + (1938,11,68,239,6) + (1939,01,65,238,2) + (1939,02,72,239,0) + (1939,03,83,238,8) + (1939,07,65,238,6) + (1939,07,79,238,0) + (1939,07,80,239,5) + (1939,08,63,238,7) + (1939,08,82,239,2) + (1939,09,61,238,8) + (1939,09,77,238,0) + (1939,09,79,238,8) + (1939,12,87,238,3) + (1940,01,61,238,3) + (1940,02,61,238,2) + (1940,03,61,238,1) + (1940,03,65,238,7) + (1940,04,79,238,0) + (1940,04,87,238,0) + (1940,07,76,239,8) + (1940,09,78,239,4) + (1940,09,80,239,0) + (1940,10,85,238,4) + (1940,11,63,238,9) + (1940,11,77,238,3) + (1940,12,76,239,1) + (1940,12,79,238,0) + (1940,12,80,239,5) + (1941,01,86,239,1) + (1941,02,63,238,9) + (1941,03,69,238,2) + (1941,03,72,239,5) + (1941,03,75,238,4) + (1941,05,62,239,5) + (1941,05,74,239,1) + (1941,06,77,238,9) + (1941,07,79,238,6) + (1941,09,69,238,6) + (1941,11,69,238,2) + (1941,11,73,238,6) + (1941,11,76,239,1) + (1941,12,75,238,3) + (1942,05,78,239,6) + (1942,06,83,238,0) + (1942,07,63,238,3) + (1942,07,80,239,0) + (1942,09,88,239,0) + (1942,10,84,239,1) + (1942,11,70,239,6) + (1943,03,65,238,4) + (1943,04,77,238,9) + (1943,05,84,239,7) + (1943,07,74,239,7) + (1943,07,78,239,3) + (1943,07,79,238,4) + (1943,07,86,239,3) + (1943,09,80,239,7) + (1943,10,78,239,8) + (1943,12,75,238,1) + (1944,01,63,238,7) + (1944,01,71,238,7) + (1944,02,87,238,8) + (1944,04,84,239,7) + (1944,05,68,239,6) + (1944,05,70,239,2) + (1944,08,77,238,4) + (1944,09,66,239,4) + (1944,10,71,238,6) + (1944,10,79,238,8) + (1944,11,73,238,3) + (1944,12,61,238,6) + (1945,02,70,239,4) + (1945,04,76,239,6) + (1945,05,70,239,1) + (1945,05,72,239,9) + (1945,05,81,238,0) + (1945,05,87,238,4) + (1945,06,62,239,0) + (1945,07,68,239,3) + (1945,07,75,238,6) + (1945,07,81,238,8) + (1945,09,67,238,4) + (1945,10,77,238,9) + (1945,12,62,239,2) + (1946,01,77,238,9) + (1946,01,86,239,6) + (1946,02,66,239,9) + (1946,02,75,238,0) + (1946,03,65,238,1) + (1946,04,67,238,8) + (1946,05,75,238,7) + (1946,05,78,239,2) + (1946,06,80,239,7) + (1946,07,62,239,8) + (1946,08,73,238,6) + (1946,08,78,239,9) + (1946,09,70,239,6) + (1946,10,68,239,7) + (1946,10,69,238,8) + (1946,12,78,239,3) + (1947,03,82,239,7) + (1947,03,84,239,5) + (1947,06,77,238,3) + (1947,07,73,238,6) + (1947,08,77,238,1) + (1947,09,79,238,8) + (1947,10,63,238,3) + (1947,10,81,238,1) + (1948,01,70,239,2) + (1948,01,71,238,3) + (1948,03,66,239,6) + (1948,03,72,239,8) + (1948,04,64,239,7) + (1948,05,63,238,9) + (1948,06,65,238,6) + (1948,06,70,239,7) + (1948,08,66,239,1) + (1948,08,84,239,9) + (1948,09,76,239,8) + (1948,09,77,238,9) + (1948,10,64,239,9) + (1948,11,69,238,5) + (1949,01,68,239,5) + (1949,01,86,239,3) + (1949,03,62,239,9) + (1949,04,87,238,1) + (1949,05,73,238,6) + (1949,08,76,239,8) + (1949,09,81,238,2) + (1949,10,67,238,7) + (1950,02,61,238,9) + (1950,04,77,238,9) + (1950,04,84,239,8) + (1950,05,67,238,0) + (1950,07,85,238,6) + (1950,10,63,238,7) + (1950,11,61,238,8) + (1950,12,66,239,0) + (1950,12,74,239,0) + (1950,12,81,238,3) + (1950,12,86,239,6) + (1951,01,88,239,6) + (1951,02,69,238,0) + (1951,04,67,238,0) + (1951,07,72,239,8) + (1951,08,65,238,8) + (1951,09,83,238,5) + (1951,10,66,239,1) + (1951,12,72,239,1) + (1951,12,81,238,2) + (1952,01,63,238,6) + (1952,01,84,239,9) + (1952,02,74,239,0) + (1952,02,88,239,4) + (1952,05,71,238,2) + (1952,05,80,239,9) + (1952,06,66,239,6) + (1952,07,76,239,3) + (1952,07,85,238,4) + (1952,10,77,238,9) + (1952,12,80,239,0) + (1952,12,84,239,6) + (1953,01,73,238,3) + (1953,02,72,239,1) + (1953,03,77,238,7) + (1953,04,87,238,4) + (1953,09,66,239,2) + (1953,10,65,238,2) + (1953,11,83,238,9) + (1953,12,78,239,3) + (1954,01,85,238,8) + (1954,02,73,238,1) + (1954,03,64,239,9) + (1954,03,66,239,7) + (1954,03,86,239,3) + (1954,06,62,239,8) + (1954,06,84,239,2) + (1954,08,64,239,4) + (1954,08,65,238,5) + (1954,08,70,239,6) + (1954,08,75,238,3) + (1954,10,78,239,4) + (1954,11,74,239,7) + (1954,11,86,239,3) + (1954,12,67,238,7) + (1955,01,86,239,4) + (1955,03,76,239,4) + (1955,04,72,239,7) + (1955,06,84,239,1) + (1955,08,63,238,6) + (1955,08,71,238,6) + (1955,09,80,239,2) + (1955,09,83,238,1) + (1955,11,77,238,5) + (1955,11,80,239,8) + (1955,12,88,239,9) + (1956,01,72,239,9) + (1956,01,80,239,9) + (1956,01,81,238,0) + (1956,03,72,239,7) + (1956,04,69,238,3) + (1956,07,61,238,8) + (1956,08,72,239,2) + (1956,09,83,238,0) + (1956,10,61,238,3) + (1956,10,83,238,7) + (1956,12,80,239,6) + (1957,04,88,239,7) + (1957,06,84,239,9) + (1957,07,75,238,1) + (1957,10,73,238,8) + (1957,11,77,238,3) + (1957,12,83,238,4) + (1958,01,61,238,2) + (1958,01,83,238,6) + (1958,03,77,238,2) + (1958,03,87,238,0) + (1958,07,62,239,3) + (1958,08,84,239,6) + (1958,10,70,239,8) + (1958,10,72,239,6) + (1958,10,83,238,5) + (1958,11,64,239,5) + (1958,11,83,238,4) + (1958,12,75,238,3) + (1959,03,70,239,6) + (1959,04,73,238,4) + (1959,05,79,238,7) + (1959,06,86,239,5) + (1959,07,87,238,5) + (1959,08,64,239,9) + (1959,08,74,239,7) + (1959,09,64,239,8) + (1959,09,85,238,5) + (1959,12,79,238,8) + (1959,12,82,239,1) + (1960,03,61,238,6) + (1960,03,75,238,0) + (1960,05,81,238,0) + (1960,06,73,238,9) + (1960,06,85,238,5) + (1960,07,80,239,7) + (1960,07,87,238,2) + (1960,08,69,238,3) + (1960,09,88,239,7) + (1960,12,67,238,9) + (1961,01,77,238,9) + (1961,01,85,238,9) + (1961,02,73,238,2) + (1961,03,63,238,3) + (1961,04,64,239,9) + (1961,04,67,238,8) + (1961,09,68,239,0) + (1961,10,70,239,3) + (1962,02,70,239,2) + (1962,05,62,239,9) + (1962,05,86,239,1) + (1962,06,78,239,0) + (1962,06,85,238,3) + (1962,07,77,238,2) + (1962,08,83,238,3) + (1962,10,65,238,1) + (1962,10,66,239,8) + (1963,01,84,239,6) + (1963,03,67,238,7) + (1963,05,78,239,0) + (1963,06,74,239,3) + (1963,06,79,238,0) + (1963,06,86,239,9) + (1963,07,66,239,2) + (1963,08,74,239,1) + (1963,08,87,238,8) + (1963,12,84,239,3) + (1964,01,78,239,3) + (1964,03,72,239,7) + (1964,04,69,238,3) + (1964,04,78,239,0) + (1964,05,66,239,3) + (1964,07,84,239,9) + (1964,08,81,238,3) + (1964,09,66,239,9) + (1964,09,86,239,5) + (1964,10,69,238,5) + (1964,10,77,238,5) + (1965,03,68,239,2) + (1965,04,81,238,6) + (1965,06,69,238,0) + (1965,07,71,238,5) + (1965,08,65,238,2) + (1965,08,66,239,9) + (1965,10,76,239,3) + (1965,12,82,239,3) + (1966,02,65,238,7) + (1966,02,73,238,7) + (1966,02,75,238,5) + (1966,02,88,239,8) + (1966,03,66,239,3) + (1966,03,69,238,2) + (1966,03,78,239,9) + (1966,03,88,239,7) + (1966,04,62,239,6) + (1966,06,70,239,4) + (1966,06,75,238,1) + (1966,07,72,239,1) + (1966,08,80,239,0) + (1966,09,62,239,1) + (1966,10,65,238,7) + (1966,11,68,239,1) + (1967,01,68,239,2) + (1967,01,88,239,8) + (1967,02,84,239,1) + (1967,03,85,238,1) + (1967,04,63,238,6) + (1967,04,76,239,9) + (1967,06,79,238,6) + (1967,08,65,238,0) + (1967,08,70,239,1) + (1967,09,69,238,5) + (1967,10,72,239,5) + (1967,10,74,239,3) + (1967,10,85,238,2) + (1967,12,76,239,9) + (1968,01,64,239,5) + (1968,01,71,238,8) + (1968,02,87,238,9) + (1968,02,88,239,6) + (1968,04,68,239,8) + (1968,06,85,238,7) + (1968,07,68,239,5) + (1968,07,81,238,0) + (1968,08,71,238,1) + (1968,08,76,239,4) + (1968,08,78,239,2) + (1968,09,67,238,6) + (1968,12,76,239,8) + (1969,01,65,238,5) + (1969,02,75,238,2) + (1969,03,73,238,3) + (1969,03,82,239,0) + (1969,05,68,239,6) + (1969,05,82,239,8) + (1969,07,69,238,5) + (1969,07,81,238,9) + (1969,08,75,238,6) + (1969,08,82,239,5) + (1969,12,74,239,9) + (1969,12,87,238,6) + (1970,01,78,239,5) + (1970,02,82,239,8) + (1970,03,81,238,0) + (1970,04,64,239,8) + (1970,04,71,238,1) + (1970,05,63,238,0) + (1970,05,83,238,6) + (1970,06,62,239,8) + (1970,06,87,238,1) + (1970,07,69,238,2) + (1970,09,69,238,0) + (1970,10,72,239,0) + (1970,10,88,239,2) + (1970,11,67,238,8) + (1970,11,78,239,3) + (1970,11,82,239,7) + (1970,11,85,238,6) + (1970,12,73,238,9) + (1971,02,77,238,6) + (1971,02,82,239,7) + (1971,02,83,238,8) + (1971,03,62,239,0) + (1971,04,82,239,5) + (1971,05,84,239,2) + (1971,06,61,238,0) + (1971,06,68,239,1) + (1971,06,84,239,1) + (1971,07,65,238,5) + (1971,11,75,238,7) + (1971,12,84,239,3) + (1972,01,80,239,9) + (1972,03,70,239,9) + (1972,04,73,238,7) + (1972,07,83,238,2) + (1972,08,63,238,5) + (1972,08,79,238,7) + (1972,09,65,238,2) + (1972,09,81,238,2) + (1973,01,73,238,9) + (1973,03,80,239,6) + (1973,04,82,239,3) + (1973,05,62,239,6) + (1973,05,66,239,2) + (1973,05,75,238,3) + (1973,06,63,238,6) + (1973,06,79,238,8) + (1973,07,75,238,1) + (1973,08,83,238,0) + (1973,10,65,238,8) + (1973,10,78,239,1) + (1973,11,81,238,7) + (1973,12,83,238,4) + (1974,02,65,238,7) + (1974,02,79,238,1) + (1974,03,87,238,0) + (1974,07,85,238,8) + (1974,07,88,239,3) + (1974,09,82,239,7) + (1974,09,87,238,4) + (1974,12,76,239,0) + (1974,12,77,238,1) + (1975,04,77,238,0) + (1975,04,81,238,4) + (1975,04,85,238,0) + (1975,05,69,238,9) + (1975,05,72,239,2) + (1975,06,85,238,8) + (1975,08,82,239,7) + (1975,09,61,238,3) + (1975,11,81,238,5) + (1975,11,85,238,1) + (1975,12,62,239,5) + (1975,12,75,238,2) + (1975,12,80,239,3) + (1976,02,62,239,6) + (1976,02,81,238,5) + (1976,03,72,239,3) + (1976,05,66,239,9) + (1976,05,68,239,7) + (1976,05,83,238,0) + (1976,06,74,239,8) + (1976,06,78,239,4) + (1976,08,63,238,1) + (1976,08,74,239,6) + (1976,09,75,238,6) + (1976,10,75,238,3) + (1976,11,67,238,2) + (1976,12,64,239,2) + (1976,12,78,239,6) + (1977,01,86,239,8) + (1977,01,88,239,6) + (1977,02,84,239,9) + (1977,03,78,239,6) + (1977,04,82,239,9) + (1977,05,80,239,0) + (1977,12,71,238,4) + (1978,01,79,238,8) + (1978,01,86,239,7) + (1978,02,66,239,0) + (1978,04,62,239,2) + (1978,04,86,239,4) + (1978,05,74,239,7) + (1978,06,73,238,9) + (1978,07,70,239,9) + (1978,07,75,238,6) + (1978,07,84,239,3) + (1978,08,81,238,7) + (1978,09,62,239,7) + (1978,10,77,238,9) + (1978,11,86,239,5) + (1979,04,84,239,5) + (1979,06,81,238,8) + (1979,06,82,239,5) + (1979,07,72,239,6) + (1979,08,62,239,7) + (1979,08,87,238,0) + (1979,10,79,238,6) + (1979,11,88,239,2) + (1979,12,86,239,3) + (1980,01,66,239,7) + (1980,01,73,238,0) + (1980,01,76,239,5) + (1980,01,81,238,0) + (1980,01,83,238,8) + (1980,03,66,239,5) + (1980,04,84,239,2) + (1980,11,88,239,9) + (1980,12,74,239,4) + (1980,12,81,238,7) + (1981,02,70,239,9) + (1981,02,83,238,6) + (1981,03,68,239,2) + (1981,05,66,239,2) + (1981,05,73,238,5) + (1981,05,82,239,2) + (1981,07,81,238,3) + (1981,11,85,238,3) + (1981,11,87,238,1) + (1981,12,75,238,4) + (1981,12,80,239,5) + (1982,01,63,238,0) + (1982,04,75,238,3) + (1982,05,77,238,0) + (1982,06,69,238,9) + (1982,06,72,239,2) + (1982,06,74,239,0) + (1982,06,76,239,8) + (1982,06,82,239,0) + (1982,07,84,239,7) + (1982,08,66,239,8) + (1982,09,62,239,1) + (1982,09,88,239,1) + (1982,11,63,238,8) + (1982,11,82,239,3) + (1983,01,82,239,4) + (1983,03,85,238,1) + (1983,04,62,239,5) + (1983,04,69,238,0) + (1983,05,64,239,2) + (1983,06,66,239,9) + (1983,07,65,238,1) + (1983,08,74,239,7) + (1983,09,66,239,6) + (1983,09,70,239,0) + (1983,10,81,238,6) + (1984,02,61,238,9) + (1984,02,64,239,4) + (1984,02,76,239,0) + (1984,03,62,239,5) + (1984,03,72,239,3) + (1984,03,87,238,8) + (1984,04,71,238,5) + (1984,04,82,239,0) + (1984,07,65,238,0) + (1984,07,76,239,5) + (1984,08,78,239,2) + (1984,09,79,238,2) + (1984,11,73,238,4) + (1984,12,86,239,6) + (1985,02,86,239,7) + (1985,04,78,239,5) + (1985,06,84,239,5) + (1985,08,68,239,3) + (1985,08,71,238,0) + (1985,08,72,239,7) + (1985,09,63,238,9) + (1985,09,66,239,4) + (1985,09,70,239,8) + (1985,09,86,239,0) + (1985,09,87,238,1) + (1985,12,62,239,3) + (1986,06,72,239,8) + (1986,09,82,239,3) + (1986,09,83,238,4) + (1986,10,70,239,4) + (1986,11,85,238,8) + (1987,02,73,238,2) + (1987,04,68,239,5) + (1987,06,87,238,2) + (1987,07,72,239,6) + (1987,07,81,238,7) + (1987,11,67,238,9) + (1987,12,64,239,9) + (1988,01,62,239,3) + (1988,03,62,239,1) + (1988,03,74,239,7) + (1988,03,88,239,1) + (1988,07,74,239,3) + (1988,07,76,239,1) + (1988,08,87,238,9) + (1988,09,64,239,3) + (1988,09,75,238,2) + (1988,10,67,238,9) + (1988,10,73,238,1) + (1988,11,81,238,0) + (1989,01,67,238,9) + (1989,02,63,238,2) + (1989,02,83,238,8) + (1989,04,73,238,8) + (1989,04,75,238,6) + (1989,05,67,238,5) + (1989,05,78,239,0) + (1989,07,62,239,6) + (1989,08,64,239,3) + (1989,09,83,238,1) + (1989,09,88,239,4) + (1989,10,88,239,1) + (1990,01,79,238,2) + (1990,02,62,239,8) + (1990,04,77,238,1) + (1990,05,79,238,8) + (1990,07,78,239,5) + (1990,10,85,238,3) + (1990,12,65,238,5) + (1991,01,72,239,6) + (1991,02,82,239,3) + (1991,03,69,238,1) + (1991,03,75,238,3) + (1991,05,73,238,3) + (1991,06,76,239,7) + (1991,08,65,238,0) + (1991,08,80,239,9) + (1991,09,83,238,7) + (1991,11,67,238,3) + (1991,11,87,238,9) + (1992,01,78,239,9) + (1992,02,62,239,6) + (1992,03,76,239,9) + (1992,04,73,238,3) + (1992,07,64,239,9) + (1992,07,75,238,8) + (1992,07,87,238,4) + (1992,08,73,238,9) + (1992,08,85,238,5) + (1992,09,73,238,8) + (1992,12,72,239,2) + (1993,01,82,239,2) + (1993,02,69,238,0) + (1993,02,86,239,7) + (1993,03,80,239,2) + (1993,04,77,238,8) + (1993,04,87,238,6) + (1993,05,79,238,5) + (1993,09,80,239,6) + (1993,11,66,239,0) + (1993,11,69,238,9) + (1993,12,67,238,0) + (1993,12,79,238,6) + (1994,01,62,239,5) + (1994,01,83,238,2) + (1994,03,70,239,3) + (1994,04,70,239,2) + (1994,05,78,239,3) + (1994,05,80,239,9) + (1994,05,81,238,0) + (1994,05,83,238,8) + (1994,06,72,239,8) + (1994,07,64,239,7) + (1994,07,71,238,0) + (1994,08,75,238,5) + (1994,09,70,239,7) + (1994,09,71,238,8) + (1994,09,81,238,6) + (1994,11,70,239,3) + (1995,02,86,239,5) + (1995,03,87,238,5) + (1995,04,82,239,7) + (1995,05,68,239,4) + (1995,05,69,238,5) + (1995,07,69,238,3) + (1995,08,65,238,6) + (1995,09,61,238,9) + (1995,10,69,238,8) + (1995,11,72,239,0) + (1995,11,87,238,5) + (1995,12,72,239,9) + (1996,02,75,238,9) + (1996,02,83,238,9) + (1996,02,87,238,5) + (1996,03,78,239,3) + (1996,03,88,239,1) + (1996,05,71,238,0) + (1996,06,69,238,3) + (1996,07,61,238,0) + (1996,09,62,239,5) + (1996,12,86,239,2) + (1997,01,82,239,8) + (1997,02,84,239,5) + (1997,05,63,238,9) + (1997,06,74,239,3) + (1997,06,81,238,6) + (1997,06,83,238,4) + (1997,07,62,239,6) + (1997,09,68,239,8) + (1997,09,72,239,2) + (1997,10,78,239,3) + (1997,11,69,238,5) + (1997,12,77,238,4) + (1998,01,86,239,3) + (1998,01,88,239,1) + (1998,04,70,239,8) + (1998,04,82,239,4) + (1998,05,81,238,6) + (1998,06,86,239,8) + (1998,07,67,238,2) + (1998,07,69,238,0) + (1998,10,75,238,7) + (1998,10,83,238,7) + (1999,03,67,238,5) + (1999,06,85,238,0) + (1999,07,71,238,5) + (1999,07,83,238,1) + (1999,08,77,238,8) + (1999,09,61,238,5) + (1999,09,87,238,5) + (1999,11,71,238,9) + (2000,02,87,238,0) + (2000,03,77,238,1) + (2000,04,69,238,0) + (2000,05,68,239,8) + (2000,05,77,238,9) + (2000,06,66,239,9) + (2000,07,67,238,9) + (2000,08,72,239,9) + (2000,09,69,238,5) + (2000,09,72,239,8) + (2000,09,82,239,6) + (2000,10,64,239,5) + (2000,12,70,239,5) + (2001,01,80,239,5) + (2001,03,69,238,0) + (2001,04,78,239,6) + (2001,05,75,238,0) + (2001,06,86,239,4) + (2001,07,83,238,8) + (2001,07,87,238,4) + (2001,10,84,239,0) + (2001,11,79,238,8) + (2001,12,61,238,7) + (2001,12,71,238,5) + (2001,12,84,239,8) + (2002,01,81,238,5) + (2002,02,67,238,2) + (2002,03,72,239,2) + (2002,04,87,238,6) + (2002,06,76,239,5) + (2002,07,76,239,4) + (2002,09,80,239,6) + (2002,09,83,238,5) + (2002,10,85,238,0) + (2003,01,70,239,5) + (2003,01,74,239,1) + (2003,01,80,239,3) + (2003,02,76,239,8) + (2003,04,64,239,0) + (2003,04,75,238,9) + (2003,05,87,238,4) + (2003,07,66,239,5) + (2003,07,72,239,7) + (2003,07,75,238,6) + (2003,08,83,238,5) + (2003,10,84,239,8) + (2003,11,69,238,8) + (2003,12,83,238,9) + (2004,01,63,238,5) + (2004,02,62,239,3) + (2004,02,63,238,4) + (2004,03,80,239,0) + (2004,04,72,239,9) + (2004,04,75,238,8) + (2004,05,71,238,1) + (2004,05,77,238,5) + (2004,07,86,239,0) + (2004,08,63,238,8) + (2004,10,71,238,4) + (2004,12,80,239,9) + (2005,01,70,239,3) + (2005,01,85,238,8) + (2005,02,70,239,2) + (2005,04,79,238,3) + (2005,05,62,239,9) + (2005,05,85,238,4) + (2005,07,80,239,5) + (2005,10,70,239,2) + (2005,11,63,238,2) + (2005,11,74,239,7) + (2005,11,76,239,5) + (2006,03,79,238,3) + (2006,04,82,239,5) + (2006,06,68,239,1) + (2006,06,82,239,3) + (2006,06,83,238,4) + (2006,06,87,238,0) + (2006,08,75,238,2) + (2006,11,80,239,8) + (2006,12,67,238,6) + (2007,02,79,238,3) + (2007,02,86,239,2) + (2007,03,84,239,3) + (2007,04,69,238,3) + (2007,05,61,238,0) + (2007,05,77,238,2) + (2007,06,73,238,5) + (2007,07,69,238,0) + (2007,07,83,238,2) + (2007,07,86,239,7) + (2007,08,70,239,4) + (2007,10,61,238,3) + (2008,04,75,238,4) + (2008,04,82,239,3) + (2008,05,73,238,5) + (2008,05,80,239,4) + (2008,07,81,238,3) + (2008,08,61,238,6) + (2008,08,85,238,8) + (2008,09,84,239,6) + (2008,10,62,239,9) + (2008,11,87,238,1) + (2008,12,70,239,7) + (2008,12,87,238,0) + (2009,01,77,238,4) + (2009,01,82,239,5) + (2009,02,70,239,8) + (2009,03,70,239,7) + (2009,03,72,239,5) + (2009,04,68,239,0) + (2009,05,79,238,8) + (2009,07,69,238,8) + (2009,07,81,238,2) + (2009,08,75,238,9) + (2009,09,86,239,3) + (2009,10,74,239,4) + (2009,10,87,238,1) + (2009,11,63,238,8) + (2009,11,64,239,5) + (2010,02,68,239,9) + (2010,03,85,238,9) + (2010,05,69,238,7) + (2010,05,78,239,4) + (2010,09,70,239,8) + (2010,10,84,239,9) + (2010,11,68,239,8) + (2010,11,81,238,3) + (2010,11,83,238,1) + (2011,01,63,238,6) + (2011,03,62,239,3) + (2011,04,82,239,8) + (2011,05,70,239,1) + (2011,07,65,238,8) + (2011,08,86,239,0) + (2011,09,81,238,6) + (2011,10,67,238,1) + (2011,10,72,239,2) + (2011,10,73,238,3) + (2011,10,82,239,0) + (2011,10,84,239,8) + (2011,10,85,238,9) + (2011,12,74,239,8) + (2011,12,77,238,7) + (2011,12,84,239,6) + (2011,12,86,239,4) + (2012,05,83,238,7) + (2012,06,61,238,2) + (2012,06,77,238,4) + (2012,11,70,239,2) + (2012,12,68,239,5) + (2012,12,86,239,3) + (2012,12,88,239,1) + (2013,01,69,238,8) + (2013,01,75,238,0) + (2013,02,79,238,5) + (2013,02,85,238,7) + (2013,03,79,238,4) + (2013,04,72,239,8) + (2013,05,83,238,6) + (2013,06,67,238,5) + (2013,06,78,239,0) + (2013,07,62,239,7) + (2013,09,71,238,6) + (2013,10,75,238,9) + (2013,11,84,239,5) + (2013,12,78,239,2) + (2013,12,88,239,0) + (2014,01,61,238,5) + (2014,01,69,238,7) + (2014,01,75,238,9) + (2014,01,83,238,9) + (2014,02,65,238,0) + (2014,02,73,238,0) + (2014,03,65,238,9) + (2014,04,65,238,8) + (2014,06,77,238,2) + (2014,07,66,239,2) + (2014,07,74,239,2) + (2014,08,74,239,1) + (2014,09,63,238,5) + (2014,09,87,238,7) + (2014,10,86,239,3) + (2014,12,70,239,9) + (2014,12,88,239,9) + (2015,03,65,238,8) + (2015,03,67,238,6) + (2015,03,86,239,1) + (2015,05,75,238,4) + (2015,08,72,239,2) + (2015,10,79,238,3) + (2015,12,71,238,9) + (2016,01,82,239,6) + (2016,02,66,239,5) + (2016,02,70,239,9) + (2016,03,84,239,2) + (2016,04,73,238,6) + (2016,04,78,239,9) + (2016,05,84,239,0) + (2016,07,61,238,7) + (2016,08,66,239,9) + (2016,09,83,238,9) + (2016,10,67,238,6) + (2016,11,81,238,7) + (2016,12,82,239,3) + (2017,01,86,239,1) + (2017,03,78,239,9) + (2017,04,72,239,4) + (2017,05,75,238,2) + (2017,05,76,239,9) + (2017,07,82,239,9) + (2017,07,83,238,0) + (2017,07,86,239,5) + (2017,08,73,238,1) + (2017,08,84,239,6) + (2017,12,85,238,1) + (2018,01,71,238,9) + (2018,01,83,238,5) + (2018,03,70,239,6) + (2018,03,76,239,0) + (2018,03,77,238,1) + (2018,06,63,238,4) + (2018,07,70,239,2) + (2018,08,63,238,2) + (2018,10,82,239,3) + (2018,11,66,239,2) + (2018,11,86,239,8) + (2018,12,72,239,3) + (2019,01,86,239,9) + (2019,02,79,238,9) + (2019,02,86,239,8) + (2019,03,68,239,9) + (2019,04,71,238,5) + (2019,05,72,239,1) + (2019,07,69,238,6) + (2019,07,71,238,2) + |] \ No newline at end of file diff --git a/src/ActiveLogin.Identity.Swedish.TestData/CoordinationTestDataTemplate.csv b/src/ActiveLogin.Identity.Swedish.TestData/CoordinationTestDataTemplate.csv new file mode 100644 index 0000000..bf7e66e --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish.TestData/CoordinationTestDataTemplate.csv @@ -0,0 +1,3 @@ +TestSamordningsnummer +199008672397 +199008672397 diff --git a/src/ActiveLogin.Identity.Swedish.TestData/FetchCommon.fs b/src/ActiveLogin.Identity.Swedish.TestData/FetchCommon.fs new file mode 100644 index 0000000..06d95b1 --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish.TestData/FetchCommon.fs @@ -0,0 +1,15 @@ +module FetchCommon + + +let private parseAsTuple (s:string) = + (s.[ 0..3 ], s.[ 4..5 ], s.[ 6..7 ], s.[ 8..10 ], s.[ 11.. 11 ]) + +let private tupleAsString (year, month, day, birthNumber, checksum) = + sprintf " (%s,%s,%s,%s,%s)" year month day birthNumber checksum + +let toStringTuple = parseAsTuple >> tupleAsString + + +let appendLine (sb:System.Text.StringBuilder) line = sb.AppendLine line |> ignore + + diff --git a/src/ActiveLogin.Identity.Swedish.TestData/FetchCoordinationTestData.fsx b/src/ActiveLogin.Identity.Swedish.TestData/FetchCoordinationTestData.fsx new file mode 100644 index 0000000..35de7f5 --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish.TestData/FetchCoordinationTestData.fsx @@ -0,0 +1,70 @@ +// To run this script, make sure fake is installed, or install it: +// "dotnet tool install fake-cli -g" or see here for more options: https://fake.build/fake-gettingstarted.html +// to run the script: +// "fake run FetchCoordinationTestData.fsx" +// It will fetch the dependencies list below using paket and run the script. +// If you are updating/adding any dependencies in the list below, remove the ".fake"-folder and the +// FetchCoordinationTestData.fsx.lock-file and run the script again to download the new dependencies. +#r "paket: +nuget FSharp.Core 4.5.4 +nuget FSharp.Data //" +#load "./.fake/FetchTestData.fsx/intellisense.fsx" +#load "./FetchCommon.fs" + +open FSharp.Data +open System.Text +open System.IO +open FetchCommon + +type TestData = CsvProvider<"CoordinationTestDataTemplate.csv"> + +let getCoordNums url = + async { + let! nums = TestData.AsyncLoad url + return + nums.Rows + |> Seq.map (fun r -> r.TestSamordningsnummer) + } + +let header = """// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +module internal ActiveLogin.Identity.Swedish.TestData.AllCoordNums + let allCoordNums = + [| +""" + +let footer = """ |]""" + +let sb = StringBuilder() + +let nums = + let validCoordinationNum numStr = + // see issue (https://github.com/ActiveLogin/ActiveLogin.Identity/issues/100) + let validMonth (numStr:string) = + numStr.[4..5] <> "00" + let validCoordinationDay (numStr:string) = + let day = numStr.[6..7] |> int + day > 60 && day < 89 // yes, the day *can* be 89 or greater, but see the issue linked above. + validMonth numStr && validCoordinationDay numStr + + [ "https://skatteverket.entryscape.net/store/9/resource/154" ] + |> List.map getCoordNums + |> Async.Parallel + |> Async.RunSynchronously + |> Seq.concat + |> Seq.sort + |> Seq.map string + |> Seq.filter validCoordinationNum + + +sb.Clear() +sb.Append(header) + +nums +|> Seq.iter (toStringTuple >> appendLine sb) +sb.Append(footer) +File.WriteAllText("AllCoordNums.fs", sb.ToString()) diff --git a/src/ActiveLogin.Identity.Swedish.TestData/FetchTestData.fsx.lock b/src/ActiveLogin.Identity.Swedish.TestData/FetchCoordinationTestData.fsx.lock similarity index 61% rename from src/ActiveLogin.Identity.Swedish.TestData/FetchTestData.fsx.lock rename to src/ActiveLogin.Identity.Swedish.TestData/FetchCoordinationTestData.fsx.lock index 593ec65..ec7c108 100644 --- a/src/ActiveLogin.Identity.Swedish.TestData/FetchTestData.fsx.lock +++ b/src/ActiveLogin.Identity.Swedish.TestData/FetchCoordinationTestData.fsx.lock @@ -2,8 +2,6 @@ STORAGE: NONE RESTRICTION: == netstandard2.0 NUGET remote: https://api.nuget.org/v3/index.json - ActiveLogin.Identity.Swedish (2.0.1) - FSharp.Core (>= 4.5.4) FSharp.Core (4.5.4) - FSharp.Data (3.0) + FSharp.Data (3.3.2) FSharp.Core (>= 4.3.4) diff --git a/src/ActiveLogin.Identity.Swedish.TestData/FetchTestData.fsx b/src/ActiveLogin.Identity.Swedish.TestData/FetchPinTestData.fsx similarity index 66% rename from src/ActiveLogin.Identity.Swedish.TestData/FetchTestData.fsx rename to src/ActiveLogin.Identity.Swedish.TestData/FetchPinTestData.fsx index 5e08ad5..c5b6f54 100644 --- a/src/ActiveLogin.Identity.Swedish.TestData/FetchTestData.fsx +++ b/src/ActiveLogin.Identity.Swedish.TestData/FetchPinTestData.fsx @@ -1,23 +1,25 @@ // To run this script, make sure fake is installed, or install it: // "dotnet tool install fake-cli -g" or see here for more options: https://fake.build/fake-gettingstarted.html // to run the script: -// "fake run FetchTestData.fsx" +// "fake run FetchPinTestData.fsx" // It will fetch the dependencies list below using paket and run the script. -// If you are updating/adding any dependencies in the list below, remove the ".fake"-folder and the FetchTestData.fsx.lock-file and run the script again to download the new dependencies. +// If you are updating/adding any dependencies in the list below, remove the ".fake"-folder and the +// FetchPinTestData.fsx.lock-file and run the script again to download the new dependencies. #r "paket: nuget FSharp.Core 4.5.4 -nuget FSharp.Data -nuget ActiveLogin.Identity.Swedish //" +nuget FSharp.Data //" #load "./.fake/FetchTestData.fsx/intellisense.fsx" +#load "./FetchCommon.fs" open FSharp.Data open System.Text open System.IO +open FetchCommon -type Pins = CsvProvider<"TestDataTemplate.csv"> +type TestData = CsvProvider<"PinTestDataTemplate.csv"> let getPins url = - async { let! pins = Pins.AsyncLoad url + async { let! pins = TestData.AsyncLoad url return pins.Rows |> Seq.map (fun r -> r.Testpersonnummer) } let header = """// @@ -34,8 +36,8 @@ module internal ActiveLogin.Identity.Swedish.TestData.AllPins let footer = """ |]""" let sb = StringBuilder() -let pins = - [ "https://skatteverket.entryscape.net/store/9/resource/149" +let pins = + [ "https://skatteverket.entryscape.net/store/9/resource/149" "https://skatteverket.entryscape.net/store/9/resource/535" "https://skatteverket.entryscape.net/store/9/resource/686" ] |> List.map getPins @@ -45,17 +47,9 @@ let pins = |> Seq.sort |> Seq.map string -let parseAsTuple (s:string) = - (s.[ 0..3 ], s.[ 4..5 ], s.[ 6..7 ], s.[ 8..10 ], s.[ 11.. 11 ]) - -let tupleAsString (year, month, day, birthNumber, checksum) = - sprintf " (%s,%s,%s,%s,%s)" year month day birthNumber checksum - -let appendLine (sb:StringBuilder) line = sb.AppendLine line |> ignore - sb.Clear() sb.Append(header) pins -|> Seq.iter (parseAsTuple >> tupleAsString >> appendLine sb) +|> Seq.iter (toStringTuple >> appendLine sb) sb.Append(footer) File.WriteAllText("AllPins.fs", sb.ToString()) diff --git a/src/ActiveLogin.Identity.Swedish.TestData/FetchPinTestData.fsx.lock b/src/ActiveLogin.Identity.Swedish.TestData/FetchPinTestData.fsx.lock new file mode 100644 index 0000000..ec7c108 --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish.TestData/FetchPinTestData.fsx.lock @@ -0,0 +1,7 @@ +STORAGE: NONE +RESTRICTION: == netstandard2.0 +NUGET + remote: https://api.nuget.org/v3/index.json + FSharp.Core (4.5.4) + FSharp.Data (3.3.2) + FSharp.Core (>= 4.3.4) diff --git a/src/ActiveLogin.Identity.Swedish.TestData/TestdataTemplate.csv b/src/ActiveLogin.Identity.Swedish.TestData/PinTestdataTemplate.csv similarity index 100% rename from src/ActiveLogin.Identity.Swedish.TestData/TestdataTemplate.csv rename to src/ActiveLogin.Identity.Swedish.TestData/PinTestdataTemplate.csv diff --git a/src/ActiveLogin.Identity.Swedish.TestData/TestData.fs b/src/ActiveLogin.Identity.Swedish.TestData/TestData.fs index 98b82cf..f969eef 100644 --- a/src/ActiveLogin.Identity.Swedish.TestData/TestData.fs +++ b/src/ActiveLogin.Identity.Swedish.TestData/TestData.fs @@ -1,72 +1,74 @@ -namespace ActiveLogin.Identity.Swedish.FSharp.TestData +module ActiveLogin.Identity.Swedish.FSharp.TestData open ActiveLogin.Identity.Swedish.FSharp -open ActiveLogin.Identity.Swedish.TestData.AllPins +open ActiveLogin.Identity.Swedish.TestData open System open System.Threading +let private rng = + // this thread-safe implementation is required to handle running lots of invocations of getRandom in parallel + let seedGenerator = Random() + let localGenerator = new ThreadLocal(fun _ -> + lock seedGenerator (fun _ -> + let seed = seedGenerator.Next() + Random(seed))) + fun (min, max) -> localGenerator.Value.Next(min, max) -/// A module that provides easy access to the official test numbers for Swedish Personal Identity Number (Personnummer) +let private random _ = rng(Int32.MinValue, Int32.MaxValue) + +/// A module that provides easy access to the official test numbers for Swedish Personal Identity Number (Personnummer) /// from Skatteverket module SwedishPersonalIdentityNumberTestData = - let private rng = - // this thread-safe implementation is required to handle running lots of invocations of getRandom in parallel - let seedGenerator = Random() - let localGenerator = new ThreadLocal(fun _ -> - lock seedGenerator (fun _ -> - let seed = seedGenerator.Next() - Random())) - fun (min, max) -> localGenerator.Value.Next(min, max) - let private random _ = rng(Int32.MinValue, Int32.MaxValue) + open ActiveLogin.Identity.Swedish.TestData.AllPins + let internal shuffledPins() = allPins |> Array.sortBy random /// All the testdata from Skatteverket presented as an array of 12 digit strings. - let raw12DigitStrings = + let raw12DigitStrings = allPins |> Array.map (fun (year, month, day, birthNumber, checksum) -> sprintf "%04i%02i%02i%03i%i" year month day birthNumber checksum) - let internal create (year, month, day, birthNumber, checksum) = - let values = - { Year = year - Month = month - Day = day - BirthNumber = birthNumber - Checksum = checksum } + let internal create values = match SwedishPersonalIdentityNumber.create values with | Ok p -> p - | Error _ -> failwith "broken test data" + | Error err -> failwithf "broken test data %A for %A" err values - /// A seqence of all test numbers ordered by date descending + /// A sequence of all test numbers ordered by date descending let allPinsByDateDesc() = seq { for pin in allPins do yield create pin } + /// A sequence of all test numbers in random order let allPinsShuffled() = seq { for pin in shuffledPins() do yield create pin } + /// A random test number - let getRandom() = + let getRandom() = let index = rng(0, Array.length allPins - 1) allPins.[index] |> create + /// - /// Returns a sequence of length specified by count, of unique random test numbers. If it is not important that the + /// Returns a sequence of length specified by count, of unique random test numbers. If it is not important that the /// sequence of numbers is unique it is more efficient to call getRandom() repeatedly /// /// The number of numbers to return - let getRandomWithCount(count) = allPinsShuffled() |> Seq.take count + let getRandomWithCount count = allPinsShuffled() |> Seq.take count - let internal isTestNumberTuple (year, month, day, birthNumber, checksum) = - allPins |> Array.contains (year, month, day, birthNumber, checksum) + let internal isTestNumberTuple values = + allPins |> Array.contains values /// /// Checks if a SwedishPersonalIdentityNumber is a test number /// /// A SwedishPersonalIdentityNumber let isTestNumber pin = - let asTuple + let asTuple { SwedishPersonalIdentityNumber.Year = year Month = month Day = day BirthNumber = birthNumber Checksum = checksum } = - (Year.value year, Month.value month, Day.value day, BirthNumber.value birthNumber, Checksum.value checksum) - pin |> asTuple |> isTestNumberTuple + (year.Value, month.Value, day.Value, birthNumber.Value, checksum.Value) + pin + |> asTuple + |> isTestNumberTuple module SwedishPersonalIdentityNumber = /// @@ -75,3 +77,61 @@ module SwedishPersonalIdentityNumber = /// A SwedishPersonalIdentityNumber let isTestNumber = SwedishPersonalIdentityNumberTestData.isTestNumber + +/// A module that provides easy access to the official test numbers for Swedish Coordination Number (Samordningsnummer) +/// from Skatteverket +module SwedishCoordinationNumberTestData = + open AllCoordNums + let internal shuffledCoordNums() = allCoordNums |> Array.sortBy random + + /// All the testdata from Skatteverket presented as an array of 12 digit strings. + let raw12DigitStrings = + allCoordNums + |> Array.map (fun (year, month, day, birthNumber, checksum) -> sprintf "%04i%02i%02i%03i%i" year month day birthNumber checksum) + + let internal create values = + match SwedishCoordinationNumber.create values with + | Ok p -> p + | Error err -> failwithf "broken test data %A for %A" err values + + /// A sequence of all test numbers ordered by date descending + let allCoordNumsByDateDesc() = seq { for coordNum in allCoordNums do yield create coordNum } + /// A sequence of all test numbers in random order + let allCoordNumsShuffled() = seq { for coordNum in shuffledCoordNums() do yield create coordNum } + /// A random test number + let getRandom() = + let index = rng(0, Array.length allCoordNums - 1) + allCoordNums.[index] + |> create + /// + /// Returns a sequence of length specified by count, of unique random test numbers. If it is not important that the + /// sequence of numbers is unique it is more efficient to call getRandom() repeatedly + /// + /// The number of numbers to return + let getRandomWithCount count = allCoordNumsShuffled() |> Seq.take count + + let internal isTestNumberTuple (year, month, day, birthNumber, checksum) = + allCoordNums |> Array.contains (year, month, day, birthNumber, checksum) + + /// + /// Checks if a SwedishCoordinationNumber is a test number + /// + /// A SwedishCoordinationNumber + let isTestNumber coordNum = + let asTuple + { SwedishCoordinationNumber.Year = year + Month = month + CoordinationDay = day + BirthNumber = birthNumber + Checksum = checksum } = + (year.Value, month.Value, day.Value, birthNumber.Value, checksum.Value) + coordNum + |> asTuple + |> isTestNumberTuple + +module SwedishCoordinationNumber = + /// + /// Checks if a SwedishCoordinationNumber is a test number + /// + /// A SwedishCoordinationNumber + let isTestNumber = SwedishCoordinationNumberTestData.isTestNumber diff --git a/src/ActiveLogin.Identity.Swedish/ActiveLogin.Identity.Swedish.fsproj b/src/ActiveLogin.Identity.Swedish/ActiveLogin.Identity.Swedish.fsproj index 6838d47..6f5a679 100644 --- a/src/ActiveLogin.Identity.Swedish/ActiveLogin.Identity.Swedish.fsproj +++ b/src/ActiveLogin.Identity.Swedish/ActiveLogin.Identity.Swedish.fsproj @@ -50,12 +50,24 @@ + + + + + + + + + + + + diff --git a/src/ActiveLogin.Identity.Swedish/CompanyIdentityNumber.fs b/src/ActiveLogin.Identity.Swedish/CompanyIdentityNumber.fs new file mode 100644 index 0000000..3916ad5 --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish/CompanyIdentityNumber.fs @@ -0,0 +1,38 @@ +module ActiveLogin.Identity.Swedish.FSharp.CompanyIdentityNumber +open ActiveLogin.Identity.Swedish.FSharp + +/// +/// Creates a out of the individual parts. +/// +/// IdentityNumberValues containing all the number parts +let create values = + // maybe this should be applicative instead of monadic? + result { + match SwedishCompanyRegistrationNumber.create values with + | Ok num -> return Company num + | Error _ -> + match SwedishPersonalIdentityNumber.create values with + | Ok num -> return num |> Personal |> Individual + | Error _ -> + match SwedishCoordinationNumber.create values with + | Ok num -> return num |> Coordination |> Individual + | Error _ -> return! "Not a valid company registration number" |> ParsingError.Invalid |> ParsingError |> Error + } + +/// +/// Converts the value of the current object to its equivalent 12 digit string representation. +/// Format is YYYYMMDDBBBC, for example 199008672397 or 191202719983. +/// +/// A SwedishCompanyRegistrationNumber +let to12DigitString (num: CompanyIdentityNumber) = + match num with + | Company num -> SwedishCompanyRegistrationNumber.to12DigitString num + | Individual (Personal pin) -> SwedishPersonalIdentityNumber.to12DigitString pin + | Individual (Coordination num) -> SwedishCoordinationNumber.to12DigitString num + +/// +/// Converts the string representation of the Swedish company registration number to its equivalent. +/// +/// A string representation of the Swedish company registration number to parse. +let parse str = Parse.parse create str + diff --git a/src/ActiveLogin.Identity.Swedish/CompanyIdentityNumberCSharp.fs b/src/ActiveLogin.Identity.Swedish/CompanyIdentityNumberCSharp.fs new file mode 100644 index 0000000..374d4aa --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish/CompanyIdentityNumberCSharp.fs @@ -0,0 +1,102 @@ +namespace ActiveLogin.Identity.Swedish + +open ActiveLogin.Identity.Swedish.FSharp +open System.Runtime.InteropServices //for OutAttribute + + +/// +/// Represents a Swedish Company Registration Number (organisationsnummer). +/// https://en.wikipedia.org/wiki/Personal_identity_number_(Sweden) +/// https://sv.wikipedia.org/wiki/Personnummer_i_Sverige +/// +[] +type CompanyIdentityNumberCSharp internal(num :CompanyIdentityNumber) = + + /// + /// Creates an instance of out of the individual parts. + /// + /// The year part. + /// The month part. + /// The day part. + /// The birth number part. + /// The checksum part. + /// An instance of if all the paramaters are valid by themselfes and in combination. + /// Thrown when any of the range arguments is invalid. + /// Thrown when checksum is invalid. + new(year, month, day, birthNumber, checksum) = + let idNum = CompanyIdentityNumber.create (year, month, day, birthNumber, checksum) |> Error.handle + + CompanyIdentityNumberCSharp(idNum) + + member internal __.IdentityNumber = num + + member this.SwedishCompanyRegistrationNumber = + match num with + | Company num -> num |> SwedishCompanyRegistrationNumberCSharp + | _ -> Unchecked.defaultof + + member this.SwedishPersonalIdentityNumber = + match num with + | Individual (Personal pin) -> pin |> SwedishPersonalIdentityNumberCSharp + | _ -> Unchecked.defaultof + + member this.SwedishCoordinationNumber = + match num with + | Individual (Coordination num) -> num |> SwedishCoordinationNumberCSharp + | _ -> Unchecked.defaultof + + /// + /// Converts the string representation of the Swedish company registration number to its equivalent. + /// + /// A string representation of the Swedish company registration number to parse. + /// Thrown when string input is null. + /// Thrown when string input cannot be recognized as a valid SwedishCompanyRegistrationNumber. + static member Parse(s) = + CompanyIdentityNumber.parse s + |> Error.handle + |> CompanyIdentityNumberCSharp + + /// + /// Converts the string representation of the company registration number to its + /// equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// A string representation of the Swedish company registration number to parse. + /// If valid, an instance of + static member TryParse((s : string), [] parseResult : CompanyIdentityNumberCSharp byref) = + let num = CompanyIdentityNumber.parse s + match num with + | Error _ -> false + | Ok num -> + parseResult <- (num |> CompanyIdentityNumberCSharp) + true + + /// + /// Converts the value of the current object to its equivalent 12 digit string representation. + /// Format is YYYYMMDDBBBC, for example 19908072391 or 191202119986. + /// + member __.To12DigitString() = CompanyIdentityNumber.to12DigitString num + + /// + /// Converts the value of the current object to its equivalent 12 digit string representation. + /// Format is YYYYMMDDBBBC, for example 19908072391 or 191202119986. + /// + override __.ToString() = __.To12DigitString() + + + /// Returns a value indicating whether this instance is equal to a specified object. + /// The object to compare to this instance. + /// true if value is an instance of and equals the value of this instance; otherwise, false. + override __.Equals(b) = + match b with + | :? CompanyIdentityNumberCSharp as n -> num = n.IdentityNumber + | _ -> false + + /// Returns the hash code for this instance. + /// A 32-bit signed integer hash code. + override __.GetHashCode() = hash num + + static member op_Equality (left: CompanyIdentityNumberCSharp, right: CompanyIdentityNumberCSharp) = + match box left, box right with + | (null, null) -> true + | (null, _) | (_, null) -> false + | _ -> left.IdentityNumber = right.IdentityNumber diff --git a/src/ActiveLogin.Identity.Swedish/HintsHelper.fs b/src/ActiveLogin.Identity.Swedish/HintsHelper.fs new file mode 100644 index 0000000..8cc93a5 --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish/HintsHelper.fs @@ -0,0 +1,30 @@ +module internal ActiveLogin.Identity.Swedish.FSharp.HintsHelper + +open System +open ActiveLogin.Identity.Swedish + +let getDateOfBirthHint (num : IndividualIdentityNumber) = + let day = + match num with + | Personal pin -> pin.Day.Value + | Coordination num -> num.RealDay + DateTime(num.Year.Value, num.Month.Value, day, 0, 0, 0, DateTimeKind.Utc) + +let getAgeHintOnDate (date : DateTime) num = + let dateOfBirth = getDateOfBirthHint num + if date >= dateOfBirth then + let months = 12 * (date.Year - dateOfBirth.Year) + (date.Month - dateOfBirth.Month) + match date.Day < dateOfBirth.Day with + | true -> + let years = (months - 1) / 12 + years |> Some + | false -> months / 12 |> Some + else None + +let getAgeHint num = getAgeHintOnDate DateTime.UtcNow num + +let getGenderHint (num : IndividualIdentityNumber) = + let isBirthNumberEven = (num.BirthNumber |> BirthNumber.value) % 2 = 0 + if isBirthNumberEven then Gender.Female + else Gender.Male + diff --git a/src/ActiveLogin.Identity.Swedish/IndividualIdentityNumber.fs b/src/ActiveLogin.Identity.Swedish/IndividualIdentityNumber.fs new file mode 100644 index 0000000..281e400 --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish/IndividualIdentityNumber.fs @@ -0,0 +1,99 @@ +module ActiveLogin.Identity.Swedish.FSharp.IndividualIdentityNumber + +open ActiveLogin.Identity.Swedish.FSharp + +/// +/// Creates a out of the individual parts. +/// +/// IdentityNumberValues containing all the number parts +let create values = + result { + match SwedishPersonalIdentityNumber.create values with + | Ok p -> return p |> Personal + | Error _ -> + match SwedishCoordinationNumber.create values with + | Ok coordNum -> return coordNum |> Coordination + | Error _ -> return! ParsingError.Invalid "Not a pin or coordination number" |> ParsingError |> Error + } + +/// +/// Converts the string representation of the identity number to its equivalent. +/// +/// +/// The specific year to use when checking if the person has turned / will turn 100 years old. +/// That information changes the delimiter (- or +). +/// +/// For more info, see: https://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/folkbokforingslag-1991481_sfs-1991-481#P18 +/// +/// A string representation of the identity number to parse. +let parseInSpecificYear parseYear str = Parse.parseInSpecificYear create parseYear str + +/// +/// Converts the string representation of the identity number to its equivalent. +/// +/// A string representation of the identity number to parse. +let parse str = Parse.parse create str + +/// +/// Converts a IdentityNumber to its equivalent 10 digit string representation. The total length, including the separator, will be 11 chars. +/// +/// +/// The specific year to use when checking if the person has turned / will turn 100 years old. +/// That information changes the delimiter (- or +). +/// +/// For more info, see: https://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/folkbokforingslag-1991481_sfs-1991-481#P18 +/// +/// An IdentityNumber +let to10DigitStringInSpecificYear serializationYear (num: IndividualIdentityNumber) = + num + |> StringHelpers.to10DigitStringInSpecificYear serializationYear + +/// +/// Converts a IdentityNumber to its equivalent 10 digit string representation. The total length, including the separator, will be 11 chars. +/// +/// An IdentityNumber +let to10DigitString (num : IndividualIdentityNumber) = + num + |> StringHelpers.to10DigitString + +/// +/// Converts the value of the current object to its equivalent 12 digit string representation. +/// Format is YYYYMMDDBBBC, for example 199008672397 or 191202719983. +/// +/// An IdentityNumber +let to12DigitString num = + num + |> StringHelpers.to12DigitString + +module Hints = + open ActiveLogin.Identity.Swedish + + /// + /// Date of birth for the person according to the identity number. + /// Not always the actual date of birth due to the limited quantity of identity numbers per day. + /// + /// An IdentityNumber + let getDateOfBirthHint num = HintsHelper.getDateOfBirthHint num + + /// + /// Get the age of the person according to the date in the identity number. + /// Not always the actual date of birth due to the limited quantity of identity numbers per day. + /// + /// The date when to calculate the age. + /// An IdentityNumber + let getAgeHintOnDate date num = HintsHelper.getAgeHintOnDate date num + + /// + /// Get the age of the person according to the date in the identity number. + /// Not always the actual date of birth due to the limited quantity of identity numbers per day. + /// + /// An IdentityNumber + let getAgeHint num = HintsHelper.getAgeHint num + + /// + /// Gender (juridiskt kön) in Sweden according to the last digit of the birth number in the identity number. + /// Odd number: Male + /// Even number: Female + /// + /// An IdentityNumber + let getGenderHint num = HintsHelper.getGenderHint num diff --git a/src/ActiveLogin.Identity.Swedish/IndividualIdentityNumberCSharp.fs b/src/ActiveLogin.Identity.Swedish/IndividualIdentityNumberCSharp.fs new file mode 100644 index 0000000..07f6b5c --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish/IndividualIdentityNumberCSharp.fs @@ -0,0 +1,204 @@ +namespace ActiveLogin.Identity.Swedish + +open ActiveLogin.Identity.Swedish.FSharp +open IndividualIdentityNumber +open System +open System.Runtime.InteropServices //for OutAttribute + +/// +/// Represents a Swedish Identity Number. +/// https://en.wikipedia.org/wiki/Personal_identity_number_(Sweden) +/// https://sv.wikipedia.org/wiki/Personnummer_i_Sverige +/// +[] +type IndividualIdentityNumberCSharp internal(num: IndividualIdentityNumber) = + + /// + /// Creates an instance of a out of the individual parts. + /// + /// The year part. + /// The month part. + /// The day part. + /// The birth number part. + /// The checksum part. + /// An instance of if all the paramaters are valid by themselfes and in combination. + /// Thrown when any of the range arguments is invalid. + /// Thrown when checksum is invalid. + private new(year, month, day, birthNumber, checksum) = + let idNum = (year, month, day, birthNumber, checksum) |> create |> Error.handle + + IndividualIdentityNumberCSharp(idNum) + + member this.SwedishPersonalIdentityNumber = + match num with + | Personal pin -> pin |> SwedishPersonalIdentityNumberCSharp + | _ -> Unchecked.defaultof + + member this.SwedishCoordinationNumber = + match num with + | Coordination num -> num |> SwedishCoordinationNumberCSharp + | _ -> Unchecked.defaultof + + /// + /// The year for date of birth. + /// + member __.Year = num.Year.Value + + /// + /// The month for date of birth. + /// + member __.Month = num.Month.Value + + /// + /// The coordination day (this is the day for date of birth + 60) + /// + member __.Day = match num.Day with | Day d -> d.Value | CoordinationDay cd -> cd.Value + + /// + /// A birth number (födelsenummer) to distinguish people born on the same day. + /// + member __.BirthNumber = num.BirthNumber.Value + + /// + /// A checksum (kontrollsiffra) used for validation. Last digit in the number. + /// + member __.Checksum = num.Checksum.Value + + member __.IsSwedishPersonalIdentityNumber = num.IsSwedishPersonalIdentityNumber + + member __.IsSwedishCoordinationNumber = num.IsSwedishCoordinationNumber + + /// + /// Converts the string representation of the Swedish identity number to its equivalent. + /// + /// A string representation of the Swedish identity number to parse. + /// + /// The specific year to use when checking if the person has turned / will turn 100 years old. + /// That information changes the delimiter (- or +). + /// + /// For more info, see: https://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/folkbokforingslag-1991481_sfs-1991-481#P18 + /// + /// Thrown when string input is null. + /// Thrown when string input cannot be recognized as a valid IdentityNumber. + static member ParseInSpecificYear((s : string), parseYear : int) = + result { let! year = parseYear |> Year.create + return! parseInSpecificYear year s } + |> Error.handle + |> IndividualIdentityNumberCSharp + + member internal __.IdentityNumber = num + + /// + /// Converts the string representation of the Swedish coordination number to its equivalent. + /// + /// A string representation of the Swedish coordination number to parse. + /// Thrown when string input is null. + /// Thrown when string input cannot be recognized as a valid IdentityNumber. + static member Parse(s) = + parse s + |> Error.handle + |> IndividualIdentityNumberCSharp + + /// + /// Converts the string representation of the coordination number to its + /// equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// A string representation of the Swedish coordination number to parse. + /// + /// The specific year to use when checking if the person has turned / will turn 100 years old. + /// That information changes the delimiter (- or +). + /// + /// For more info, see: https://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/folkbokforingslag-1991481_sfs-1991-481#P18 + /// + /// If valid, an instance of + static member TryParseInSpecificYear((s : string), (parseYear : int), + [] parseResult : IndividualIdentityNumberCSharp byref) = + let num = result { let! year = parseYear |> Year.create + return! parseInSpecificYear year s } + match num with + | Error _ -> false + | Ok num -> + parseResult <- (num |> IndividualIdentityNumberCSharp) + true + + /// + /// Converts the string representation of the coordination number to its + /// equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// A string representation of the Swedish coordination number to parse. + /// If valid, an instance of + static member TryParse((s : string), [] parseResult : IndividualIdentityNumberCSharp byref) = + let num = parse s + match num with + | Error _ -> false + | Ok num -> + parseResult <- (num |> IndividualIdentityNumberCSharp) + true + + /// + /// Creates an instance of a out of a swedish personal identity number. + /// + /// The SwedishPersonalIdentityNumber. + /// An instance of + static member FromSwedishPersonalIdentityNumber(pin: SwedishPersonalIdentityNumberCSharp) = + IndividualIdentityNumberCSharp(Personal pin.IdentityNumber) + + /// + /// Creates an instance of a out of a swedish coordination number. + /// + /// The SwedishCoordinationNumber. + /// An instance of + static member FromSwedishCoordinationNumber(num: SwedishCoordinationNumberCSharp) = + IndividualIdentityNumberCSharp(Coordination num.IdentityNumber) + + /// + /// Converts the value of the current object to its equivalent 10 digit string representation. The total length, including the separator, will be 11 chars. + /// Format is YYMMDDXBBBC, for example 990807-2391 or 120211+9986. + /// + /// + /// The specific year to use when checking if the person has turned / will turn 100 years old. + /// That information changes the delimiter (- or +). + /// + /// For more info, see: https://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/folkbokforingslag-1991481_sfs-1991-481#P18 + /// + member __.To10DigitStringInSpecificYear(serializationYear : int) = + match serializationYear |> Year.create with + | Error _ -> raise (ArgumentOutOfRangeException("year", serializationYear, "Invalid year.")) + | Ok year -> to10DigitStringInSpecificYear year num |> Error.handle + + /// + /// Converts the value of the current object to its equivalent short string representation. + /// Format is YYMMDDXBBBC, for example 990807-2391 or 120211+9986. + /// + member __.To10DigitString() = to10DigitString num |> Error.handle + + /// + /// Converts the value of the current object to its equivalent 12 digit string representation. + /// Format is YYYYMMDDBBBC, for example 19908072391 or 191202119986. + /// + member __.To12DigitString() = to12DigitString num + + /// + /// Converts the value of the current object to its equivalent 12 digit string representation. + /// Format is YYYYMMDDBBBC, for example 19908072391 or 191202119986. + /// + override __.ToString() = __.To12DigitString() + + /// Returns a value indicating whether this instance is equal to a specified object. + /// The object to compare to this instance. + /// true if value is an instance of and equals the value of this instance; otherwise, false. + override __.Equals(b) = + match b with + | :? IndividualIdentityNumberCSharp as n -> num = n.IdentityNumber + | _ -> false + + /// Returns the hash code for this instance. + /// A 32-bit signed integer hash code. + override __.GetHashCode() = hash num + + static member op_Equality (left: IndividualIdentityNumberCSharp, right: IndividualIdentityNumberCSharp) = + match box left, box right with + | (null, null) -> true + | (null, _) | (_, null) -> false + | _ -> left.IdentityNumber = right.IdentityNumber + diff --git a/src/ActiveLogin.Identity.Swedish/IndividualIdentityNumberCSharpHintExtensions.fs b/src/ActiveLogin.Identity.Swedish/IndividualIdentityNumberCSharpHintExtensions.fs new file mode 100644 index 0000000..f34d2c6 --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish/IndividualIdentityNumberCSharpHintExtensions.fs @@ -0,0 +1,50 @@ +namespace ActiveLogin.Identity.Swedish.Extensions +open System +open System.Runtime.CompilerServices +open ActiveLogin.Identity.Swedish +open ActiveLogin.Identity.Swedish.FSharp + +[] +type IdentityNumberCSharpHintExtensions() = + + /// + /// Date of birth for the person according to the identity number. + /// Not always the actual date of birth due to the limited quantity of identity numbers per day. + /// + [] + static member GetDateOfBirthHint(num : IndividualIdentityNumberCSharp) = + IndividualIdentityNumber.Hints.getDateOfBirthHint num.IdentityNumber + + /// + /// Gender (juridiskt kön) in Sweden according to the last digit of the birth number in the identity number. + /// Odd number: Male + /// Even number: Female + /// + [] + static member GetGenderHint(num : IndividualIdentityNumberCSharp) = + IndividualIdentityNumber.Hints.getGenderHint num.IdentityNumber + + /// + /// Get the age of the person according to the date in the identity number. + /// Not always the actual date of birth due to the limited quantity of identity numbers per day. + /// + /// + /// The date when to calculate the age. + /// + [] + static member GetAgeHint(num : IndividualIdentityNumberCSharp, date : DateTime) = + IndividualIdentityNumber.Hints.getAgeHintOnDate date num.IdentityNumber + |> function + | None -> invalidArg "num" "The person is not yet born." + | Some i -> i + + /// + /// Get the age of the person according to the date in the identity number. + /// Not always the actual date of birth due to the limited quantity of identity numbers per day. + /// + [] + static member GetAgeHint(num : IndividualIdentityNumberCSharp) = + IndividualIdentityNumber.Hints.getAgeHint num.IdentityNumber + |> function + | None -> invalidArg "num" "The person is not yet born." + | Some i -> i diff --git a/src/ActiveLogin.Identity.Swedish/Parse.fs b/src/ActiveLogin.Identity.Swedish/Parse.fs index 4218d3c..f12abb5 100644 --- a/src/ActiveLogin.Identity.Swedish/Parse.fs +++ b/src/ActiveLogin.Identity.Swedish/Parse.fs @@ -1,104 +1,139 @@ -module internal ActiveLogin.Identity.Swedish.FSharp.Parse - -open System - -type private PinType<'T> = - | TwelveDigits of 'T - | TenDigits of 'T - -let parse parseYear = - let toChars str = [ for c in str -> c ] - let toString = Array.ofList >> String - - let requireNotEmpty str = - match String.IsNullOrWhiteSpace str with - | false -> str |> Ok - | true when isNull str -> - ArgumentNullError - |> Error - | true -> - Empty - |> ParsingError - |> Error - - let requireDigitCount (str : string) = - let chars = str |> toChars - - let numDigits = - chars - |> List.filter Char.IsDigit - |> List.length - match numDigits with - | 10 -> (chars |> TenDigits) |> Ok - | 12 -> (chars |> TwelveDigits) |> Ok - | _ -> - Length - |> ParsingError - |> Error - - let clean numberType = - let (|IsDigit|IsPlus|NotDigitOrPlus|) char = - match char |> Char.IsDigit with - | true -> IsDigit - | false when char = '+' -> IsPlus - | false -> NotDigitOrPlus - - let folder char state = - match state |> List.length, char with - | 4, IsPlus -> char :: state - | 4, IsDigit -> char :: ('-' :: state) - | _, IsDigit -> char :: state - | _ -> state - - match numberType with - | TwelveDigits chars -> - chars - |> List.filter Char.IsDigit - |> toString - |> TwelveDigits - | TenDigits chars -> - (chars, []) - ||> List.foldBack folder - |> toString - |> TenDigits - - let parseNumberValues (numberType : PinType) = - result { - match numberType with - | TwelveDigits str -> - // YYYYMMDDbbbc - // 012345678901 - return { Year = str.[0..3] |> int - Month = str.[4..5] |> int - Day = str.[6..7] |> int - BirthNumber = str.[8..10] |> int - Checksum = str.[11..11] |> int } - | TenDigits str -> - // YYMMDD-bbbc or YYMMDD+bbbc - // 01234567890 01234567890 - let shortYear = (str.[0..1] |> int) - let getCentury (year : int) = (year / 100) * 100 - let parseYear = Year.value parseYear - let parseCentury = getCentury parseYear - let fullYearGuess = parseCentury + shortYear - let lastDigitsParseYear = parseYear % 100 - let delimiter = str.[6] - - let! fullYear = - match delimiter with - | '-' when shortYear <= lastDigitsParseYear -> fullYearGuess |> Ok - | '-' -> fullYearGuess - 100 |> Ok - | '+' when shortYear <= lastDigitsParseYear -> fullYearGuess - 100 |> Ok - | '+' -> fullYearGuess - 200 |> Ok - | _ -> "delimiter" |> Invalid |> ParsingError |> Error - return { Year = fullYear - Month = str.[2..3] |> int - Day = str.[4..5] |> int - BirthNumber = str.[7..9] |> int - Checksum = str.[10..10] |> int } - } - - requireNotEmpty - >> Result.bind requireDigitCount - >> Result.map clean - >> Result.bind parseNumberValues +module internal ActiveLogin.Identity.Swedish.FSharp.Parse + +open System + +type private PinType<'T> = + | TwelveDigits of 'T + | TenDigits of 'T + +type private IdNumberType = + | Personal of ParseYear: Year + | CompanyNumber + + +let private (|IsDigit|IsPlus|NotDigitOrPlus|) char = + match char |> Char.IsDigit with + | true -> IsDigit + | false when char = '+' -> IsPlus + | false -> NotDigitOrPlus + +let private parseInternal (numberType: IdNumberType) = + let toChars str = [ for c in str -> c ] + let toString = Array.ofList >> String + + let requireNotEmpty str = + match String.IsNullOrWhiteSpace str with + | false -> str |> Ok + | true when isNull str -> + ArgumentNullError + |> Error + | true -> + Empty + |> ParsingError + |> Error + + let requireDigitCount (str : string) = + let chars = str |> toChars + + let numDigits = + chars + |> List.filter Char.IsDigit + |> List.length + match numDigits with + | 10 -> (chars |> TenDigits) |> Ok + | 12 -> (chars |> TwelveDigits) |> Ok + | _ -> + Length + |> ParsingError + |> Error + + let clean numDigits = + let folder char state = + match state |> List.length, char with + | 4, IsPlus -> char :: state + | 4, IsDigit -> char :: ('-' :: state) + | _, IsDigit -> char :: state + | _ -> state + + match numDigits with + | TwelveDigits chars -> + chars + |> List.filter Char.IsDigit + |> toString + |> TwelveDigits + | TenDigits chars -> + (chars, []) + ||> List.foldBack folder + |> toString + |> TenDigits + + let parseNumberValues (numDigits : PinType) = + result { + match numDigits with + | TwelveDigits str -> + // YYYYMMDDbbbc + // 012345678901 + let year = str.[0..3] |> int + let month = str.[4..5] |> int + let day = str.[6..7] |> int + let birthNumber = str.[8..10] |> int + let checksum = str.[11..11] |> int + return (year, month, day, birthNumber, checksum) + | TenDigits str -> + match numberType with + | Personal parseYear -> + // YYMMDD-bbbc or YYMMDD+bbbc + // 01234567890 01234567890 + let shortYear = (str.[0..1] |> int) + let getCentury (year : int) = (year / 100) * 100 + let parseYear = Year.value parseYear + let parseCentury = getCentury parseYear + let fullYearGuess = parseCentury + shortYear + let lastDigitsParseYear = parseYear % 100 + let delimiter = str.[6] + + let! fullYear = + match delimiter with + | '-' when shortYear <= lastDigitsParseYear -> fullYearGuess |> Ok + | '-' -> fullYearGuess - 100 |> Ok + | '+' when shortYear <= lastDigitsParseYear -> fullYearGuess - 100 |> Ok + | '+' -> fullYearGuess - 200 |> Ok + | _ -> "delimiter" |> Invalid |> ParsingError |> Error + let month = str.[2..3] |> int + let day = str.[4..5] |> int + let birthNumber = str.[7..9] |> int + let checksum = str.[10..10] |> int + + return (fullYear, month, day, birthNumber, checksum) + | CompanyNumber -> + // XXYYZZ-QQQC + // 01234567890 + let delimiter = str.[6] + match delimiter with + | '-' -> + let x = "16" + str.[0..1] |> int // TODO remove magic number + let y = str.[2..3] |> int + let z = str.[4..5] |> int + let q = str.[7..9] |> int + let checksum = str.[10..10] |> int + return (x, y, z, q, checksum) + | _ -> return! "delimiter" |> Invalid |> ParsingError |> Error + } + + requireNotEmpty + >> Result.bind requireDigitCount + >> Result.map clean + >> Result.bind parseNumberValues + +let parseInSpecificYear createFunc parseYear str = + parseInternal (Personal parseYear) str + |> Result.bind createFunc + |> Result.mapError ParsingError.toParsingError + +let parse createFunc str = result { let! year = DateTime.UtcNow.Year |> Year.create + return! parseInSpecificYear createFunc year str } + +let parseCompanyNumber createFunc str = + parseInternal IdNumberType.CompanyNumber str + |> Result.bind createFunc + |> Result.mapError ParsingError.toParsingError diff --git a/src/ActiveLogin.Identity.Swedish/StringHelpers.fs b/src/ActiveLogin.Identity.Swedish/StringHelpers.fs new file mode 100644 index 0000000..c1e8add --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish/StringHelpers.fs @@ -0,0 +1,57 @@ +module internal ActiveLogin.Identity.Swedish.FSharp.StringHelpers +open ActiveLogin.Identity.Swedish.FSharp +open System + +let private validSerializationYear (serializationYear: Year) (pinYear: Year) = + if serializationYear < pinYear + then + "SerializationYear cannot be a year before the person was born" + |> InvalidSerializationYear + |> Error + + elif (serializationYear |> Year.value) > ((pinYear |> Year.value) + 199) + then + "SerializationYear cannot be a more than 199 years after the person was born" + |> InvalidSerializationYear + |> Error + else + serializationYear |> Ok + +let private parseDay day = + match day with + | Day d -> d.Value + | CoordinationDay cd -> cd.Value + +let to10DigitStringInSpecificYear serializationYear (num : IndividualIdentityNumber) = + result { + let! validYear = validSerializationYear serializationYear num.Year + let delimiter = + if (validYear |> Year.value) - (num.Year |> Year.value) >= 100 then "+" + else "-" + + return sprintf "%02i%02i%02i%s%03i%1i" + (num.Year.Value % 100) + num.Month.Value + (num.Day |> parseDay) + delimiter + num.BirthNumber.Value + num.Checksum.Value +} + +let to10DigitString (num : IndividualIdentityNumber) = + let year = + DateTime.UtcNow.Year + |> Year.create + |> function + | Ok y -> y + | Error _ -> invalidArg "year" "DateTime.Year wasn't a valid year" + to10DigitStringInSpecificYear year num + +let to12DigitString (num: IndividualIdentityNumber) = + sprintf "%02i%02i%02i%03i%1i" + num.Year.Value + num.Month.Value + (num.Day |> parseDay) + num.BirthNumber.Value + num.Checksum.Value + diff --git a/src/ActiveLogin.Identity.Swedish/SwedishCompanyRegistrationNumber.fs b/src/ActiveLogin.Identity.Swedish/SwedishCompanyRegistrationNumber.fs new file mode 100644 index 0000000..85f8fdf --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish/SwedishCompanyRegistrationNumber.fs @@ -0,0 +1,60 @@ +module ActiveLogin.Identity.Swedish.FSharp.SwedishCompanyRegistrationNumber +open ActiveLogin.Identity.Swedish.FSharp + +let create (x, y, z, q, checksum) = + result { + let digits = + if x < 99 + then + sprintf "%02i%02i%02i%03i" x y z q + else + sprintf "%04i%02i%02i%03i" x y z q + |> (fun str -> str |> Seq.map (fun s -> s.ToString() |> int)) + let! x = x |> X.create + let! y = y |> Y.create + let! z = z |> Z.create + let! q = q |> Q.create + let! c = checksum |> Checksum.createFromDigits digits + return { SwedishCompanyRegistrationNumber.X = x + Y = y + Z = z + Q = q + Checksum = c } + } + +/// +/// Converts a SwedishCompanyRegistrationNumber to its equivalent 10 digit string representation. The total length, including the separator, will be 11 chars. +/// +/// A SwedishCompanyRegistrationNumber +let to10DigitString (num : SwedishCompanyRegistrationNumber) = + result { + let delimiter = "-" + + return sprintf "%02i%02i%02i%s%03i%1i" + (num.X.Value % 100) + num.Y.Value + num.Z.Value + delimiter + num.Q.Value + num.Checksum.Value + } + +/// +/// Converts the value of the current object to its equivalent 12 digit string representation. +/// Format is YYYYMMDDBBBC, for example 199008672397 or 191202719983. +/// +/// A SwedishCompanyRegistrationNumber +let to12DigitString (num: SwedishCompanyRegistrationNumber) = + sprintf "%02i%02i%02i%03i%1i" + num.X.Value + num.Y.Value + num.Z.Value + num.Q.Value + num.Checksum.Value + +/// +/// Converts the string representation of the Swedish company registration number to its equivalent. +/// +/// A string representation of the Swedish company registration number to parse. +let parse str = Parse.parse create str + diff --git a/src/ActiveLogin.Identity.Swedish/SwedishCompanyRegistrationNumberCSharp.fs b/src/ActiveLogin.Identity.Swedish/SwedishCompanyRegistrationNumberCSharp.fs new file mode 100644 index 0000000..2a01b60 --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish/SwedishCompanyRegistrationNumberCSharp.fs @@ -0,0 +1,75 @@ +namespace ActiveLogin.Identity.Swedish + +open ActiveLogin.Identity.Swedish.FSharp +open SwedishCompanyRegistrationNumber +open System.Runtime.InteropServices //for OutAttribute + + +/// +/// Represents a Swedish Company Registration Number (organisationsnummer). +/// https://en.wikipedia.org/wiki/Personal_identity_number_(Sweden) +/// https://sv.wikipedia.org/wiki/Personnummer_i_Sverige +/// +[] +type SwedishCompanyRegistrationNumberCSharp internal(num : SwedishCompanyRegistrationNumber) = + + /// + /// Creates an instance of out of the individual parts. + /// + /// The year part. + /// The month part. + /// The day part. + /// The birth number part. + /// The checksum part. + /// An instance of if all the paramaters are valid by themselfes and in combination. + /// Thrown when any of the range arguments is invalid. + /// Thrown when checksum is invalid. + new(year, month, day, birthNumber, checksum) = + let idNum = create (year, month, day, birthNumber, checksum) |> Error.handle + + SwedishCompanyRegistrationNumberCSharp(idNum) + + member internal __.IdentityNumber = num + + /// + /// Converts the string representation of the Swedish company registration number to its equivalent. + /// + /// A string representation of the Swedish company registration number to parse. + /// Thrown when string input is null. + /// Thrown when string input cannot be recognized as a valid SwedishCompanyRegistrationNumber. + static member Parse(s) = + parse s + |> Error.handle + |> SwedishCompanyRegistrationNumberCSharp + + /// + /// Converts the string representation of the company registration number to its + /// equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// A string representation of the Swedish company registration number to parse. + /// If valid, an instance of + static member TryParse((s : string), [] parseResult : SwedishCompanyRegistrationNumberCSharp byref) = + let num = parse s + match num with + | Error _ -> false + | Ok num -> + parseResult <- (num |> SwedishCompanyRegistrationNumberCSharp) + true + + /// Returns a value indicating whether this instance is equal to a specified object. + /// The object to compare to this instance. + /// true if value is an instance of and equals the value of this instance; otherwise, false. + override __.Equals(b) = + match b with + | :? SwedishCompanyRegistrationNumberCSharp as n -> num = n.IdentityNumber + | _ -> false + + /// Returns the hash code for this instance. + /// A 32-bit signed integer hash code. + override __.GetHashCode() = hash num + + static member op_Equality (left: SwedishCompanyRegistrationNumberCSharp, right: SwedishCompanyRegistrationNumberCSharp) = + match box left, box right with + | (null, null) -> true + | (null, _) | (_, null) -> false + | _ -> left.IdentityNumber = right.IdentityNumber diff --git a/src/ActiveLogin.Identity.Swedish/SwedishCoordinationNumber.fs b/src/ActiveLogin.Identity.Swedish/SwedishCoordinationNumber.fs new file mode 100644 index 0000000..12d278c --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish/SwedishCoordinationNumber.fs @@ -0,0 +1,107 @@ +module ActiveLogin.Identity.Swedish.FSharp.SwedishCoordinationNumber +open ActiveLogin.Identity.Swedish.FSharp + +let toIdentityNumber = Coordination + +/// +/// Creates a out of the individual parts. +/// +/// IdentityNumberValues containing all the number parts +let create (year, month, day, birthNumber, checksum) = + result { + let! y = year |> Year.create + let! m = month |> Month.create + let! d = day |> CoordinationDay.create y m + let! s = birthNumber |> BirthNumber.create + let! c = checksum |> Checksum.create y m (CoordinationDay d) s + return { SwedishCoordinationNumber.Year = y + Month = m + CoordinationDay = d + BirthNumber = s + Checksum = c } + } + +/// +/// Converts a SwedishCoordinationNumber to its equivalent 10 digit string representation. The total length, including the separator, will be 11 chars. +/// +/// +/// The specific year to use when checking if the person has turned / will turn 100 years old. +/// That information changes the delimiter (- or +). +/// +/// For more info, see: https://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/folkbokforingslag-1991481_sfs-1991-481#P18 +/// +/// A SwedishCoordinationNumber +let to10DigitStringInSpecificYear serializationYear (num: SwedishCoordinationNumber) = + num + |> toIdentityNumber + |> StringHelpers.to10DigitStringInSpecificYear serializationYear + +/// +/// Converts a SwedishCoordinationNumber to its equivalent 10 digit string representation. The total length, including the separator, will be 11 chars. +/// +/// A SwedishCoordinationNumber +let to10DigitString (num : SwedishCoordinationNumber) = + num + |> toIdentityNumber + |> StringHelpers.to10DigitString + +/// +/// Converts the value of the current object to its equivalent 12 digit string representation. +/// Format is YYYYMMDDBBBC, for example 199008672397 or 191202719983. +/// +/// A SwedishCoordinationNumber +let to12DigitString num = + num + |> toIdentityNumber + |> StringHelpers.to12DigitString + +/// +/// Converts the string representation of the Swedish coordination number to its equivalent. +/// +/// +/// The specific year to use when checking if the person has turned / will turn 100 years old. +/// That information changes the delimiter (- or +). +/// +/// For more info, see: https://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/folkbokforingslag-1991481_sfs-1991-481#P18 +/// +/// A string representation of the Swedish coordination number to parse. +let parseInSpecificYear parseYear str = Parse.parseInSpecificYear create parseYear str + +/// +/// Converts the string representation of the Swedish coordination number to its equivalent. +/// +/// A string representation of the Swedish coordination number to parse. +let parse str = Parse.parse create str + +module Hints = + open ActiveLogin.Identity.Swedish + + /// + /// Date of birth for the person according to the coordination number. + /// Not always the actual date of birth due to the limited quantity of coordination numbers per day. + /// + /// A SwedishCoordinationNumber + let getDateOfBirthHint num = HintsHelper.getDateOfBirthHint (Coordination num) + + /// + /// Get the age of the person according to the date in the coordination number. + /// Not always the actual date of birth due to the limited quantity of coordination numbers per day. + /// + /// The date when to calculate the age. + /// A SwedishCoordinationNumber + let getAgeHintOnDate date num = HintsHelper.getAgeHintOnDate date (Coordination num) + + /// + /// Get the age of the person according to the date in the coordination number. + /// Not always the actual date of birth due to the limited quantity of coordination numbers per day. + /// + /// A SwedishCoordinationNumber + let getAgeHint num = HintsHelper.getAgeHint (Coordination num) + + /// + /// Gender (juridiskt kön) in Sweden according to the last digit of the birth number in the coordination number. + /// Odd number: Male + /// Even number: Female + /// + /// A SwedishCoordinationNumber + let getGenderHint num = HintsHelper.getGenderHint (Coordination num) diff --git a/src/ActiveLogin.Identity.Swedish/SwedishCoordinationNumberCSharp.fs b/src/ActiveLogin.Identity.Swedish/SwedishCoordinationNumberCSharp.fs new file mode 100644 index 0000000..ce4d8a8 --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish/SwedishCoordinationNumberCSharp.fs @@ -0,0 +1,178 @@ +namespace ActiveLogin.Identity.Swedish + +open ActiveLogin.Identity.Swedish.FSharp +open SwedishCoordinationNumber +open System +open System.Runtime.InteropServices //for OutAttribute + +/// +/// Represents a Swedish Coordination Number (Samordningsnummer). +/// https://en.wikipedia.org/wiki/Personal_identity_number_(Sweden) +/// https://sv.wikipedia.org/wiki/Personnummer_i_Sverige +/// +[] +type SwedishCoordinationNumberCSharp internal(num : SwedishCoordinationNumber) = + + /// + /// Creates an instance of out of the individual parts. + /// + /// The year part. + /// The month part. + /// The day part. + /// The birth number part. + /// The checksum part. + /// An instance of if all the paramaters are valid by themselfes and in combination. + /// Thrown when any of the range arguments is invalid. + /// Thrown when checksum is invalid. + new(year, month, day, birthNumber, checksum) = + let idNum = (year, month, day, birthNumber, checksum) |> create |> Error.handle + + SwedishCoordinationNumberCSharp(idNum) + + member internal __.IdentityNumber = num + + /// + /// The year for date of birth. + /// + member __.Year = num.Year.Value + + /// + /// The month for date of birth. + /// + member __.Month = num.Month.Value + + /// + /// The coordination day (this is the day for date of birth + 60) + /// + member __.CoordinationDay = num.CoordinationDay.Value + + /// + /// The day for date of birth + /// + member __.RealDay = num.RealDay + + /// + /// A birth number (födelsenummer) to distinguish people born on the same day. + /// + member __.BirthNumber = num.BirthNumber.Value + + /// + /// A checksum (kontrollsiffra) used for validation. Last digit in the number. + /// + member __.Checksum = num.Checksum.Value + + /// + /// Converts the string representation of the Swedish coordination number to its equivalent. + /// + /// A string representation of the Swedish coordination number to parse. + /// + /// The specific year to use when checking if the person has turned / will turn 100 years old. + /// That information changes the delimiter (- or +). + /// + /// For more info, see: https://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/folkbokforingslag-1991481_sfs-1991-481#P18 + /// + /// Thrown when string input is null. + /// Thrown when string input cannot be recognized as a valid SwedishCoordinationNumber. + static member ParseInSpecificYear((s : string), parseYear : int) = + result { let! year = parseYear |> Year.create + return! parseInSpecificYear year s } + |> Error.handle + |> SwedishCoordinationNumberCSharp + + /// + /// Converts the string representation of the Swedish coordination number to its equivalent. + /// + /// A string representation of the Swedish coordination number to parse. + /// Thrown when string input is null. + /// Thrown when string input cannot be recognized as a valid SwedishCoordinationNumber. + static member Parse(s) = + parse s + |> Error.handle + |> SwedishCoordinationNumberCSharp + + /// + /// Converts the string representation of the coordination number to its + /// equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// A string representation of the Swedish coordination number to parse. + /// + /// The specific year to use when checking if the person has turned / will turn 100 years old. + /// That information changes the delimiter (- or +). + /// + /// For more info, see: https://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/folkbokforingslag-1991481_sfs-1991-481#P18 + /// + /// If valid, an instance of + static member TryParseInSpecificYear((s : string), (parseYear : int), + [] parseResult : SwedishCoordinationNumberCSharp byref) = + let num = result { let! year = parseYear |> Year.create + return! parseInSpecificYear year s } + match num with + | Error _ -> false + | Ok num -> + parseResult <- (num |> SwedishCoordinationNumberCSharp) + true + + /// + /// Converts the string representation of the coordination number to its + /// equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// A string representation of the Swedish coordination number to parse. + /// If valid, an instance of + static member TryParse((s : string), [] parseResult : SwedishCoordinationNumberCSharp byref) = + let num = parse s + match num with + | Error _ -> false + | Ok num -> + parseResult <- (num |> SwedishCoordinationNumberCSharp) + true + + /// + /// Converts the value of the current object to its equivalent 10 digit string representation. The total length, including the separator, will be 11 chars. + /// Format is YYMMDDXBBBC, for example 990807-2391 or 120211+9986. + /// + /// + /// The specific year to use when checking if the person has turned / will turn 100 years old. + /// That information changes the delimiter (- or +). + /// + /// For more info, see: https://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/folkbokforingslag-1991481_sfs-1991-481#P18 + /// + member __.To10DigitStringInSpecificYear(serializationYear : int) = + match serializationYear |> Year.create with + | Error _ -> raise (ArgumentOutOfRangeException("year", serializationYear, "Invalid year.")) + | Ok year -> to10DigitStringInSpecificYear year num |> Error.handle + + /// + /// Converts the value of the current object to its equivalent short string representation. + /// Format is YYMMDDXBBBC, for example 990807-2391 or 120211+9986. + /// + member __.To10DigitString() = to10DigitString num |> Error.handle + + /// + /// Converts the value of the current object to its equivalent 12 digit string representation. + /// Format is YYYYMMDDBBBC, for example 19908072391 or 191202119986. + /// + member __.To12DigitString() = to12DigitString num + + /// + /// Converts the value of the current object to its equivalent 12 digit string representation. + /// Format is YYYYMMDDBBBC, for example 19908072391 or 191202119986. + /// + override __.ToString() = __.To12DigitString() + + /// Returns a value indicating whether this instance is equal to a specified object. + /// The object to compare to this instance. + /// true if value is an instance of and equals the value of this instance; otherwise, false. + override __.Equals(b) = + match b with + | :? SwedishCoordinationNumberCSharp as n -> num = n.IdentityNumber + | _ -> false + + /// Returns the hash code for this instance. + /// A 32-bit signed integer hash code. + override __.GetHashCode() = hash num + + static member op_Equality (left: SwedishCoordinationNumberCSharp, right: SwedishCoordinationNumberCSharp) = + match box left, box right with + | (null, null) -> true + | (null, _) | (_, null) -> false + | _ -> left.IdentityNumber = right.IdentityNumber diff --git a/src/ActiveLogin.Identity.Swedish/SwedishCoordinationNumberCSharpHintExtensions.fs b/src/ActiveLogin.Identity.Swedish/SwedishCoordinationNumberCSharpHintExtensions.fs new file mode 100644 index 0000000..f4d23db --- /dev/null +++ b/src/ActiveLogin.Identity.Swedish/SwedishCoordinationNumberCSharpHintExtensions.fs @@ -0,0 +1,50 @@ +namespace ActiveLogin.Identity.Swedish.Extensions +open System +open System.Runtime.CompilerServices +open ActiveLogin.Identity.Swedish +open ActiveLogin.Identity.Swedish.FSharp + +[] +type SwedishCoordinationNumberCSharpHintExtensions() = + + /// + /// Date of birth for the person according to the coordination number. + /// Not always the actual date of birth due to the limited quantity of coordination numbers per day. + /// + [] + static member GetDateOfBirthHint(num : SwedishCoordinationNumberCSharp) = + SwedishCoordinationNumber.Hints.getDateOfBirthHint num.IdentityNumber + + /// + /// Gender (juridiskt kön) in Sweden according to the last digit of the birth number in the coordination number. + /// Odd number: Male + /// Even number: Female + /// + [] + static member GetGenderHint(num : SwedishCoordinationNumberCSharp) = + SwedishCoordinationNumber.Hints.getGenderHint num.IdentityNumber + + /// + /// Get the age of the person according to the date in the coordination number. + /// Not always the actual date of birth due to the limited quantity of coordination numbers per day. + /// + /// + /// The date when to calculate the age. + /// + [] + static member GetAgeHint(num : SwedishCoordinationNumberCSharp, date : DateTime) = + SwedishCoordinationNumber.Hints.getAgeHintOnDate date num.IdentityNumber + |> function + | None -> invalidArg "num" "The person is not yet born." + | Some i -> i + + /// + /// Get the age of the person according to the date in the coordination number. + /// Not always the actual date of birth due to the limited quantity of coordination numbers per day. + /// + [] + static member GetAgeHint(num : SwedishCoordinationNumberCSharp) = + SwedishCoordinationNumber.Hints.getAgeHint num.IdentityNumber + |> function + | None -> invalidArg "num" "The person is not yet born." + | Some i -> i diff --git a/src/ActiveLogin.Identity.Swedish/SwedishPersonalIdentityNumber.fs b/src/ActiveLogin.Identity.Swedish/SwedishPersonalIdentityNumber.fs index ed81a75..d4baf16 100644 --- a/src/ActiveLogin.Identity.Swedish/SwedishPersonalIdentityNumber.fs +++ b/src/ActiveLogin.Identity.Swedish/SwedishPersonalIdentityNumber.fs @@ -1,18 +1,18 @@ module ActiveLogin.Identity.Swedish.FSharp.SwedishPersonalIdentityNumber -open System +let toIdentityNumber = Personal /// /// Creates a out of the individual parts. /// -/// SwedishPersonalIdentityNumberValues containing all the number parts -let create (values : SwedishPersonalIdentityNumberValues) = +/// IdentityNumberValues containing all the number parts +let create (year, month, day, birthNumber, checksum) = result { - let! y = values.Year |> Year.create - let! m = values.Month |> Month.create - let! d = values.Day |> Day.create y m - let! s = values.BirthNumber |> BirthNumber.create - let! c = values.Checksum |> Checksum.create y m d s + let! y = year |> Year.create + let! m = month |> Month.create + let! d = day |> Day.create y m + let! s = birthNumber |> BirthNumber.create + let! c = checksum |> Checksum.create y m (Day d) s return { SwedishPersonalIdentityNumber.Year = y Month = m Day = d @@ -20,28 +20,6 @@ let create (values : SwedishPersonalIdentityNumberValues) = Checksum = c } } -let private extractValues (pin : SwedishPersonalIdentityNumber) : SwedishPersonalIdentityNumberValues = - { Year = pin.Year |> Year.value - Month = pin.Month |> Month.value - Day = pin.Day |> Day.value - BirthNumber = pin.BirthNumber |> BirthNumber.value - Checksum = pin.Checksum |> Checksum.value } - -let private validSerializationYear (serializationYear: Year) (pinYear: Year) = - if serializationYear < pinYear - then - "SerializationYear cannot be a year before the person was born" - |> InvalidSerializationYear - |> Error - - elif (serializationYear |> Year.value) > ((pinYear |> Year.value) + 199) - then - "SerializationYear cannot be a more than 199 years after the person was born" - |> InvalidSerializationYear - |> Error - else - serializationYear |> Ok - /// /// Converts a SwedishPersonalIdentityNumber to its equivalent 10 digit string representation. The total length, including the separator, will be 11 chars. /// @@ -52,56 +30,29 @@ let private validSerializationYear (serializationYear: Year) (pinYear: Year) = /// For more info, see: https://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/folkbokforingslag-1991481_sfs-1991-481#P18 /// /// A SwedishPersonalIdentityNumber -let to10DigitStringInSpecificYear serializationYear (pin : SwedishPersonalIdentityNumber) = - result { - let! validYear = validSerializationYear serializationYear pin.Year - let delimiter = - if (validYear |> Year.value) - (pin.Year |> Year.value) >= 100 then "+" - else "-" - - let vs = extractValues pin - return sprintf "%02i%02i%02i%s%03i%1i" (vs.Year % 100) vs.Month vs.Day delimiter vs.BirthNumber vs.Checksum - } +let to10DigitStringInSpecificYear serializationYear pin = + pin + |> toIdentityNumber + |> StringHelpers.to10DigitStringInSpecificYear serializationYear /// /// Converts a SwedishPersonalIdentityNumber to its equivalent 10 digit string representation. The total length, including the separator, will be 11 chars. /// /// A SwedishPersonalIdentityNumber -let to10DigitString (pin : SwedishPersonalIdentityNumber) = - let year = - DateTime.UtcNow.Year - |> Year.create - |> function - | Ok y -> y - | Error _ -> invalidArg "year" "DateTime.Year wasn't a year" - to10DigitStringInSpecificYear year pin +let to10DigitString pin = + pin + |> toIdentityNumber + |> StringHelpers.to10DigitString /// /// Converts the value of the current object to its equivalent 12 digit string representation. -/// Format is YYYYMMDDBBBC, for example 19908072391 or 191202119986. +/// Format is YYYYMMDDBBBC, for example 199008072390 or 191202119986. /// /// A SwedishPersonalIdentityNumber let to12DigitString pin = - let vs = extractValues pin - sprintf "%02i%02i%02i%03i%1i" vs.Year vs.Month vs.Day vs.BirthNumber vs.Checksum - -let internal toParsingError err = - let invalidWithMsg msg i = - i |> sprintf "%s %i" msg |> Invalid |> ParsingError - match err with - | InvalidYear y -> - y |> invalidWithMsg "InvalidYear:" - | InvalidMonth m -> - m |> invalidWithMsg "Invalid month:" - | InvalidDay d | InvalidDayAndCoordinationDay d -> - d |> invalidWithMsg "Invalid day:" - | InvalidBirthNumber b -> - b |> invalidWithMsg "Invalid birthnumber:" - | InvalidChecksum c -> - c |> invalidWithMsg "Invalid checksum:" - | ParsingError err -> ParsingError err - | ArgumentNullError -> ArgumentNullError - | InvalidSerializationYear msg -> InvalidSerializationYear msg + pin + |> toIdentityNumber + |> StringHelpers.to12DigitString /// /// Converts the string representation of the Swedish personal identity number to its equivalent. @@ -113,18 +64,13 @@ let internal toParsingError err = /// For more info, see: https://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/folkbokforingslag-1991481_sfs-1991-481#P18 /// /// A string representation of the Swedish personal identity number to parse. -let parseInSpecificYear parseYear str = - match Parse.parse parseYear str with - | Ok pinValues -> create pinValues - | Error error -> Error error - |> Result.mapError toParsingError +let parseInSpecificYear parseYear str = Parse.parseInSpecificYear create parseYear str /// /// Converts the string representation of the personal identity number to its equivalent. /// /// A string representation of the Swedish personal identity number to parse. -let parse str = result { let! year = DateTime.UtcNow.Year |> Year.create - return! parseInSpecificYear year str } +let parse str = Parse.parse create str module Hints = open ActiveLogin.Identity.Swedish @@ -134,8 +80,7 @@ module Hints = /// Not always the actual date of birth due to the limited quantity of personal identity numbers per day. /// /// A SwedishPersonalIdentityNumber - let getDateOfBirthHint (pin : SwedishPersonalIdentityNumber) = - DateTime(pin.Year.Value, pin.Month.Value, pin.Day.Value, 0, 0, 0, DateTimeKind.Utc) + let getDateOfBirthHint pin = HintsHelper.getDateOfBirthHint (Personal pin) /// /// Get the age of the person according to the date in the personal identity number. @@ -143,23 +88,14 @@ module Hints = /// /// The date when to calculate the age. /// A SwedishPersonalIdentityNumber - let getAgeHintOnDate (date : DateTime) pin = - let dateOfBirth = getDateOfBirthHint pin - if date >= dateOfBirth then - let months = 12 * (date.Year - dateOfBirth.Year) + (date.Month - dateOfBirth.Month) - match date.Day < dateOfBirth.Day with - | true -> - let years = (months - 1) / 12 - years |> Some - | false -> months / 12 |> Some - else None + let getAgeHintOnDate date pin = HintsHelper.getAgeHintOnDate date (Personal pin) /// /// Get the age of the person according to the date in the personal identity number. /// Not always the actual date of birth due to the limited quantity of personal identity numbers per day. /// /// A SwedishPersonalIdentityNumber - let getAgeHint pin = getAgeHintOnDate DateTime.UtcNow pin + let getAgeHint pin = HintsHelper.getAgeHint (Personal pin) /// /// Gender (juridiskt kön) in Sweden according to the last digit of the birth number in the personal identity number. @@ -167,7 +103,4 @@ module Hints = /// Even number: Female /// /// A SwedishPersonalIdentityNumber - let getGenderHint (pin : SwedishPersonalIdentityNumber) = - let isBirthNumberEven = (pin.BirthNumber |> BirthNumber.value) % 2 = 0 - if isBirthNumberEven then Gender.Female - else Gender.Male + let getGenderHint pin = HintsHelper.getGenderHint (Personal pin) diff --git a/src/ActiveLogin.Identity.Swedish/SwedishPersonalIdentityNumberCSharp.fs b/src/ActiveLogin.Identity.Swedish/SwedishPersonalIdentityNumberCSharp.fs index e28c267..524d87f 100644 --- a/src/ActiveLogin.Identity.Swedish/SwedishPersonalIdentityNumberCSharp.fs +++ b/src/ActiveLogin.Identity.Swedish/SwedishPersonalIdentityNumberCSharp.fs @@ -11,8 +11,7 @@ open System.Runtime.InteropServices //for OutAttribute /// https://sv.wikipedia.org/wiki/Personnummer_i_Sverige /// [] -type SwedishPersonalIdentityNumberCSharp private(pin : SwedishPersonalIdentityNumber) = - let identityNumber = pin +type SwedishPersonalIdentityNumberCSharp internal(pin : SwedishPersonalIdentityNumber) = /// /// Creates an instance of out of the individual parts. @@ -26,41 +25,36 @@ type SwedishPersonalIdentityNumberCSharp private(pin : SwedishPersonalIdentityNu /// Thrown when any of the range arguments is invalid. /// Thrown when checksum is invalid. new(year, month, day, birthNumber, checksum) = - let pin = - create { Year = year - Month = month - Day = day - BirthNumber = birthNumber - Checksum = checksum } - |> Error.handle - SwedishPersonalIdentityNumberCSharp(pin) + let idNum = (year, month, day, birthNumber, checksum) |> create |> Error.handle - member internal __.IdentityNumber = identityNumber + SwedishPersonalIdentityNumberCSharp(idNum) + + member internal __.IdentityNumber = pin /// /// The year for date of birth. /// - member __.Year = identityNumber.Year |> Year.value + member __.Year = pin.Year.Value /// /// The month for date of birth. /// - member __.Month = identityNumber.Month |> Month.value + member __.Month = pin.Month.Value /// /// The day for date of birth. /// - member __.Day = identityNumber.Day |> Day.value + member __.Day = pin.Day.Value /// /// A birth number (födelsenummer) to distinguish people born on the same day. /// - member __.BirthNumber = identityNumber.BirthNumber |> BirthNumber.value + member __.BirthNumber = pin.BirthNumber.Value /// /// A checksum (kontrollsiffra) used for validation. Last digit in the PIN. /// - member __.Checksum = identityNumber.Checksum |> Checksum.value + member __.Checksum = pin.Checksum.Value /// /// Converts the string representation of the Swedish personal identity number to its equivalent. @@ -138,19 +132,19 @@ type SwedishPersonalIdentityNumberCSharp private(pin : SwedishPersonalIdentityNu member __.To10DigitStringInSpecificYear(serializationYear : int) = match serializationYear |> Year.create with | Error _ -> raise (ArgumentOutOfRangeException("year", serializationYear, "Invalid year.")) - | Ok year -> to10DigitStringInSpecificYear year identityNumber |> Error.handle + | Ok year -> to10DigitStringInSpecificYear year pin |> Error.handle /// /// Converts the value of the current object to its equivalent short string representation. /// Format is YYMMDDXBBBC, for example 990807-2391 or 120211+9986. /// - member __.To10DigitString() = to10DigitString identityNumber |> Error.handle + member __.To10DigitString() = to10DigitString pin |> Error.handle /// /// Converts the value of the current object to its equivalent 12 digit string representation. /// Format is YYYYMMDDBBBC, for example 19908072391 or 191202119986. /// - member __.To12DigitString() = to12DigitString identityNumber + member __.To12DigitString() = to12DigitString pin /// /// Converts the value of the current object to its equivalent 12 digit string representation. @@ -163,12 +157,12 @@ type SwedishPersonalIdentityNumberCSharp private(pin : SwedishPersonalIdentityNu /// true if value is an instance of and equals the value of this instance; otherwise, false. override __.Equals(b) = match b with - | :? SwedishPersonalIdentityNumberCSharp as pin -> identityNumber = pin.IdentityNumber + | :? SwedishPersonalIdentityNumberCSharp as p -> pin = p.IdentityNumber | _ -> false /// Returns the hash code for this instance. /// A 32-bit signed integer hash code. - override __.GetHashCode() = hash identityNumber + override __.GetHashCode() = hash pin static member op_Equality (left: SwedishPersonalIdentityNumberCSharp, right: SwedishPersonalIdentityNumberCSharp) = match box left, box right with diff --git a/src/ActiveLogin.Identity.Swedish/Types.fs b/src/ActiveLogin.Identity.Swedish/Types.fs index df9b433..3d3d3ad 100644 --- a/src/ActiveLogin.Identity.Swedish/Types.fs +++ b/src/ActiveLogin.Identity.Swedish/Types.fs @@ -11,7 +11,7 @@ type ParsingError = type Error = | InvalidYear of int | InvalidMonth of int - | InvalidDayAndCoordinationDay of int + | InvalidDayAndCoordinationDay of int //TODO remove this type, with CoordinationNumber support this should not be used | InvalidDay of int | InvalidBirthNumber of int | InvalidChecksum of int @@ -19,6 +19,26 @@ type Error = | ParsingError of ParsingError | InvalidSerializationYear of string +module ParsingError = + let internal toParsingError err = + let invalidWithMsg msg i = + i |> sprintf "%s %i" msg |> Invalid |> ParsingError + match err with + | InvalidYear y -> + y |> invalidWithMsg "InvalidYear:" + | InvalidMonth m -> + m |> invalidWithMsg "Invalid month:" + | InvalidDay d | InvalidDayAndCoordinationDay d -> + d |> invalidWithMsg "Invalid day:" + | InvalidBirthNumber b -> + b |> invalidWithMsg "Invalid birthnumber:" + | InvalidChecksum c -> + c |> invalidWithMsg "Invalid checksum:" + | ParsingError err -> ParsingError err + | ArgumentNullError -> ArgumentNullError + | InvalidSerializationYear msg -> InvalidSerializationYear msg + + module Error = /// This function will raise the most fitting Exceptions for the Error type provided. let handle result = @@ -121,6 +141,37 @@ module Day = type Day with member this.Value = Day.value this +type CoordinationDay = CoordinationDay of int + +module CoordinationDay = + let create (Year inYear) (Month inMonth) day = + let coordinationNumberDaysAdded = 60 + let daysInMonth = DateTime.DaysInMonth(inYear, inMonth) + + let isCoordinationDay d = + let dayWithoutCoordinationAddon = d - coordinationNumberDaysAdded + dayWithoutCoordinationAddon >= 1 && dayWithoutCoordinationAddon <= daysInMonth + + match isCoordinationDay day with + | true -> + day + |> CoordinationDay + |> Ok + | false -> + day + |> InvalidDayAndCoordinationDay + |> Error + + let value (CoordinationDay day) = day + +type CoordinationDay with + member this.Value = this |> CoordinationDay.value + member this.RealDay = this.Value - 60 + +type DayInternal = + | Day of Day + | CoordinationDay of CoordinationDay + type BirthNumber = private BirthNumber of int module BirthNumber = @@ -143,37 +194,57 @@ type BirthNumber with type Checksum = private Checksum of int module Checksum = - let create (Year year) (Month month) (Day day) (BirthNumber birth) checksum = - let isValidChecksum = - let getCheckSum digits = - let checksum = - digits - |> Seq.rev - |> Seq.mapi (fun (i : int) (d : int) -> - if i % 2 = 0 then d * 2 - else d) - |> Seq.rev - |> Seq.sumBy (fun (d : int) -> - if d > 9 then d - 9 - else d) - (checksum * 9) % 10 + let calculateChecksum digits = + digits + |> Seq.rev + |> Seq.mapi (fun (i : int) (d : int) -> + if i % 2 = 0 then d * 2 + else d) + |> Seq.rev + |> Seq.sumBy (fun (d : int) -> + if d > 9 then d - 9 + else d) + |> fun x -> (x * 9) % 10 + let createFromDigits digits expectedChecksum = + let checksum = calculateChecksum digits + if checksum = expectedChecksum then + expectedChecksum + |> Checksum + |> Ok + else + expectedChecksum + |> InvalidChecksum + |> Error + + let private create' (Year year) (Month month) day (BirthNumber birth) expectedChecksum = + let checksum = let twoDigitYear = year % 100 - let pNum = sprintf "%02i%02i%02i%03i" twoDigitYear month day birth - let digits = Seq.map (fun s -> Int32.Parse <| s.ToString()) pNum - let calculated = digits |> getCheckSum - calculated = checksum - if isValidChecksum then - checksum + let numberStr = sprintf "%02i%02i%02i%03i" twoDigitYear month day birth + let digits = numberStr |> Seq.map (fun s -> s.ToString() |> int) + calculateChecksum digits + + if checksum = expectedChecksum then + expectedChecksum |> Checksum |> Ok else - checksum + expectedChecksum |> InvalidChecksum |> Error + let create y m (day: DayInternal) b c = + let day = + match day with + | Day (day) -> day.Value + | CoordinationDay (day) -> day.Value + create' y m day b c + let value (Checksum sum) = sum +type Checksum with + member this.Value = this |> Checksum.value + /// Represents a Swedish Personal Identity Number (Svenskt Personnummer). /// https://en.wikipedia.org/wiki/Personal_identity_number_(Sweden) /// https://sv.wikipedia.org/wiki/Personnummer_i_Sverige @@ -194,9 +265,145 @@ type SwedishPersonalIdentityNumber = /// override this.ToString() = sprintf "%A" this -type SwedishPersonalIdentityNumberValues = - { Year : int - Month : int - Day : int - BirthNumber : int - Checksum : int } + +/// Represents a Swedish Coordination Identity Number (Samordningsnummer). +type SwedishCoordinationNumber = + { /// The year for date of birth. + Year : Year + /// The month for date of birth. + Month : Month + /// The day for date of birth + 60. + CoordinationDay : CoordinationDay + /// A birth number (födelsenummer) to distinguish people born on the same day. + BirthNumber : BirthNumber + /// A checksum (kontrollsiffra) used for validation. Last digit in the PIN. + Checksum : Checksum } + /// + /// Converts the value of the current object to its equivalent 12 digit string representation. + /// Format is YYYYMMDDBBBC, for example ??? or ???. + /// + override this.ToString() = sprintf "%A" this + member this.RealDay = this.CoordinationDay.RealDay + +type X = private X of int +module X = + let create x = + if x < 1 || x > 9999 then ParsingError.Invalid "Must be 0001-9999" |> ParsingError |> Error + else x |> X |> Ok + let value (X x) = x +type X with + member this.Value = this |> X.value + +type Y = private Y of int +module Y = + let create y = + if y < 20 || y > 99 then ParsingError.Invalid "Must be 20-99 " |> ParsingError |> Error + else y |> Y |> Ok + + let value (Y y) = y +type Y with + member this.Value = this |> Y.value + +type Z = private Z of int +module Z = + let create z = + if z < 1 || z > 99 then ParsingError.Invalid "Must be 01-99" |> ParsingError |> Error + else z |> Z |> Ok + + let value (Z z) = z +type Z with + member this.Value = this |> Z.value + +type Q = private Q of int +module Q = + let create q = + if q < 1 || q > 999 then ParsingError.Invalid "Must be 001 - 999" |> ParsingError |> Error + else q |> Q |> Ok + + let value (Q q) = q + +type Q with + member this.Value = this |> Q.value + +/// Represents a Swedish Company Registration Number (organisationsnummer). +type SwedishCompanyRegistrationNumber = + internal + { X : X + Y : Y + Z : Z + Q : Q + Checksum : Checksum } + /// + /// Converts the value of the current object to its equivalent 12 digit string representation. + /// Format is YYYYMMDDBBBC, for example ??? or ???. + /// + override this.ToString() = sprintf "%A" this + +/// Represents a Swedish Individual Identity Number. +type IndividualIdentityNumber = + | Personal of SwedishPersonalIdentityNumber + | Coordination of SwedishCoordinationNumber + + /// The year for date of birth. + member this.Year = + match this with + | Personal p -> p.Year + | Coordination c -> c.Year + + /// The month for date of birth. + member this.Month = + match this with + | Personal p -> p.Month + | Coordination c -> c.Month + + /// The day for date of birth. + member this.Day = + match this with + | Personal p -> p.Day |> Day + | Coordination c -> c.CoordinationDay |> CoordinationDay + + /// A birth number (födelsenummer) to distinguish people born on the same day. + member this.BirthNumber = + match this with + | Personal p -> p.BirthNumber + | Coordination c -> c.BirthNumber + + /// A checksum (kontrollsiffra) used for validation. Last digit in the PIN. + member this.Checksum = + match this with + | Personal p -> p.Checksum + | Coordination c -> c.Checksum + + /// Returns a value indicating whether this is a SwedishPersonalIdentityNumber. + member this.IsSwedishPersonalIdentityNumber = + match this with + | Personal _ -> true + | _ -> false + + /// Returns a value indicating whether this is a SwedishCoordinationNumber. + member this.IsSwedishCoordinationNumber = + match this with + | Coordination _ -> true + | _ -> false + +type CompanyIdentityNumber = + | Individual of IndividualIdentityNumber + | Company of SwedishCompanyRegistrationNumber + + /// Returns a value indicating whether this is a SwedishPersonalIdentityNumber. + member this.IsSwedishPersonalIdentityNumber = + match this with + | Individual (Personal _) -> true + | _ -> false + + /// Returns a value indicating whether this is a SwedishCoordinationNumber. + member this.IsSwedishCoordinationNumber = + match this with + | Individual (Coordination _) -> true + | _ -> false + + /// Returns a value indicating whether this is a SwedishCompanyRegistrationNumber. + member this.IsSwedishCompanyRegistrationNumber = + match this with + | Company _ -> true + | _ -> false diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/ActiveLogin.Identity.Swedish.FSharp.Test.fsproj b/test/ActiveLogin.Identity.Swedish.FSharp.Test/ActiveLogin.Identity.Swedish.FSharp.Test.fsproj index ffc1bb7..132b20b 100644 --- a/test/ActiveLogin.Identity.Swedish.FSharp.Test/ActiveLogin.Identity.Swedish.FSharp.Test.fsproj +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/ActiveLogin.Identity.Swedish.FSharp.Test.fsproj @@ -14,10 +14,15 @@ + + + + + diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/Generators.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/Generators.fs index 3e1878c..cf8b3ef 100644 --- a/test/ActiveLogin.Identity.Swedish.FSharp.Test/Generators.fs +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/Generators.fs @@ -12,39 +12,53 @@ let private chooseFromArray xs = type EmptyString = EmptyString of string type Digits = Digits of string type Max200 = Max200 of int -type Valid12Digit = Valid12Digit of string -type ValidValues = ValidValues of SwedishPersonalIdentityNumberValues type InvalidYear = InvalidYear of int type InvalidMonth = InvalidMonth of int type ValidYear = ValidYear of int -type InvalidPinString = InvalidPinString of string -type ValidPin = ValidPin of SwedishPersonalIdentityNumber type ValidMonth = ValidMonth of int -type WithInvalidDay = WithInvalidDay of SwedishPersonalIdentityNumberValues -type WithValidDay = WithValidDay of SwedishPersonalIdentityNumberValues type InvalidBirthNumber = InvalidBirthNumber of int type ValidBirthNumber = ValidBirthNumber of int -type TwoEqualPins = TwoEqualPins of SwedishPersonalIdentityNumber * SwedishPersonalIdentityNumber -type TwoPins = TwoPins of SwedishPersonalIdentityNumber * SwedishPersonalIdentityNumber -type Char100 = Char100 of char[] -type Age = Age of Years : int * Months : int * Days: double -type LeapDayPin = LeapDayPin of SwedishPersonalIdentityNumber - - -let stringToValues (pin : string) = - { Year = pin.[0..3] |> int - Month = pin.[4..5] |> int - Day = pin.[6..7] |> int - BirthNumber = pin.[8..10] |> int - Checksum = pin.[11..11] |> int } +type Char100 = Char100 of char [] +type Age = Age of Years: int * Months: int * Days: double +type IdentityNumberValues = (int * int * int * int * int) + +module Pin = + type Valid12Digit = Valid12Digit of string + type ValidValues = ValidValues of IdentityNumberValues + type InvalidPinString = InvalidPinString of string + type ValidPin = ValidPin of SwedishPersonalIdentityNumber + type WithInvalidDay = WithInvalidDay of IdentityNumberValues + type WithValidDay = WithValidDay of IdentityNumberValues + type TwoEqualPins = TwoEqualPins of SwedishPersonalIdentityNumber * SwedishPersonalIdentityNumber + type TwoPins = TwoPins of SwedishPersonalIdentityNumber * SwedishPersonalIdentityNumber + type LeapDayPin = LeapDayPin of SwedishPersonalIdentityNumber + +module CoordNum = + type ValidNum = ValidNum of SwedishCoordinationNumber + type Valid12Digit = Valid12Digit of string + type InvalidNumString = InvalidNumString of string + type TwoEqualCoordNums = TwoEqualCoordNums of SwedishCoordinationNumber * SwedishCoordinationNumber + type ValidValues = ValidValues of IdentityNumberValues + type TwoCoordNums = TwoCoordNums of SwedishCoordinationNumber * SwedishCoordinationNumber + type WithInvalidDay = WithInvalidDay of IdentityNumberValues + type WithValidDay = WithValidDay of IdentityNumberValues + type LeapDayCoordNum = LeapDayCoordNum of SwedishCoordinationNumber + + +let stringToValues (pin: string) = + ( pin.[0..3] |> int, + pin.[4..5] |> int, + pin.[6..7] |> int, + pin.[8..10] |> int, + pin.[11..11] |> int ) module Generators = - let max200Gen() = Gen.choose(-100, 200) |> Gen.map Max200 |> Arb.fromGen + let max200Gen() = Gen.choose (-100, 200) |> Gen.map Max200 |> Arb.fromGen let emptyStringGen() = let emptyStringWithLength length = String.replicate length " " - Gen.sized(fun s -> Gen.choose(0,s) |> Gen.map emptyStringWithLength) + Gen.sized (fun s -> Gen.choose (0, s) |> Gen.map emptyStringWithLength) |> Gen.map EmptyString |> Arb.fromGen @@ -52,18 +66,12 @@ module Generators = let createDigits (strs: string list) = System.String.Join("", strs) |> Digits - Gen.choose(0,9) + Gen.choose (0, 9) |> Gen.map string |> Gen.listOf |> Gen.map createDigits |> Arb.fromGen - let valid12Digit = chooseFromArray SwedishPersonalIdentityNumberTestData.raw12DigitStrings - let valid12DigitGen() = valid12Digit |> Gen.map Valid12Digit |> Arb.fromGen - - let validValues = valid12Digit |> Gen.map (stringToValues >> ValidValues) - let validValuesGen() = validValues |> Arb.fromGen - let outsideRange min max = let low = Gen.choose (Int32.MinValue, min - 1) let high = Gen.choose (max + 1, Int32.MaxValue) @@ -90,7 +98,7 @@ module Generators = let private validMonth = - (1,12) + (1, 12) |> Gen.choose let validMonthGen() = @@ -98,31 +106,6 @@ module Generators = |> Gen.map ValidMonth |> Arb.fromGen - let withInvalidDay = - gen { - let! (ValidValues validValues) = validValues - let daysInMonth = DateTime.DaysInMonth(validValues.Year, validValues.Month) - let! invalidDay = outsideRange 1 daysInMonth - return { validValues with Day = invalidDay } |> WithInvalidDay - } - - let withInvalidDayGen() = withInvalidDay |> Arb.fromGen - - - let private validDay year month = - let daysInMonth = DateTime.DaysInMonth(year, month) - Gen.choose (1, daysInMonth) - - let withValidDay = - gen { - let! (ValidValues validValues) = validValues - let! validDay = validDay validValues.Year validValues.Month - return { validValues with Day = validDay } |> WithValidDay - } - - let withValidDayGen() = withValidDay |> Arb.fromGen - - let invalidBirthNumberGen() = (1, 999) ||> outsideRange @@ -136,67 +119,6 @@ module Generators = |> Gen.map ValidBirthNumber |> Arb.fromGen - - let twoEqualPinsGen() = - gen { - let! (ValidValues values) = validValues - let pin1 = - values - |> SwedishPersonalIdentityNumber.createOrFail - - let pin2 = - values - |> SwedishPersonalIdentityNumber.createOrFail - - return (pin1, pin2) |> TwoEqualPins - } - |> Arb.fromGen - - - let twoPinsGen() = - gen { - let pin1 = SwedishPersonalIdentityNumberTestData.getRandom() - let pin2 = SwedishPersonalIdentityNumberTestData.getRandom() - return (pin1, pin2) |> TwoPins - } - |> Arb.fromGen - - - let validPinGen() = - gen { return SwedishPersonalIdentityNumberTestData.getRandom() |> ValidPin } - |> Arb.fromGen - - let invalidPinStringGen() = - gen { - let! valid = valid12Digit - let withInvalidYear = - gen { - return "0000" + valid.[ 4.. ] - } - let withInvalidMonth = - gen { - let! month = Gen.choose(13,99) |> Gen.map string - return valid.[ 0..3 ] + month + valid.[ 6.. ] - } - let withInvalidDay = - gen { - let year = valid.[ 0..3 ] |> int - let month = valid.[ 4..5 ] |> int - let daysInMonth = DateTime.DaysInMonth(year, month) - let! day = Gen.choose(daysInMonth + 1, 99) |> Gen.map string - return valid.[ 0..5 ] + day + valid.[ 8.. ] - } - let withInvalidBirthNumber = - gen { - return valid.[ 0..7 ] + "000" + valid.[ 11.. ] - } - let withInvalidChecksum = - let checksum = valid.[ 11.. ] - let invalid = checksum |> int |> fun i -> (i + 1) % 10 |> string - gen { return valid.[ 0..10 ] + invalid } - return! Gen.oneof [ withInvalidYear; withInvalidMonth; withInvalidDay; withInvalidBirthNumber; withInvalidChecksum ] - } |> Gen.map InvalidPinString |> Arb.fromGen - let char100() = Gen.arrayOfLength 100 Arb.generate |> Gen.map Char100 @@ -204,48 +126,258 @@ module Generators = let age() = gen { - let! years = Gen.choose(0, 199) - let! months = Gen.choose(0, 11) - let! days = Gen.choose(0,27) |> Gen.map float + let! years = Gen.choose (0, 199) + let! months = Gen.choose (0, 11) + let! days = Gen.choose (0, 27) |> Gen.map float - return Age (Years = years, Months = months,Days = days) + return Age(Years = years, Months = months, Days = days) } |> Arb.fromGen - let private leapDayPins = - let isLeapDay (pin: SwedishPersonalIdentityNumber) = - pin.Month.Value = 2 && pin.Day.Value = 29 - - SwedishPersonalIdentityNumberTestData.allPinsShuffled() - |> Seq.filter isLeapDay - |> Seq.toArray - - let leapDayPinGen() = - leapDayPins - |> chooseFromArray - |> Gen.map LeapDayPin - |> Arb.fromGen - + module Pin = + let valid12Digit = chooseFromArray SwedishPersonalIdentityNumberTestData.raw12DigitStrings + let valid12DigitGen() = valid12Digit |> Gen.map Pin.Valid12Digit |> Arb.fromGen + let validDay year month = + let daysInMonth = DateTime.DaysInMonth(year, month) + Gen.choose (1, daysInMonth) + + let validValues = valid12Digit |> Gen.map (stringToValues >> Pin.ValidValues) + let validValuesGen() = validValues |> Arb.fromGen + + let withInvalidDay = + gen { + let! (Pin.ValidValues (year, month, day, birthNumber, checksum)) = validValues + let daysInMonth = DateTime.DaysInMonth(year, month) + let! invalidDay = outsideRange 1 daysInMonth + return (year, month, invalidDay, birthNumber, checksum) |> Pin.WithInvalidDay + } + + let withInvalidDayGen() = withInvalidDay |> Arb.fromGen + + let withValidDay = + gen { + let! (Pin.ValidValues (year, month, day, birthNumber, checksum)) = validValues + let! validDay = validDay year month + return (year, month, validDay, birthNumber, checksum) |> Pin.WithValidDay + } + + let withValidDayGen() = withValidDay |> Arb.fromGen + + let twoEqualPinsGen() = + gen { + let! (Pin.ValidValues values) = validValues + let pin1 = + values + |> SwedishPersonalIdentityNumber.createOrFail + + let pin2 = + values + |> SwedishPersonalIdentityNumber.createOrFail + + return (pin1, pin2) |> Pin.TwoEqualPins + } + |> Arb.fromGen + + + let twoPinsGen() = + gen { + let pin1 = SwedishPersonalIdentityNumberTestData.getRandom() + let pin2 = SwedishPersonalIdentityNumberTestData.getRandom() + return (pin1, pin2) |> Pin.TwoPins + } + |> Arb.fromGen + + + let validPinGen() = + gen { return SwedishPersonalIdentityNumberTestData.getRandom() |> Pin.ValidPin } + |> Arb.fromGen + + let invalidPinStringGen() = + gen { + let! valid = valid12Digit + let withInvalidYear = + gen { + return "0000" + valid.[4..] + } + let withInvalidMonth = + gen { + let! month = Gen.choose (13, 99) |> Gen.map string + return valid.[0..3] + month + valid.[6..] + } + let withInvalidDay = + gen { + let year = valid.[0..3] |> int + let month = valid.[4..5] |> int + let daysInMonth = DateTime.DaysInMonth(year, month) + let! day = Gen.choose (daysInMonth + 1, 99) |> Gen.map string + return valid.[0..5] + day + valid.[8..] + } + let withInvalidBirthNumber = + gen { + return valid.[0..7] + "000" + valid.[11..] + } + let withInvalidChecksum = + let checksum = valid.[11..] + let invalid = checksum |> int |> fun i -> (i + 1) % 10 |> string + gen { return valid.[0..10] + invalid } + return! Gen.oneof [ withInvalidYear; withInvalidMonth; withInvalidDay; withInvalidBirthNumber; withInvalidChecksum ] + } |> Gen.map Pin.InvalidPinString |> Arb.fromGen + + let leapDayPins = + let isLeapDay (pin: SwedishPersonalIdentityNumber) = + pin.Month.Value = 2 && pin.Day.Value = 29 + + SwedishPersonalIdentityNumberTestData.allPinsShuffled() + |> Seq.filter isLeapDay + |> Seq.toArray + + let leapDayPinGen() = + leapDayPins + |> chooseFromArray + |> Gen.map Pin.LeapDayPin + |> Arb.fromGen + + module CoordNum = + + let validCoordNumGen() = + gen { return SwedishCoordinationNumberTestData.getRandom() |> CoordNum.ValidNum } + |> Arb.fromGen + + let valid12Digit = chooseFromArray SwedishCoordinationNumberTestData.raw12DigitStrings + let valid12DigitGen() = valid12Digit |> Gen.map CoordNum.Valid12Digit |> Arb.fromGen + + let validValues = valid12Digit |> Gen.map (stringToValues >> CoordNum.ValidValues) + let validValuesGen() = validValues |> Arb.fromGen + + let invalidCoordinationDay daysInMonth = + gen { + let tooLow = Gen.choose(0,60) + let tooHigh = Gen.choose(daysInMonth + 61, 99) + return! Gen.oneof [ tooLow; tooHigh ] + } + + let invalidNumStringGen() = + gen { + let! valid12Digit = valid12Digit + + let withInvalidYear = + gen { + return "0000" + valid12Digit.[4..] + } + let withInvalidMonth = + gen { + let! month = Gen.choose (13, 99) |> Gen.map string + return valid12Digit.[0..3] + month + valid12Digit.[6..] + } + let withInvalidDay = + gen { + let year = valid12Digit.[0..3] |> int + let month = valid12Digit.[4..5] |> int + let daysInMonth = DateTime.DaysInMonth(year, month) + let day = invalidCoordinationDay daysInMonth + let! dayStr = day |> Gen.map (fun num -> num.ToString("00")) + return valid12Digit.[0..5] + dayStr + valid12Digit.[8..] + } + let withInvalidBirthNumber = + gen { + return valid12Digit.[0..7] + "000" + valid12Digit.[11..] + } + let withInvalidChecksum = + let checksum = valid12Digit.[11..] + let invalid = checksum |> int |> fun i -> (i + 1) % 10 |> string + gen { return valid12Digit.[0..10] + invalid } + return! Gen.oneof [ withInvalidYear; withInvalidMonth; withInvalidDay; withInvalidBirthNumber; withInvalidChecksum ] + } |> Gen.map CoordNum.InvalidNumString |> Arb.fromGen + + let twoEqualCoordNumsGen() = + gen { + let! (CoordNum.ValidValues values) = validValues + let num1 = + values + |> SwedishCoordinationNumber.createOrFail + + let num2 = + values + |> SwedishCoordinationNumber.createOrFail + + return (num1, num2) |> CoordNum.TwoEqualCoordNums + } + |> Arb.fromGen + + let twoCoordNumsGen() = + gen { + let coordNum1 = SwedishCoordinationNumberTestData.getRandom() + let coordNum2 = SwedishCoordinationNumberTestData.getRandom() + return (coordNum1, coordNum2) |> CoordNum.TwoCoordNums + } + |> Arb.fromGen + + + let withInvalidDay = + gen { + let! (CoordNum.ValidValues (year, month, day, birthNumber, checksum)) = validValues + let daysInMonth = DateTime.DaysInMonth(year, month) + let! invalidDay = invalidCoordinationDay daysInMonth + return (year, month, invalidDay, birthNumber, checksum) |> CoordNum.WithInvalidDay + } + + let withInvalidDayGen() = withInvalidDay |> Arb.fromGen + + let validCoordinationDay year month = + Pin.validDay year month |> Gen.map (fun d -> d + 60) + + let withValidCoordinationDay = + gen { + let! (CoordNum.ValidValues (year, month, day, birthNumber, checksum)) = validValues + let! validDay = validCoordinationDay year month + return (year, month, validDay, birthNumber, checksum) |> CoordNum.WithValidDay + } + + let withValidDayGen() = withValidCoordinationDay |> Arb.fromGen + + let leapDayCoordNums = + let isLeapDay (num: SwedishCoordinationNumber) = + num.Month.Value = 2 && num.RealDay = 29 + + SwedishCoordinationNumberTestData.allCoordNumsShuffled() + |> Seq.filter isLeapDay + |> Seq.toArray + + let leapDayCoordNumGen() = + if leapDayCoordNums.Length < 1 then failwith "The test data does not contain any coordination numbers with leap days" + leapDayCoordNums + |> chooseFromArray + |> Gen.map CoordNum.LeapDayCoordNum + |> Arb.fromGen open Generators -type PinGenerators() = +type ValueGenerators() = static member EmptyString() = emptyStringGen() static member Digits() = digitsGen() static member Max200() = max200Gen() - static member Valid12Digit() = valid12DigitGen() - static member ValidValues() = validValuesGen() + static member Valid12DigitPin() = Pin.valid12DigitGen() + static member ValidPinValues() = Pin.validValuesGen() static member InvalidYear() = invalidYearGen() static member InvalidMonth() = invalidMonthGen() static member ValidYear() = validYearGen() - static member InvalidPinString() = invalidPinStringGen() - static member ValidPin() = validPinGen() + static member InvalidPinString() = Pin.invalidPinStringGen() + static member ValidPin() = Pin.validPinGen() static member ValidMonth() = validMonthGen() - static member WithInvalidDay() = withInvalidDayGen() - static member WithValidDay() = withValidDayGen() - static member InvalidBirthNumber() = invalidBirthNumberGen() + static member PinWithInvalidDay() = Pin.withInvalidDayGen() + static member PinWithValidDay() = Pin.withValidDayGen() + static member PinInvalidBirthNumber() = invalidBirthNumberGen() static member ValidBirthNumber() = validBirthNumberGen() - static member TwoEqualPins() = twoEqualPinsGen() - static member TwoPins() = twoPinsGen() + static member TwoEqualPins() = Pin.twoEqualPinsGen() + static member TwoPins() = Pin.twoPinsGen() static member Char100() = char100() static member Age() = age() - static member LeapDayPin() = leapDayPinGen() + static member LeapDayPin() = Pin.leapDayPinGen() + static member ValidCoordNum() = CoordNum.validCoordNumGen() + static member InvalidCoordNumString() = CoordNum.invalidNumStringGen() + static member TwoEqualCoordNums() = CoordNum.twoEqualCoordNumsGen() + static member TwoCoordNums() = CoordNum.twoCoordNumsGen() + static member Valid12DigitCoordNum() = CoordNum.valid12DigitGen() + static member ValidCoordNumValues() = CoordNum.validValuesGen() + static member CoordNumWithInvalidDay() = CoordNum.withInvalidDayGen() + static member CoordNumWithValidDay() = CoordNum.withValidDayGen() + static member LeapDayCoordNum() = CoordNum.leapDayCoordNumGen() diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/HintsHelper.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/HintsHelper.fs new file mode 100644 index 0000000..4f6054d --- /dev/null +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/HintsHelper.fs @@ -0,0 +1,2 @@ +module ActiveLogin.Identity.Swedish.FSharp.Test.HintsHelper + diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/PinTestHelpers.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/PinTestHelpers.fs index 7c28817..b9228e3 100644 --- a/test/ActiveLogin.Identity.Swedish.FSharp.Test/PinTestHelpers.fs +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/PinTestHelpers.fs @@ -6,7 +6,7 @@ open Expecto open System open System.Threading -let private arbTypes = [ typeof ] +let private arbTypes = [ typeof ] let private config = { FsCheckConfig.defaultConfig with arbitrary = arbTypes @ FsCheckConfig.defaultConfig.arbitrary } let testProp name = testPropertyWithConfig config name @@ -18,11 +18,11 @@ let tee f x = f x |> ignore; x let quickParseR (str:string) = let values = - { Year = str.[ 0..3 ] |> int - Month = str.[ 4..5 ] |> int - Day = str.[ 6..7 ] |> int - BirthNumber = str.[ 8..10 ] |> int - Checksum = str.[ 11..11 ] |> int } + ( str.[ 0..3 ] |> int, + str.[ 4..5 ] |> int, + str.[ 6..7 ] |> int, + str.[ 8..10 ] |> int, + str.[ 11..11 ] |> int ) SwedishPersonalIdentityNumber.create values let quickParse str = @@ -31,11 +31,11 @@ let quickParse str = | Error e -> e.ToString() |> failwithf "Test setup error %s" let pinToValues (pin:SwedishPersonalIdentityNumber) = - { Year = pin.Year |> Year.value - Month = pin.Month |> Month.value - Day = pin.Day |> Day.value - BirthNumber = pin.BirthNumber |> BirthNumber.value - Checksum = pin.Checksum |> Checksum.value } + ( pin.Year |> Year.value, + pin.Month |> Month.value, + pin.Day |> Day.value, + pin.BirthNumber |> BirthNumber.value, + pin.Checksum |> Checksum.value ) type Rng = @@ -50,3 +50,24 @@ let rng = Random())) { Next = fun (min, max) -> localGenerator.Value.Next(min, max) NextDouble = localGenerator.Value.NextDouble } + + +let getRandomFromArray arr = + fun () -> + let index = rng.Next(0, Array.length arr - 1) + arr.[index] + +let removeHyphen (str:string) = + let isHyphen (c:char) = "-".Contains(c) + String.filter (isHyphen >> not) str + +let surroundEachChar (chars:char[]) (pin:string) = + let rnd = getRandomFromArray chars + let surroundWith c = [| rnd(); c; rnd() |] + + Seq.collect surroundWith pin + |> Array.ofSeq + |> System.String + + +let isDigit (c:char) = "0123456789".Contains(c) diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_create.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_create.fs new file mode 100644 index 0000000..048a6b0 --- /dev/null +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_create.fs @@ -0,0 +1,85 @@ +/// +/// Tested with offical test Personal Identity Numbers from Skatteverket: +/// https://skatteverket.entryscape.net/catalog/9/datasets/147 +/// +module ActiveLogin.Identity.Swedish.FSharp.Test.SwedishCoordinationNumber_create + +open Swensen.Unquote +open Expecto +open FsCheck +open ActiveLogin.Identity.Swedish.FSharp +open ActiveLogin.Identity.Swedish.FSharp.TestData +open System.Reflection + +[] +let tests = + testList "SwedishCoordinationNumber.create" [ + testProp "roundtrip 12DigitString -> create -> to12DigitString" <| fun (Gen.CoordNum.Valid12Digit str) -> + str + |> Gen.stringToValues + |> SwedishCoordinationNumber.create + |> Result.map SwedishCoordinationNumber.to12DigitString =! Ok str + + testPropWithMaxTest 20000 "invalid year returns InvalidYear Error" <| + fun (Gen.CoordNum.ValidValues (y, m, d, b, c), Gen.InvalidYear invalidYear) -> + let result = SwedishCoordinationNumber.create (invalidYear, m, d, b, c) + result =! Error(InvalidYear invalidYear) + + testPropWithMaxTest 20000 "valid year does not return InvalidYear Error" <| + fun (Gen.CoordNum.ValidValues (y, m, d, b, c), Gen.ValidYear validYear) -> + let result = SwedishCoordinationNumber.create (validYear, m, d, b, c) + result <>! (Error(InvalidYear validYear)) + + testProp "invalid month returns InvalidMonth Error" <| + fun (Gen.CoordNum.ValidValues (y, m, d, b, c), Gen.InvalidMonth invalidMonth) -> + let result = SwedishCoordinationNumber.create (y, invalidMonth, d, b, c) + result =! Error(InvalidMonth invalidMonth) + + testProp "valid month does not return InvalidMonth Error" <| + fun (Gen.CoordNum.ValidValues (y, m, d, b, c), Gen.ValidMonth validMonth) -> + let result = SwedishCoordinationNumber.create (y, validMonth, d, b, c) + result <>! (Error(InvalidMonth validMonth)) + + testProp "invalid day returns InvalidDay Error" <| fun (Gen.CoordNum.WithInvalidDay (y, m, d, b, c)) -> + let result = SwedishCoordinationNumber.create (y, m, d, b, c) + result =! Error(InvalidDayAndCoordinationDay d) + + testProp "valid day does not return InvalidDay Error" <| fun (Gen.CoordNum.WithValidDay (y, m, d, b, c)) -> + let result = SwedishCoordinationNumber.create (y, m, d, b, c) + result <>! Error(InvalidDayAndCoordinationDay d) + result <>! Error(InvalidDay d) + + testProp "invalid birth number returns InvalidBirthNumber Error" <| + fun (Gen.CoordNum.ValidValues (y, m, d, b, c), Gen.InvalidBirthNumber invalidBirthNumber) -> + let result = SwedishCoordinationNumber.create (y, m, d, invalidBirthNumber, c) + result =! Error(InvalidBirthNumber invalidBirthNumber) + + testPropWithMaxTest 3000 "valid birth number does not return InvalidBirthNumber Error" <| + fun (Gen.CoordNum.ValidValues (y, m, d, b, c), Gen.ValidBirthNumber validBirthNumber) -> + let result = SwedishCoordinationNumber.create (y, m, d, validBirthNumber, c ) + result <>! Error(InvalidBirthNumber validBirthNumber) + + testProp "invalid checksum returns InvalidChecksum Error" <| + fun (Gen.CoordNum.ValidValues (y, m, d, b, c)) -> + let invalidChecksums = + [ 0..9 ] + |> List.except [ c ] + + let withInvalidChecksums = + invalidChecksums + |> List.map (fun checksum -> (y, m, d, b, checksum)) + + let invalidChecksumsAndResults = + withInvalidChecksums + |> List.map (fun (y, m, d, b, c) -> c, SwedishCoordinationNumber.create (y, m, d, b, c)) + + invalidChecksumsAndResults + |> List.iter (fun (invalidChecksum, result) -> + match result with + | Error (InvalidChecksum actual) -> invalidChecksum =! actual + | _ -> failwith "Expected InvalidChecksum Error") + + testCase "fsharp should have no public constructor" <| fun () -> + let typ = typeof + let numConstructors = typ.GetConstructors(BindingFlags.Public) |> Array.length + numConstructors =! 0 ] diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_equality.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_equality.fs new file mode 100644 index 0000000..32c8401 --- /dev/null +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_equality.fs @@ -0,0 +1,43 @@ +/// +/// Tested with offical test Personal Identity Numbers from Skatteverket: +/// https://skatteverket.entryscape.net/catalog/9/datasets/147 +/// +module ActiveLogin.Identity.Swedish.FSharp.Test.SwedishCoordinationNumber_equality + +open Swensen.Unquote +open Expecto +open FsCheck + + +[] +let tests = testList "SwedishCoordinationNumber.equality" [ + testProp "identical numbers are equal when using operator" <| + fun (Gen.CoordNum.TwoEqualCoordNums (num1, num2)) -> + num1 = num2 =! true + num2 = num1 =! true + testProp "identical numbers are equal when using .Equals()" <| + fun (Gen.CoordNum.TwoEqualCoordNums (num1, num2)) -> + num1.Equals(num2) =! true + num2.Equals(num1) =! true + testProp "identical numbers are equal when using .Equals() and one number is object" <| + fun (Gen.CoordNum.TwoEqualCoordNums (num1, num2)) -> + let num2 = num2 :> obj + num1.Equals(num2) =! true + num2.Equals(num1) =! true + testProp "different numbers are not equal" <| + fun (Gen.CoordNum.TwoCoordNums (num1, num2)) -> + num1 <> num2 ==> lazy + num1 <> num2 =! true + num2 <> num1 =! true + testProp "different numbers are not equal using .Equals()" <| + fun (Gen.CoordNum.TwoCoordNums (num1, num2)) -> + num1 <> num2 ==> lazy + num1.Equals(num2) =! false + num2.Equals(num1) =! false + testProp "a num is not equal to null using .Equals()" <| + fun (Gen.CoordNum.ValidNum num) -> + num.Equals(null) =! false + testProp "a num is not equal to object null using .Equals()" <| + fun (Gen.CoordNum.ValidNum num) -> + let nullObject = null :> obj + num.Equals(nullObject) =! false ] diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_hash.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_hash.fs new file mode 100644 index 0000000..71b6985 --- /dev/null +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_hash.fs @@ -0,0 +1,13 @@ +/// +/// Tested with offical test Personal Identity Numbers from Skatteverket: +/// https://skatteverket.entryscape.net/catalog/9/datasets/147 +/// +module ActiveLogin.Identity.Swedish.FSharp.Test.SwedishCoordinationNumber_hash +open Expecto +open Swensen.Unquote + +[] +let tests = testList "SwedishCoordinationNumber.hash" [ + testProp "identical numbers have the same hash code" <| + fun (Gen.CoordNum.TwoEqualCoordNums (num1, num2)) -> + hash num1 =! hash num2 ] diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_hints.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_hints.fs new file mode 100644 index 0000000..9d5f3e4 --- /dev/null +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_hints.fs @@ -0,0 +1,84 @@ +/// +/// Tested with offical test Personal Identity Numbers from Skatteverket: +/// https://skatteverket.entryscape.net/catalog/9/datasets/147 +/// +module ActiveLogin.Identity.Swedish.FSharp.Test.SwedishCoordinationNumber_hints + +open Swensen.Unquote +open Expecto +open Expecto.Flip +open FsCheck +open ActiveLogin.Identity.Swedish.FSharp.SwedishCoordinationNumber +open System +open ActiveLogin.Identity.Swedish.FSharp +open ActiveLogin.Identity.Swedish.FSharp.Test.PinTestHelpers +open ActiveLogin.Identity.Swedish + + +let getDateOfBirth (num: SwedishCoordinationNumber) = + {| Date = DateTime(num.Year.Value, num.Month.Value, num.RealDay) + IsLeapDay = num.Month.Value = 2 && num.RealDay = 29 |} + +let (|Even|Odd|) (num:BirthNumber) = + match num.Value % 2 with + | 0 -> Even + | _ -> Odd + +[] +let tests = + testList "SwedishCoordinationNumber.hints" [ + testList "getAgeHint" [ + testProp "a person ages by years counting from their date of birth" + <| fun (Gen.CoordNum.ValidNum num, Gen.Age (years, months, days)) -> + not (num.Month.Value = 2 && num.RealDay = 29) ==> + lazy + let dateOfBirth = getDateOfBirth num + let checkDate = + dateOfBirth.Date + .AddYears(years) + .AddMonths(months) + .AddDays(days) + + Hints.getAgeHintOnDate checkDate num =! Some years + +// Right now we do not have leap days in the test data and cannot run this test +// testProp "a person born on a leap day also ages by years counting from their date of birth" +// <| fun (Gen.CoordNum.LeapDayCoordNum num, Gen.Age (years, months, days)) -> +// let dateOfBirth = getDateOfBirth num +// // Since there isn't a leap day every year we need to add 1 day to the checkdate +// let checkDate = +// dateOfBirth.Date +// .AddYears(years) +// .AddMonths(months) +// .AddDays(days + 1.) +// +// Hints.getAgeHintOnDate checkDate num =! Some years + + testProp "cannot get age for date before person was born" <| fun (Gen.CoordNum.ValidNum num) -> + let dateOfBirth = getDateOfBirth num + let checkOffset = rng.NextDouble() * 199. * 365. + let checkDate = dateOfBirth.Date.AddDays -checkOffset + let result = Hints.getAgeHintOnDate checkDate num + result |> Expect.isNone "age should be None" + + testProp "getAgeHint uses DateTime.UtcNow as checkYear" <| fun (Gen.CoordNum.ValidNum num) -> + let age1 = Hints.getAgeHintOnDate DateTime.UtcNow num + let age2 = Hints.getAgeHint num + age1 =! age2 ] + + testList "getDateOfBirthHint" [ + testProp "get date of birth hint extracts year, month and date from number" <| fun (Gen.CoordNum.ValidNum num) -> + let result = Hints.getDateOfBirthHint num + + result.Year =! num.Year.Value + result.Month =! num.Month.Value + result.Day =! num.RealDay + ] + + testList "getGenderHint" [ + testProp "even birthnumber indicates a female, odd birthnumber a male" <| fun (Gen.CoordNum.ValidNum num) -> + match num.BirthNumber with + | Even -> Hints.getGenderHint num =! Gender.Female + | Odd -> Hints.getGenderHint num =! Gender.Male + ] + ] diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_parse.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_parse.fs new file mode 100644 index 0000000..e984537 --- /dev/null +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishCoordinationNumber_parse.fs @@ -0,0 +1,145 @@ +module ActiveLogin.Identity.Swedish.FSharp.Test.SwedishCoordinationNumber_parse + +open Swensen.Unquote +open Expecto +open ActiveLogin.Identity.Swedish.FSharp +open FsCheck + +let private isInvalidNumberOfDigits (str: string) = + if System.String.IsNullOrWhiteSpace str then false + else + str + |> String.filter isDigit + |> (fun s -> s.Length <> 10 && s.Length <> 12) + +let private validNumberTests = testList "valid coordination numbers" [ + testProp "roundtrip for 12 digit string" <| fun (Gen.CoordNum.ValidNum num) -> + num + |> SwedishCoordinationNumber.to12DigitString + |> SwedishCoordinationNumber.parse =! Ok num + + testProp "roundtrip for 10 digit string with delimiter" <| fun (Gen.CoordNum.ValidNum num) -> + num + |> SwedishCoordinationNumber.to10DigitString + |> Result.bind SwedishCoordinationNumber.parse =! Ok num + + testProp "roundtrip for 10 digit string without hyphen-delimiter" <| fun (Gen.CoordNum.ValidNum num) -> + num + |> SwedishCoordinationNumber.to10DigitString + |> Result.map removeHyphen + |> Result.bind SwedishCoordinationNumber.parse =! Ok num + + testProp "roundtrip for 12 digit string mixed with 'non-digits'" + <| fun (Gen.CoordNum.ValidNum num, Gen.Char100 charArray) -> + let charsWithoutDigits = + charArray + |> Array.filter (isDigit >> not) + + num + |> SwedishCoordinationNumber.to12DigitString + |> surroundEachChar charsWithoutDigits + |> SwedishCoordinationNumber.parse =! Ok num + + testProp "roundtrip for 10 digit string mixed with 'non-digits' except plus" + <| fun (Gen.CoordNum.ValidNum num, Gen.Char100 charArray) -> + let charsWithoutPlus = + let isDigitOrPlus (c:char) = "0123456789+".Contains c + + charArray + |> Array.filter (isDigitOrPlus >> not) + + num + |> SwedishCoordinationNumber.to10DigitString + |> Result.map (surroundEachChar charsWithoutPlus) + |> Result.bind SwedishCoordinationNumber.parse =! Ok num + + testProp "roundtrip for 10 digit string without hyphen delimiter, mixed with 'non-digits' except plus" + <| fun (Gen.CoordNum.ValidNum num, Gen.Char100 charArray) -> + let charsWithoutPlus = + let isDigitOrPlus (c:char) = "0123456789+".Contains c + + charArray + |> Array.filter (isDigitOrPlus >> not) + + num + |> SwedishCoordinationNumber.to10DigitString + |> Result.map removeHyphen + |> Result.map (surroundEachChar charsWithoutPlus) + |> Result.bind SwedishCoordinationNumber.parse =! Ok num + + testPropWithMaxTest 400 "roundtrip for 12 digit string 'in specific year'" <| fun (Gen.CoordNum.ValidNum num) -> + let offset = rng.Next (0, 200) + let year = num.Year |> Year.map ((+) offset) + + num + |> SwedishCoordinationNumber.to12DigitString + |> SwedishCoordinationNumber.parseInSpecificYear year =! Ok num + + testPropWithMaxTest 400 "roundtrip for 10 digit string 'in specific year'" <| fun (Gen.CoordNum.ValidNum num) -> + let offset = rng.Next (0, 200) + let year = num.Year |> Year.map ((+) offset) + + num + |> SwedishCoordinationNumber.to10DigitStringInSpecificYear year + |> Result.bind (SwedishCoordinationNumber.parseInSpecificYear year) = Ok num + + testPropWithMaxTest 400 "roundtrip for 10 digit string without hyphen delimeter 'in specific year'" + <| fun (Gen.CoordNum.ValidNum num) -> + let offset = rng.Next (0, 200) + let year = num.Year |> Year.map ((+) offset) + + num + |> SwedishCoordinationNumber.to10DigitStringInSpecificYear year + |> Result.map removeHyphen + |> Result.bind (SwedishCoordinationNumber.parseInSpecificYear year) = Ok num + ] + +let invalidNumberTests = testList "invalid coordination numbers" [ + test "null string returns argument null error" { + null + |> SwedishCoordinationNumber.parse =! Error ArgumentNullError } + + testProp "empty string returns parsing error" <| fun (Gen.EmptyString str) -> + str + |> SwedishCoordinationNumber.parse =! Error (ParsingError Empty) + + testProp "invalid number of digits returns parsing error" <| fun (Gen.Digits digits) -> + isInvalidNumberOfDigits digits ==> + lazy (digits + |> SwedishCoordinationNumber.parse =! Error (ParsingError Length)) + + testProp "invalid num returns parsing error" <| fun (Gen.CoordNum.InvalidNumString str) -> + match SwedishCoordinationNumber.parse str with + | Error (ParsingError (Invalid _)) -> true + | _ -> failwith "Did not return expected error" + + testProp "parseInSpecificYear with empty string returns parsing error" <| fun (Gen.EmptyString str, Gen.ValidYear year) -> + let y = Year.createOrFail year + str + |> SwedishCoordinationNumber.parseInSpecificYear y =! Error (ParsingError Empty) + + testProp "parseInSpecificYear with null string returns argument null error" <| fun (Gen.ValidYear year) -> + let y = Year.createOrFail year + null + |> SwedishCoordinationNumber.parseInSpecificYear y =! Error ArgumentNullError + + testPropWithMaxTest 400 "cannot convert a num to 10 digit string in a specific year when the person would be 200 years or older" + <| fun (Gen.CoordNum.ValidNum num) -> + let offset = + let maxYear = System.DateTime.MaxValue.Year - num.Year.Value + rng.Next(200, maxYear) + let year = num.Year |> Year.map (fun year -> year + offset) + + num + |> SwedishCoordinationNumber.to10DigitStringInSpecificYear year =! Error (InvalidSerializationYear "SerializationYear cannot be a more than 199 years after the person was born") + + testPropWithMaxTest 400 "cannot convert a num to 10 digit string in a specific year before the person was born" + <| fun (Gen.CoordNum.ValidNum num) -> + let offset = rng.Next(1, num.Year.Value) + let year = num.Year |> Year.map (fun year -> year - offset) + + num + |> SwedishCoordinationNumber.to10DigitStringInSpecificYear year =! Error (InvalidSerializationYear "SerializationYear cannot be a year before the person was born") ] + +[] +let tests = testList "SwedishCoordinationNumber.parse" [ validNumberTests ; invalidNumberTests ] diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_create.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_create.fs index 2301b93..c40387b 100644 --- a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_create.fs +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_create.fs @@ -14,71 +14,65 @@ open ActiveLogin.Identity.Swedish.FSharp.TestData [] let tests = - testList "create" [ - testProp "roundtrip 12DigitString -> create -> to12DigitString" <| fun (Gen.Valid12Digit str) -> + testList "SwedishPersonalIdentityNumber.create" [ + testProp "roundtrip 12DigitString -> create -> to12DigitString" <| fun (Gen.Pin.Valid12Digit str) -> str |> Gen.stringToValues |> SwedishPersonalIdentityNumber.create |> Result.map SwedishPersonalIdentityNumber.to12DigitString = Ok str testPropWithMaxTest 20000 "invalid year returns InvalidYear Error" <| - fun (Gen.ValidValues validValues, Gen.InvalidYear invalidYear) -> - let input = { validValues with Year = invalidYear } - let result = SwedishPersonalIdentityNumber.create input + fun (Gen.Pin.ValidValues (y, m, d, b, c), Gen.InvalidYear invalidYear) -> + let result = SwedishPersonalIdentityNumber.create (invalidYear, m, d, b, c) result =! Error(InvalidYear invalidYear) testPropWithMaxTest 20000 "valid year does not return InvalidYear Error" <| - fun (Gen.ValidValues values, Gen.ValidYear validYear) -> - let input = { values with Year = validYear } - let result = SwedishPersonalIdentityNumber.create input + fun (Gen.Pin.ValidValues (y, m, d, b, c), Gen.ValidYear validYear) -> + let result = SwedishPersonalIdentityNumber.create (y, m, d, b, c) result <>! (Error(InvalidYear validYear)) testProp "invalid month returns InvalidMonth Error" <| - fun (Gen.ValidValues validValues, Gen.InvalidMonth invalidMonth) -> - let input = { validValues with Month = invalidMonth } - let result = SwedishPersonalIdentityNumber.create input + fun (Gen.Pin.ValidValues (y, m, d, b, c), Gen.InvalidMonth invalidMonth) -> + let result = SwedishPersonalIdentityNumber.create (y, invalidMonth, d, b, c) result =! Error(InvalidMonth invalidMonth) testProp "valid month does not return InvalidMonth Error" <| - fun (Gen.ValidValues values, Gen.ValidMonth validMonth) -> - let input = { values with Month = validMonth } - let result = SwedishPersonalIdentityNumber.create input + fun (Gen.Pin.ValidValues (y, m, d, b, c), Gen.ValidMonth validMonth) -> + let result = SwedishPersonalIdentityNumber.create (y, m, d, b, c) result <>! (Error(InvalidMonth validMonth)) - testProp "invalid day returns InvalidDay Error" <| fun (Gen.WithInvalidDay input) -> - let result = SwedishPersonalIdentityNumber.create input - result =! Error(InvalidDayAndCoordinationDay input.Day ) + testProp "invalid day returns InvalidDay Error" <| fun (Gen.Pin.WithInvalidDay (y, m, d, b, c)) -> + let result = SwedishPersonalIdentityNumber.create (y, m, d, b, c) + result =! Error(InvalidDayAndCoordinationDay d) - testProp "valid day does not return InvalidDay Error" <| fun (Gen.WithValidDay input) -> - let result = SwedishPersonalIdentityNumber.create input - result <>! Error(InvalidDayAndCoordinationDay input.Day) - result <>! Error(InvalidDay input.Day) + testProp "valid day does not return InvalidDay Error" <| fun (Gen.Pin.WithValidDay (y, m, d, b, c)) -> + let result = SwedishPersonalIdentityNumber.create (y, m, d, b, c) + result <>! Error(InvalidDayAndCoordinationDay d) + result <>! Error(InvalidDay d) testProp "invalid birth number returns InvalidBirthNumber Error" <| - fun (Gen.ValidValues validValues, Gen.InvalidBirthNumber invalidBirthnumber) -> - let input = { validValues with BirthNumber = invalidBirthnumber } - let result = SwedishPersonalIdentityNumber.create input - result =! Error(InvalidBirthNumber invalidBirthnumber) + fun (Gen.Pin.ValidValues (y, m, d, b, c), Gen.InvalidBirthNumber invalidBirthNumber) -> + let result = SwedishPersonalIdentityNumber.create (y, m, d, invalidBirthNumber, c) + result =! Error(InvalidBirthNumber invalidBirthNumber) testPropWithMaxTest 3000 "valid birth number does not return InvalidBirthNumber Error" <| - fun (Gen.ValidValues validValues, Gen.ValidBirthNumber validBirthNumber) -> - let input = { validValues with BirthNumber = validBirthNumber} - let result = SwedishPersonalIdentityNumber.create input + fun (Gen.Pin.ValidValues (y, m, d, b, c), Gen.ValidBirthNumber validBirthNumber) -> + let result = SwedishPersonalIdentityNumber.create (y, m, d, validBirthNumber, c) result <>! Error(InvalidBirthNumber validBirthNumber) testProp "invalid checksum returns InvalidChecksum Error" <| - fun (Gen.ValidValues values) -> + fun (Gen.Pin.ValidValues (y, m, d, b, c)) -> let invalidChecksums = [ 0..9 ] - |> List.except [ values.Checksum ] + |> List.except [ c ] let withInvalidChecksums = invalidChecksums - |> List.map (fun checksum -> { values with Checksum = checksum }) + |> List.map (fun checksum -> (y, m, d, b, checksum)) let invalidChecksumsAndResults = withInvalidChecksums - |> List.map (fun values -> values.Checksum, SwedishPersonalIdentityNumber.create values) + |> List.map (fun (y, m, d, b, c) -> c, SwedishPersonalIdentityNumber.create (y, m, d, b, c)) invalidChecksumsAndResults |> List.iter (fun (invalidChecksum, result) -> @@ -86,9 +80,9 @@ let tests = | Error (InvalidChecksum actual) -> invalidChecksum =! actual | _ -> failwith "Expected InvalidChecksum Error") - testProp "possible coordination-number day" <| fun (Gen.ValidValues values) -> - let coordinationDay = values.Day + 60 - let result = { values with Day = coordinationDay } |> SwedishPersonalIdentityNumber.create + testProp "possible coordination-number day" <| fun (Gen.Pin.ValidValues (y, m, d, b, c)) -> + let coordinationDay = d + 60 + let result = SwedishPersonalIdentityNumber.create (y, m, coordinationDay, b, c) result =! Error(InvalidDay coordinationDay) testCase "fsharp should have no public constructor" <| fun () -> diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_equality.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_equality.fs index 626a665..f30c9f7 100644 --- a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_equality.fs +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_equality.fs @@ -10,34 +10,34 @@ open FsCheck [] -let tests = testList "equality" [ +let tests = testList "SwedishPersonalIdentityNumber.equality" [ testProp "identical pins are equal when using operator" <| - fun (Gen.TwoEqualPins (pin1, pin2)) -> + fun (Gen.Pin.TwoEqualPins (pin1, pin2)) -> pin1 = pin2 =! true pin2 = pin1 =! true testProp "identical pins are equal when using .Equals()" <| - fun (Gen.TwoEqualPins (pin1, pin2)) -> + fun (Gen.Pin.TwoEqualPins (pin1, pin2)) -> pin1.Equals(pin2) =! true pin2.Equals(pin1) =! true testProp "identical pins are equal when using .Equals() and one pin is object" <| - fun (Gen.TwoEqualPins (pin1, pin2)) -> + fun (Gen.Pin.TwoEqualPins (pin1, pin2)) -> let pin2 = pin2 :> obj pin1.Equals(pin2) =! true pin2.Equals(pin1) =! true testProp "different pins are not equal" <| - fun (Gen.TwoPins (pin1, pin2)) -> + fun (Gen.Pin.TwoPins (pin1, pin2)) -> pin1 <> pin2 ==> lazy pin1 <> pin2 =! true pin2 <> pin1 =! true testProp "different pins are not equal using .Equals()" <| - fun (Gen.TwoPins (pin1, pin2)) -> + fun (Gen.Pin.TwoPins (pin1, pin2)) -> pin1 <> pin2 ==> lazy pin1.Equals(pin2) =! false pin2.Equals(pin1) =! false testProp "a pin is not equal to null using .Equals()" <| - fun (Gen.ValidPin pin) -> + fun (Gen.Pin.ValidPin pin) -> pin.Equals(null) =! false testProp "a pin is not equal to object null using .Equals()" <| - fun (Gen.ValidPin pin) -> + fun (Gen.Pin.ValidPin pin) -> let nullObject = null :> obj pin.Equals(nullObject) =! false ] diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_hash.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_hash.fs index 5ddd02f..ec1da88 100644 --- a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_hash.fs +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_hash.fs @@ -7,7 +7,7 @@ open Expecto open Swensen.Unquote [] -let tests = testList "hash" [ +let tests = testList "SwedishPersonalIdentityNumber.hash" [ testProp "identical pins have the same hash code" <| - fun (Gen.TwoEqualPins (pin1, pin2)) -> + fun (Gen.Pin.TwoEqualPins (pin1, pin2)) -> hash pin1 =! hash pin2 ] diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_hints.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_hints.fs index 6496389..84a2301 100644 --- a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_hints.fs +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_hints.fs @@ -26,10 +26,10 @@ let (|Even|Odd|) (num:BirthNumber) = [] let tests = - testList "hints" [ + testList "SwedishPersonalIdentityNumber.hints" [ testList "getAgeHint" [ testProp "a person ages by years counting from their date of birth" - <| fun (Gen.ValidPin pin, Gen.Age (years, months, days)) -> + <| fun (Gen.Pin.ValidPin pin, Gen.Age (years, months, days)) -> not (pin.Month.Value = 2 && pin.Day.Value = 29) ==> lazy let dateOfBirth = getDateOfBirth pin @@ -42,7 +42,7 @@ let tests = Hints.getAgeHintOnDate checkDate pin =! Some years testProp "a person born on a leap day also ages by years counting from their date of birth" - <| fun (Gen.LeapDayPin pin, Gen.Age (years, months, days)) -> + <| fun (Gen.Pin.LeapDayPin pin, Gen.Age (years, months, days)) -> let dateOfBirth = getDateOfBirth pin // Since there isn't a leap day every year we need to add 1 day to the checkdate let checkDate = @@ -53,20 +53,20 @@ let tests = Hints.getAgeHintOnDate checkDate pin =! Some years - testProp "cannot get age for date before person was born" <| fun (Gen.ValidPin pin) -> + testProp "cannot get age for date before person was born" <| fun (Gen.Pin.ValidPin pin) -> let dateOfBirth = getDateOfBirth pin let checkOffset = rng.NextDouble() * 199. * 365. let checkDate = dateOfBirth.Date.AddDays -checkOffset let result = Hints.getAgeHintOnDate checkDate pin result |> Expect.isNone "age should be None" - testProp "getAgeHint uses DateTime.UtcNow as checkYear" <| fun (Gen.ValidPin pin) -> + testProp "getAgeHint uses DateTime.UtcNow as checkYear" <| fun (Gen.Pin.ValidPin pin) -> let age1 = Hints.getAgeHintOnDate DateTime.UtcNow pin let age2 = Hints.getAgeHint pin age1 =! age2 ] testList "getDateOfBirthHint" [ - testProp "get date of birth hint extracts year, month and date from pin" <| fun (Gen.ValidPin pin) -> + testProp "get date of birth hint extracts year, month and date from pin" <| fun (Gen.Pin.ValidPin pin) -> let result = Hints.getDateOfBirthHint pin result.Year =! pin.Year.Value @@ -75,7 +75,7 @@ let tests = ] testList "getGenderHint" [ - testProp "even birthnumber indicates a female, odd birthnumber a male" <| fun (Gen.ValidPin pin) -> + testProp "even birthnumber indicates a female, odd birthnumber a male" <| fun (Gen.Pin.ValidPin pin) -> match pin.BirthNumber with | Even -> Hints.getGenderHint pin =! Gender.Female | Odd -> Hints.getGenderHint pin =! Gender.Male diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_parse.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_parse.fs index 3998942..da7e789 100644 --- a/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_parse.fs +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/SwedishPersonalIdentityNumber_parse.fs @@ -6,24 +6,6 @@ open ActiveLogin.Identity.Swedish.FSharp open ActiveLogin.Identity.Swedish.FSharp.TestData open FsCheck -let private getRandomFromArray arr = - fun () -> - let index = rng.Next(0, Array.length arr - 1) - arr.[index] - -let private removeHyphen (str:string) = - let isHyphen (c:char) = "-".Contains(c) - String.filter (isHyphen >> not) str - -let private surroundEachChar (chars:char[]) (pin:string) = - let rnd = getRandomFromArray chars - let surroundWith c = [| rnd(); c; rnd() |] - - Seq.collect surroundWith pin - |> Array.ofSeq - |> System.String - -let private isDigit (c:char) = "0123456789".Contains(c) let private isInvalidNumberOfDigits (str: string) = if System.String.IsNullOrWhiteSpace str then false @@ -33,24 +15,24 @@ let private isInvalidNumberOfDigits (str: string) = |> (fun s -> s.Length <> 10 && s.Length <> 12) let private validPinTests = testList "valid pins" [ - testProp "roundtrip for 12 digit string" <| fun (Gen.ValidPin pin) -> + testProp "roundtrip for 12 digit string" <| fun (Gen.Pin.ValidPin pin) -> pin |> SwedishPersonalIdentityNumber.to12DigitString |> SwedishPersonalIdentityNumber.parse =! Ok pin - testProp "roundtrip for 10 digit string with delimiter" <| fun (Gen.ValidPin pin) -> + testProp "roundtrip for 10 digit string with delimiter" <| fun (Gen.Pin.ValidPin pin) -> pin |> SwedishPersonalIdentityNumber.to10DigitString |> Result.bind SwedishPersonalIdentityNumber.parse =! Ok pin - testProp "roundtrip for 10 digit string without hyphen-delimiter" <| fun (Gen.ValidPin pin) -> + testProp "roundtrip for 10 digit string without hyphen-delimiter" <| fun (Gen.Pin.ValidPin pin) -> pin |> SwedishPersonalIdentityNumber.to10DigitString |> Result.map removeHyphen |> Result.bind SwedishPersonalIdentityNumber.parse =! Ok pin testProp "roundtrip for 12 digit string mixed with 'non-digits'" - <| fun (Gen.ValidPin pin, Gen.Char100 charArray) -> + <| fun (Gen.Pin.ValidPin pin, Gen.Char100 charArray) -> let charsWithoutDigits = charArray |> Array.filter (isDigit >> not) @@ -61,7 +43,7 @@ let private validPinTests = testList "valid pins" [ |> SwedishPersonalIdentityNumber.parse =! Ok pin testProp "roundtrip for 10 digit string mixed with 'non-digits' except plus" - <| fun (Gen.ValidPin pin, Gen.Char100 charArray) -> + <| fun (Gen.Pin.ValidPin pin, Gen.Char100 charArray) -> let charsWithoutPlus = let isDigitOrPlus (c:char) = "0123456789+".Contains c @@ -74,7 +56,7 @@ let private validPinTests = testList "valid pins" [ |> Result.bind SwedishPersonalIdentityNumber.parse =! Ok pin testProp "roundtrip for 10 digit string without hyphen delimiter, mixed with 'non-digits' except plus" - <| fun (Gen.ValidPin pin, Gen.Char100 charArray) -> + <| fun (Gen.Pin.ValidPin pin, Gen.Char100 charArray) -> let charsWithoutPlus = let isDigitOrPlus (c:char) = "0123456789+".Contains c @@ -87,7 +69,7 @@ let private validPinTests = testList "valid pins" [ |> Result.map (surroundEachChar charsWithoutPlus) |> Result.bind SwedishPersonalIdentityNumber.parse =! Ok pin - testPropWithMaxTest 400 "roundtrip for 12 digit string 'in specific year'" <| fun (Gen.ValidPin pin) -> + testPropWithMaxTest 400 "roundtrip for 12 digit string 'in specific year'" <| fun (Gen.Pin.ValidPin pin) -> let offset = rng.Next (0, 200) let year = pin.Year |> Year.map ((+) offset) @@ -95,7 +77,7 @@ let private validPinTests = testList "valid pins" [ |> SwedishPersonalIdentityNumber.to12DigitString |> SwedishPersonalIdentityNumber.parseInSpecificYear year =! Ok pin - testPropWithMaxTest 400 "roundtrip for 10 digit string 'in specific year'" <| fun (Gen.ValidPin pin) -> + testPropWithMaxTest 400 "roundtrip for 10 digit string 'in specific year'" <| fun (Gen.Pin.ValidPin pin) -> let offset = rng.Next (0, 200) let year = pin.Year |> Year.map ((+) offset) @@ -104,7 +86,7 @@ let private validPinTests = testList "valid pins" [ |> Result.bind (SwedishPersonalIdentityNumber.parseInSpecificYear year) = Ok pin testPropWithMaxTest 400 "roundtrip for 10 digit string without hyphen delimeter 'in specific year'" - <| fun (Gen.ValidPin pin) -> + <| fun (Gen.Pin.ValidPin pin) -> let offset = rng.Next (0, 200) let year = pin.Year |> Year.map ((+) offset) @@ -128,7 +110,7 @@ let invalidPinTests = testList "invalid pins" [ lazy (digits |> SwedishPersonalIdentityNumber.parse =! Error (ParsingError Length)) - testProp "invalid pin returns parsing error" <| fun (Gen.InvalidPinString str) -> + testProp "invalid pin returns parsing error" <| fun (Gen.Pin.InvalidPinString str) -> match SwedishPersonalIdentityNumber.parse str with | Error (ParsingError (Invalid _)) -> true | _ -> failwith "Did not return expected error" @@ -144,7 +126,7 @@ let invalidPinTests = testList "invalid pins" [ |> SwedishPersonalIdentityNumber.parseInSpecificYear y =! Error ArgumentNullError testPropWithMaxTest 400 "cannot convert a pin to 10 digit string in a specific year when the person would be 200 years or older" - <| fun (Gen.ValidPin pin) -> + <| fun (Gen.Pin.ValidPin pin) -> let offset = let maxYear = System.DateTime.MaxValue.Year - pin.Year.Value rng.Next(200, maxYear) @@ -154,7 +136,7 @@ let invalidPinTests = testList "invalid pins" [ |> SwedishPersonalIdentityNumber.to10DigitStringInSpecificYear year =! Error (InvalidSerializationYear "SerializationYear cannot be a more than 199 years after the person was born") testPropWithMaxTest 400 "cannot convert a pin to 10 digit string in a specific year before the person was born" - <| fun (Gen.ValidPin pin) -> + <| fun (Gen.Pin.ValidPin pin) -> let offset = rng.Next(1, pin.Year.Value) let year = pin.Year |> Year.map (fun year -> year - offset) @@ -162,4 +144,4 @@ let invalidPinTests = testList "invalid pins" [ |> SwedishPersonalIdentityNumber.to10DigitStringInSpecificYear year =! Error (InvalidSerializationYear "SerializationYear cannot be a year before the person was born") ] [] -let tests = testList "parse" [ validPinTests ; invalidPinTests ] +let tests = testList "SwedishPersonalIdentityNumber.parse" [ validPinTests ; invalidPinTests ] diff --git a/test/ActiveLogin.Identity.Swedish.FSharp.Test/TestExtensions.fs b/test/ActiveLogin.Identity.Swedish.FSharp.Test/TestExtensions.fs index dd5724a..ff55894 100644 --- a/test/ActiveLogin.Identity.Swedish.FSharp.Test/TestExtensions.fs +++ b/test/ActiveLogin.Identity.Swedish.FSharp.Test/TestExtensions.fs @@ -3,18 +3,19 @@ module ActiveLogin.Identity.Swedish.FSharp.TestExtensions open Expecto open Expecto.Flip open Swensen.Unquote +open System module Expect = - let equalPin (expected: SwedishPersonalIdentityNumberValues) (actual: Result) = + let equalPin (year, month, day, birthNumber, checksum) (actual: Result) = actual |> Expect.isOk "should be ok" match actual with | Error _ -> failwith "test error" | Ok pin -> - pin.Year |> Year.value =! expected.Year - pin.Month |> Month.value =! expected.Month - pin.Day |> Day.value =! expected.Day - pin.BirthNumber |> BirthNumber.value =! expected.BirthNumber - pin.Checksum |> Checksum.value =! expected.Checksum + pin.Year |> Year.value =! year + pin.Month |> Month.value =! month + pin.Day |> Day.value =! day + pin.BirthNumber |> BirthNumber.value =! birthNumber + pin.Checksum |> Checksum.value =! checksum module Result = let iter f res = @@ -34,6 +35,55 @@ module SwedishPersonalIdentityNumber = let createOrFail = SwedishPersonalIdentityNumber.create >> Result.OkValue let parseOrFail = SwedishPersonalIdentityNumber.parse >> Result.OkValue +module SwedishCoordinationNumber = + let createOrFail = SwedishCoordinationNumber.create >> Result.OkValue + +module CoordinationDay = + let createOrFail y m = CoordinationDay.create y m >> Result.OkValue + +module Checksum = + + let createOrFail y m d b = Checksum.create y m d b >> Result.OkValue + + // copied from production code :( + let getChecksum (year: Year) (month: Month) day (birth: BirthNumber) = + let day' = + match day with + | Day d -> d.Value + | CoordinationDay cd -> cd.Value + let twoDigitYear = year.Value % 100 + let numberStr = sprintf "%02i%02i%02i%03i" twoDigitYear month.Value day' birth.Value + let digits = numberStr |> Seq.map (fun s -> s.ToString() |> int) + digits + |> Seq.rev + |> Seq.mapi (fun (i : int) (d : int) -> + if i % 2 = 0 then d * 2 + else d) + |> Seq.rev + |> Seq.sumBy (fun (d : int) -> + if d > 9 then d - 9 + else d) + |> fun x -> (x * 9) % 10 + |> createOrFail year month day birth + module Year = let createOrFail = Year.create >> Result.OkValue let map f y = y |> Year.value |> f |> createOrFail + +module Month = + let createOrFail = Month.create >> Result.OkValue + + let getCheckSum (year) (month) day' (birth) = + let twoDigitYear = year % 100 + let numberStr = sprintf "%02i%02i%02i%03i" twoDigitYear month day' birth + let digits = numberStr |> Seq.map (fun s -> s.ToString() |> int) + digits + |> Seq.rev + |> Seq.mapi (fun (i : int) (d : int) -> + if i % 2 = 0 then d * 2 + else d) + |> Seq.rev + |> Seq.sumBy (fun (d : int) -> + if d > 9 then d - 9 + else d) + |> fun x -> (x * 9) % 10 diff --git a/test/ActiveLogin.Identity.Swedish.Test/SwedishPersonalIdentityNumber_ToString.cs b/test/ActiveLogin.Identity.Swedish.Test/SwedishPersonalIdentityNumber_ToString.cs index f24078c..4c41ef0 100644 --- a/test/ActiveLogin.Identity.Swedish.Test/SwedishPersonalIdentityNumber_ToString.cs +++ b/test/ActiveLogin.Identity.Swedish.Test/SwedishPersonalIdentityNumber_ToString.cs @@ -18,8 +18,8 @@ public void ToString_Returns_12DigitString() [Fact] public void ToString_Returns_Native_FSharp_ToString() { - var values = new FSharp.Types.SwedishPersonalIdentityNumberValues(1999, 08, 07, 239, 1); - var personalIdentityNumber = FSharp.SwedishPersonalIdentityNumber.create(values).ResultValue; + var personalIdentityNumber = + FSharp.SwedishPersonalIdentityNumber.create(1999, 08, 07, 239, 1).ResultValue; var str = personalIdentityNumber.ToString(); Assert.Contains("Year 1999", str); Assert.Contains("Month 8", str);