Open anfly0 opened 4 years ago
Here is some code for validating and calculating the checksum of Swedish personal numbers using Luhns algorithm.
There are 2 versions available, one recursive and one based on Enum.
defmodule LuhnChecker do
def valid?(number) when is_integer(number) and number >= 0 do
digits = Enum.reverse(Integer.digits(number))
rem(sum_digits(digits, 1), 10) === 0
end
def checksum(number) when is_integer(number) and number >= 0 do
calculate_checksum(Integer.digits(number))
end
# alternate solution to calculating the checksum
defp checksum1(digits) do
sum =
digits
|> Enum.zip(Stream.cycle([2, 1]))
|> Enum.flat_map(fn {digit, factor} -> (digit * factor) |> Integer.digits() end)
|> Enum.sum()
ceil(sum / 10) * 10 - sum
end
defp calculate_checksum(digits) do
sum = sum_digits(digits, 2)
ceil(sum / 10) * 10 - sum
end
defp sum_digits([], _), do: 0
defp sum_digits([digit | digits], factor) do
sum_digits(
digits,
next_factor(factor)
) + checksum_digit(digit * factor)
end
defp checksum_digit(digit) when digit <= 9, do: digit
defp checksum_digit(digit), do: digit - 9
defp next_factor(1), do: 2
defp next_factor(2), do: 1
end
First of all, thank you, @kwando, for taking the time. This looks very promising, and I would be happy to merge some version of this. If you want and have the time, could you wrap this up in a PR? Just decide on what version you think is the cleanest and add some basic tests.
If you would like to take a crack at the rest of this issue, you're of course more than welcome to do so.
There is actually already a HEX package for validating strings of numbers based on Luhn's algorithm: https://hex.pm/packages/luhn.
I threw together the following example that validates:
def check_personal_number(personal_number)
when is_binary(personal_number) do
with true <- String.length(personal_number) == 12,
true <- check_personal_number_century(personal_number),
true <- personal_number |> String.slice(2, 10) |> Luhn.valid?() do
{:ok, personal_number}
else
false -> {:error, "Invalid personal number: #{personal_number}"}
end
end
def check_personal_number(nil) do
{:ok, nil}
end
defp check_personal_number_century("19" <> _), do: true
defp check_personal_number_century("20" <> _), do: true
defp check_personal_number_century(_), do: false
Then of course, one could validate that the month and day are reasonable numbers. My mind goes to NimbleParsec and this Elixir Forum topic. Now, needless to say, there is the Gregorian calendar to observe in this regard. But, I guess that a rudimentary check is better than nothing to begin with?
For testing there's the Swedish Tax Authority's published personal numbers reserved for testing. I guess one would want tests that cover the cases. With the current ones all Luhn validations will be performed on mere zeros.
First stab. No previous experience in writing parsers so take it for what it is.
defmodule MyParser do
import NimbleParsec
century =
choice([string("19"), string("20")])
year =
integer(2)
month =
~w(01 02 03 04 05 06 07 08 09 10 11 12)
|> Enum.map(&string/1)
|> choice()
day =
(~w(01 02 03 04 05 06 07 08 09) ++ Enum.map(10..31, &to_string/1))
|> Enum.map(&string/1)
|> choice()
defparsec :personal_number, century |> concat(year) |> concat(month) |> concat(day)
end
Add personal number validation to the
new
methods inExBankID.Auth.Payload
andExBankID.Sign.Payload
.