Itin parser and validator added

This commit is contained in:
Danil Negrienko 2024-12-12 21:17:30 -05:00
parent 0edc3c3139
commit 695ffc31c5
14 changed files with 513 additions and 108 deletions

View File

@ -1,9 +1,9 @@
defmodule UkraineTaxidEx.Base do
@callback length() :: non_neg_integer()
@callback parse(data :: {:ok, String.t()} | String.t(), options :: Keyword.t()) ::
{:ok, term} | {:error, atom()}
@callback to_map(data :: term) :: map()
@callback to_string(data :: term) :: String.t()
@callback length() :: non_neg_integer()
defmacro __using__(_) do
quote do
@ -11,9 +11,8 @@ defmodule UkraineTaxidEx.Base do
alias UkraineTaxidEx.{Base, Serialize, Commons}
@impl Base
@spec length() :: non_neg_integer()
def length(), do: @length
@parse_module (Module.split(__MODULE__) ++ ["Parser"]) |> Module.safe_concat()
# def parse_module(), do: @parse_module
@impl Base
@spec to_map(data :: t()) :: map()
@ -23,7 +22,19 @@ defmodule UkraineTaxidEx.Base do
@spec to_string(data :: t()) :: binary()
defdelegate to_string(data), to: Serialize
defoverridable to_string: 1, to_map: 1, length: 0
@doc """
Returns the length of the code.
"""
@impl Base
@spec length() :: non_neg_integer()
def length(), do: @length
@impl Base
@spec parse(data :: {:ok, String.t()} | String.t(), options :: Keyword.t()) ::
{:ok, t()} | {:error, atom()}
defdelegate parse(data, options \\ [normalize?: false, clean?: false]), to: @parse_module
defoverridable to_string: 1, to_map: 1, length: 0, parse: 2
end
end
end

View File

@ -7,6 +7,33 @@ defmodule UkraineTaxidEx.BaseParser do
@behaviour UkraineTaxidEx.BaseParser
alias UkraineTaxidEx.BaseParser
@type string_or_ok() :: String.t() | {:ok, String.t()}
@type struct_or_error() :: {:ok, term} | {:error, atom()}
@struct_module Module.split(__MODULE__) |> Enum.slice(0..-2//1) |> Module.safe_concat()
# def struct_module(), do: @struct_module
defp to_struct(map), do: struct(@struct_module, map)
@impl BaseParser
@spec parse(data :: string_or_ok, options :: BaseParser.options()) :: struct_or_error()
def parse(data, options \\ [normalize?: false, clean?: false])
def parse({:ok, string}, options), do: parse(string, options)
def parse({:error, error}, _options), do: {:error, error}
def parse(string, options) do
length = (Keyword.get(options, :normalize?, false) && length()) || 0
clean? = Keyword.get(options, :clean?, false)
string
|> digits(length, clean?)
|> undigits()
|> validate()
|> generate()
end
defp generate({:error, error}), do: {:error, error}
end
end
end

View File

@ -0,0 +1,65 @@
defmodule UkraineTaxidEx.BaseValidator do
@callback validate(String.t()) ::
{:ok, String.t()}
| {:error,
:length_too_short | :length_too_long | :invalid_length | :invalid_checksum}
@callback violates_length?(String.t()) :: boolean
@callback violates_length_too_short?(String.t()) :: boolean
@callback violates_length_too_long?(String.t()) :: boolean
@callback violates_checksum?(String.t()) :: boolean
defmacro __using__(_) do
quote do
alias UkraineTaxidEx.BaseValidator
@behaviour UkraineTaxidEx.BaseValidator
import UkraineTaxidEx.Commons, only: [digits: 1, digits_and_check_digit: 1, error: 1, ok: 1]
@impl BaseValidator
@spec validate(String.t()) ::
{:ok, String.t()}
| {:error,
:length_too_short | :length_too_long | :invalid_length | :invalid_checksum}
def validate(string) do
cond do
violates_length_too_short?(string) -> error(:length_too_short)
violates_length_too_long?(string) -> error(:length_too_long)
violates_checksum?(string) -> error(:invalid_checksum)
true -> ok(string)
end
end
@doc "Check whether a given EDRPOU violates the required length"
@impl BaseValidator
@spec violates_length?(String.t()) :: boolean
def violates_length?(string),
do: String.length(string) != length()
@doc "Check whether a given EDRPOU too short"
@impl BaseValidator
@spec violates_length_too_short?(String.t()) :: boolean
def violates_length_too_short?(string),
do: String.length(string) < length()
@doc "Check whether a given EDRPOU too long"
@impl BaseValidator
@spec violates_length_too_long?(String.t()) :: boolean
def violates_length_too_long?(string),
do: String.length(string) > length()
@doc "Check whether a given EDRPOU has correct checksum"
@impl BaseValidator
@spec violates_checksum?(String.t()) :: boolean
def violates_checksum?(string) do
{digits, check_digit} =
string
|> digits()
|> digits_and_check_digit()
check_sum = check_sum(digits)
check_sum != check_digit
end
end
end
end

View File

@ -6,8 +6,6 @@ defmodule UkraineTaxidEx.Edrpou do
"""
@length 8
alias UkraineTaxidEx.Edrpou.Parser
use UkraineTaxidEx.Base
@type t :: %__MODULE__{
code: String.t(),
@ -17,8 +15,5 @@ defmodule UkraineTaxidEx.Edrpou do
defstruct code: nil, check_digit: nil, check_sum: nil
@impl Base
@spec parse(data :: {:ok, String.t()} | String.t(), options :: Keyword.t()) ::
{:ok, t()} | {:error, atom()}
defdelegate parse(data, options \\ [normalize?: false, clean?: false]), to: Parser
use UkraineTaxidEx.Base
end

View File

@ -2,31 +2,8 @@ defmodule UkraineTaxidEx.Edrpou.Parser do
@moduledoc """
Parser module for EDRPOU (Unified State Register of Ukrainian Enterprises and Organizations) codes.
Handles validation and structure creation for EDRPOU codes with additional options for normalization and cleaning.
"""
alias UkraineTaxidEx.Edrpou
import UkraineTaxidEx.Edrpou, only: [length: 0]
import UkraineTaxidEx.Edrpou.CheckSum, only: [check_sum: 1]
import UkraineTaxidEx.Edrpou.Validator, only: [validate: 1]
import UkraineTaxidEx.Commons, only: [check_digit: 1, digits: 1, digits: 3, undigits: 1, ok: 1]
use UkraineTaxidEx.BaseParser
@type edrpou_string() :: String.t()
@type edrpou_string_or_ok() :: edrpou_string() | {:ok, edrpou_string()}
@type edrpou() :: Edrpou.t()
@type edrpou_or_error() ::
{:ok, Edrpou.t()}
| {:error,
:length_too_short
| :length_too_long
| :invalid_checksum}
@impl BaseParser
@doc """
Parses an EDRPOU code string into a structured format (clean and normalize, validate and decompose).
Parses an EDRPOU code string into a structured format (clean and normalize, validate and decompo).
Options:
- normalize?: When true, pads string to full EDRPOU length. Defaults to false.
- clean?: When true, removes non-digit characters before processing. Defaults to false.
@ -34,6 +11,7 @@ defmodule UkraineTaxidEx.Edrpou.Parser do
## Examples
```elixir
iex> UkraineTaxidEx.Edrpou.Parser.parse("00032112")
{:ok, %UkraineTaxidEx.Edrpou{code: "00032112", check_digit: 2, check_sum: 2}}
@ -54,33 +32,34 @@ defmodule UkraineTaxidEx.Edrpou.Parser do
iex> UkraineTaxidEx.Edrpou.Parser.parse("123", normalize?: true)
{:error, :invalid_checksum}
```
"""
@spec parse(data :: edrpou_string_or_ok, options :: BaseParser.options()) ::
edrpou_or_error()
def parse(data, options \\ [normalize?: false, clean?: false])
def parse({:ok, edrpou_string}, options), do: parse(edrpou_string, options)
def parse({:error, error}, _options), do: {:error, error}
def parse(edrpou_string, options) do
length = (Keyword.get(options, :normalize?, false) && length()) || 0
clean? = Keyword.get(options, :clean?, false)
alias UkraineTaxidEx.Edrpou
edrpou_string
|> digits(length, clean?)
|> undigits()
|> validate()
|> generate_edrpou()
end
import UkraineTaxidEx.Edrpou, only: [length: 0]
import UkraineTaxidEx.Edrpou.CheckSum, only: [check_sum: 1]
import UkraineTaxidEx.Edrpou.Validator, only: [validate: 1]
import UkraineTaxidEx.Commons, only: [check_digit: 1, digits: 1, digits: 3, undigits: 1, ok: 1]
defp generate_edrpou({:error, error}), do: {:error, error}
use UkraineTaxidEx.BaseParser
defp generate_edrpou({:ok, edrpou_string}) do
digits = digits(edrpou_string)
@type edrpou_string() :: String.t()
@type edrpou() :: Edrpou.t()
@type edrpou_or_error() ::
{:ok, Edrpou.t()}
| {:error,
:length_too_short
| :length_too_long
| :invalid_checksum}
%{code: edrpou_string, check_sum: check_sum(digits), check_digit: check_digit(digits)}
|> create_struct()
defp generate({:error, error}), do: {:error, error}
defp generate({:ok, string}) do
digits = digits(string)
%{code: string, check_sum: check_sum(digits), check_digit: check_digit(digits)}
|> to_struct()
|> ok()
end
defp create_struct(map), do: struct(Edrpou, map)
end

View File

@ -3,13 +3,7 @@ defmodule UkraineTaxidEx.Edrpou.Validator do
Functions for validating EDRPOU number format and checksum.
This module provides validation functions to verify if an EDRPOU number meets the standard requirements including length and checksum validation.
"""
import UkraineTaxidEx.Commons, only: [digits: 1, digits_and_check_digit: 1, error: 1, ok: 1]
import UkraineTaxidEx.Edrpou, only: [length: 0]
import UkraineTaxidEx.Edrpou.CheckSum, only: [check_sum: 1]
@doc """
Validates an EDRPOU number to check if it meets length requirements and has a valid checksum.
Returns:
@ -17,44 +11,25 @@ defmodule UkraineTaxidEx.Edrpou.Validator do
* `{:error, :length_too_short}` if shorter than required length
* `{:error, :length_too_long}` if longer than required length
* `{:error, :invalid_checksum}` if checksum is invalid
## Examples
```elixir
iex> UkraineTaxidEx.Edrpou.Validator.validate("00032112")
{:ok, "00032112"}
iex> UkraineTaxidEx.Edrpou.Validator.validate("0003211")
{:error, :length_too_short}
iex> UkraineTaxidEx.Edrpou.Validator.validate("000321122")
{:error, :length_too_long}
iex> UkraineTaxidEx.Edrpou.Validator.validate("00032113")
{:error, :invalid_checksum}
```
"""
@spec validate(String.t()) ::
{:ok, String.t()}
| {:error, :length_too_short | :length_too_long | :invalid_length | :invalid_checksum}
def validate(edrpou) do
cond do
violates_length_too_short?(edrpou) -> error(:length_too_short)
violates_length_too_long?(edrpou) -> error(:length_too_long)
violates_checksum?(edrpou) -> error(:invalid_checksum)
true -> ok(edrpou)
end
end
import UkraineTaxidEx.Edrpou, only: [length: 0]
import UkraineTaxidEx.Edrpou.CheckSum, only: [check_sum: 1]
@doc "Check whether a given EDRPOU violates the required length"
@spec violates_length?(String.t()) :: boolean
def violates_length?(edrpou),
do: String.length(edrpou) != length()
@doc "Check whether a given EDRPOU too short"
@spec violates_length_too_short?(String.t()) :: boolean
def violates_length_too_short?(edrpou),
do: String.length(edrpou) < length()
@doc "Check whether a given EDRPOU too long"
@spec violates_length_too_long?(String.t()) :: boolean
def violates_length_too_long?(edrpou),
do: String.length(edrpou) > length()
@doc "Check whether a given EDRPOU has correct checksum"
@spec violates_checksum?(String.t()) :: boolean
def violates_checksum?(edrpou) do
{digits, check_digit} =
edrpou
|> digits()
|> digits_and_check_digit()
check_sum = check_sum(digits)
check_sum != check_digit
end
use UkraineTaxidEx.BaseValidator
end

View File

@ -5,20 +5,19 @@ defmodule UkraineTaxidEx.Itin do
The ITIN is a unique 10-digit number that identifies individuals in Ukraine. It is issued to all individuals when they are registered in the official tax register. The Individual Taxpayer Identification Number (ITIN) is an automated system that collects, stores, and processes data on individuals in Ukraine.
"""
@base_date Date.new!(1899, 12, 31)
@length 10
use UkraineTaxidEx.Base
@type gender :: 0 | 1
@type t :: %__MODULE__{
code: String.t(),
birth_date: Date.t(),
number: integer(),
gender: gender,
check_digit: C.digit(),
check_sum: C.digit()
}
defstruct code: nil, birth_date: nil, gender: nil, check_digit: nil, check_sum: nil
defstruct code: nil, birth_date: nil, number: nil, gender: nil, check_digit: nil, check_sum: nil
def base_date(), do: @base_date
use UkraineTaxidEx.Base
end

View File

@ -0,0 +1,42 @@
defmodule UkraineTaxidEx.Itin.CheckSum do
@moduledoc """
Module for calculating the checksum of Ukrainian Individual Tax Identification Numbers (ITIN).
Provides functions for checksum calculation based on weighted digits and helper functions
for working with ITIN weights and dividers. Uses specified numerical weights to multiply
each digit of the ITIN and validate its authenticity.
"""
alias UkraineTaxidEx.Commons, as: C
import UkraineTaxidEx.Commons, only: [value_digits: 1]
@weights [-1, 5, 7, 9, 4, 6, 10, 5, 7]
@doc """
Returns the list of numerical weights used to calculate the ITIN checksum.
Each digit in the ITIN is multiplied by its corresponding weight.
"""
@spec weights() :: C.digits()
def weights(), do: @weights
@spec divider() :: non_neg_integer()
defp divider(), do: 11
@doc """
Calculate checksum for ITIN number.
The checksum for ITIN is calculated in several steps:
1. Multiply each digit by its corresponding weight
2. Sum the products
3. Take mod 11 of the sum
4. If mod 11 is greater or equal than 10, repeat steps 2-4 with weights +2
"""
@spec check_sum(digits :: C.digits(), weights :: C.digits()) :: integer()
def check_sum(digits, weights \\ @weights) do
digits
|> value_digits()
|> Enum.zip(weights)
|> Enum.reduce(0, fn {digit, weight}, acc -> digit * weight + acc end)
|> rem(divider())
|> rem(10)
end
end

View File

@ -12,10 +12,10 @@ defmodule UkraineTaxidEx.Itin.Error do
:length_to_short
]
@messages [
invalid_length: "EDRPOU violates the required length",
invalid_checksum: "EDRPOU checksum is invalid",
length_to_long: "EDRPOU longer then required length",
length_to_short: "EDRPOU shorter then required length"
invalid_length: "ITIN violates the required length",
invalid_checksum: "ITIN checksum is invalid",
length_to_long: "ITIN longer then required length",
length_to_short: "ITIN shorter then required length"
]
@spec message(error()) :: String.t()

View File

@ -0,0 +1,93 @@
defmodule UkraineTaxidEx.Itin.Parser do
@moduledoc """
This module provides parsing functionality for Ukrainian Individual Taxpayer Identification Numbers (ITIN).
ITINs (also known as РНОКПП/ІПН) are unique identifiers assigned to individuals in Ukraine for tax purposes.
The parser validates the number format and extracts meaningful components like the checksum according to
official requirements.
Key features:
- Validates ITIN format and length
- Calculates and verifies checksum
- Parses components into a structured format
- Handles both raw strings and pre-validated input
Examples of successful ITIN parsing:
```elixir
iex> UkraineTaxidEx.Itin.Parser.parse("2222222222")
{:ok, %UkraineTaxidEx.Itin{code: "2222222222", birth_date: ~D[1960-12-17], number: 2222, gender: 0, check_sum: 2, check_digit: 2}}
iex> UkraineTaxidEx.Itin.Parser.parse("3333333333")
{:ok, %UkraineTaxidEx.Itin{code: "3333333333", birth_date: ~D[1991-03-05], number: 3333, gender: 1, check_sum: 3, check_digit: 3}}
```
Examples of unsuccessful ITIN parsing:
```elixir
iex> UkraineTaxidEx.Itin.Parser.parse("123456")
{:error, :length_too_short}
iex> UkraineTaxidEx.Itin.Parser.parse("12345678901")
{:error, :length_too_long}
iex> UkraineTaxidEx.Itin.Parser.parse("1234567890")
{:error, :invalid_checksum}
```
"""
alias UkraineTaxidEx.Itin
alias UkraineTaxidEx.Commons, as: C
import UkraineTaxidEx.Itin, only: [length: 0]
import UkraineTaxidEx.Itin.CheckSum, only: [check_sum: 1]
import UkraineTaxidEx.Itin.Validator, only: [validate: 1]
import UkraineTaxidEx.Commons, only: [check_digit: 1, digits: 1, digits: 3, undigits: 1, ok: 1]
require Integer
@type itin_string() :: String.t()
@type itin() :: Itin.t()
@type itin_or_error() ::
{:ok, Itin.t()}
| {:error,
:length_too_short
| :length_too_long
| :invalid_checksum}
@base_date Date.new!(1899, 12, 31)
def base_date(), do: @base_date
use UkraineTaxidEx.BaseParser
defp generate({:ok, string}) do
digits = digits(string)
%{
code: string,
birth_date: birth_date(digits),
number: number(digits),
gender: gender(digits),
check_sum: check_sum(digits),
check_digit: check_digit(digits)
}
|> to_struct()
|> ok()
end
@spec slice(digits :: C.digits(), Range.t()) :: Date.t()
defp slice(digits, range) do
digits
|> Enum.slice(range)
|> undigits()
|> String.to_integer()
end
@spec birth_date(digits :: C.digits()) :: Date.t()
def birth_date(digits), do: Date.add(base_date(), slice(digits, 0..4))
@spec number(digits :: C.digits()) :: integer()
def number(digits), do: slice(digits, 5..8)
@spec gender(digits :: C.digits()) :: Itin.gender()
def gender(digits), do: (Integer.is_odd(slice(digits, -2..-2//1)) && 1) || 0
end

View File

@ -0,0 +1,35 @@
defmodule UkraineTaxidEx.Itin.Validator do
@moduledoc """
Validator module for Ukrainian Individual Taxpayer Identification Number (ITIN/IPN).
Handles validation of ITIN numbers according to Ukrainian tax authority requirements.
Validates an ITIN number to check if it meets length requirements and has a valid checksum.
Returns:
* `{:ok, itin}` if validation successful
* `{:error, :length_too_short}` if shorter than required length
* `{:error, :length_too_long}` if longer than required length
* `{:error, :invalid_checksum}` if checksum is invalid
Examples:
```elixir
iex> UkraineTaxidEx.Itin.Validator.validate("3184710691")
{:ok, "3184710691"}
iex> UkraineTaxidEx.Itin.Validator.validate("123456")
{:error, :length_too_short}
iex> UkraineTaxidEx.Itin.Validator.validate("12345678901")
{:error, :length_too_long}
iex> UkraineTaxidEx.Itin.Validator.validate("3184710692")
{:error, :invalid_checksum}
```
"""
import UkraineTaxidEx.Itin, only: [length: 0]
import UkraineTaxidEx.Itin.CheckSum, only: [check_sum: 1]
use UkraineTaxidEx.BaseValidator
end

View File

@ -1,3 +1,5 @@
defmodule UkraineTaxidEx.EdrpouTest do
use ExUnit.Case
doctest UkraineTaxidEx.Edrpou.Validator
doctest UkraineTaxidEx.Itin.Validator
end

View File

@ -0,0 +1,95 @@
defmodule UkraineTaxidEx.Itin.CheckSumTest do
use ExUnit.Case, async: true
alias UkraineTaxidEx.Itin.CheckSum
describe "weights/0" do
test "returns the correct list of weights" do
expected_weights = [-1, 5, 7, 9, 4, 6, 10, 5, 7]
assert CheckSum.weights() == expected_weights
end
test "returns a list of 9 weights" do
assert length(CheckSum.weights()) == 9
end
end
describe "check_sum/2" do
test "calculates correct checksum for valid ITIN (3334513284)" do
# Test case with known ITIN and its checksum
digits = [3, 3, 3, 4, 5, 1, 3, 2, 8, 4]
assert CheckSum.check_sum(digits) == 4
end
test "calculates correct checksum for valid ITIN (2038817850)" do
# Test case with known ITIN and its checksum
digits = [2, 0, 3, 8, 8, 1, 7, 8, 5, 0]
assert CheckSum.check_sum(digits) == 0
end
test "calculates correct checksum for valid ITIN (3486110380)" do
# Test case with known ITIN and its checksum
digits = [3, 4, 8, 6, 1, 1, 0, 3, 8, 0]
assert CheckSum.check_sum(digits) == 0
end
test "calculates correct checksum for valid ITIN (3402109517)" do
# Test case with known ITIN and its checksum
digits = [3, 4, 0, 2, 1, 0, 9, 5, 1, 7]
assert CheckSum.check_sum(digits) == 7
end
test "calculates correct checksum for valid ITIN (2598917292)" do
# Test case with known ITIN and its checksum
digits = [2, 5, 9, 8, 9, 1, 7, 2, 9, 2]
assert CheckSum.check_sum(digits) == 2
end
test "calculates correct checksum for valid ITIN (3505804094)" do
# Test case with known ITIN and its checksum
digits = [3, 5, 0, 5, 8, 0, 4, 0, 9, 4]
assert CheckSum.check_sum(digits) == 4
end
test "calculates correct checksum for valid ITIN (2971306745)" do
# Test case with known ITIN and its checksum
digits = [2, 9, 7, 1, 3, 0, 6, 7, 4, 5]
assert CheckSum.check_sum(digits) == 5
end
test "calculates correct checksum for ITIN with invalid check digit (2598917291)" do
# Test case with known ITIN and its checksum
digits = [2, 5, 9, 8, 9, 1, 7, 2, 9, 1]
assert CheckSum.check_sum(digits) == 2
end
test "calculates correct checksum for ITIN with invalid check digit (3505804099)" do
# Test case with known ITIN and its checksum
digits = [3, 5, 0, 5, 8, 0, 4, 0, 9, 9]
assert CheckSum.check_sum(digits) == 4
end
test "calculates correct checksum for ITIN with invalid check digit (2971306747)" do
# Test case with known ITIN and its checksum
digits = [2, 9, 7, 1, 3, 0, 6, 7, 4, 7]
assert CheckSum.check_sum(digits) == 5
end
test "calculates checksum with custom weights" do
digits = [1, 2, 3]
weights = [1, 2, 3]
assert CheckSum.check_sum(digits, weights) == 5
end
test "handles zero digits" do
digits = [0, 0, 0, 0, 0, 0, 0, 0, 0]
assert CheckSum.check_sum(digits) == 0
end
test "handles single digit input" do
digits = [5]
weights = [2]
# (5*2) = 10 % 11 % 10 = 0
assert CheckSum.check_sum(digits, weights) == 0
end
end
end

View File

@ -0,0 +1,87 @@
defmodule UkraineTaxidEx.Itin.ParserTest do
use ExUnit.Case, async: true
alias UkraineTaxidEx.Itin
alias UkraineTaxidEx.Itin.Parser
describe "parse/1" do
test "successfully parses valid ITIN" do
assert {:ok, %Itin{} = itin} = Parser.parse("2847211760")
assert itin.code == "2847211760"
assert itin.birth_date == ~D[1977-12-14]
assert itin.number == 1176
assert itin.gender == 0
assert itin.check_sum == 0
assert itin.check_digit == 0
assert {:ok, %Itin{} = itin} = Parser.parse("3176422893")
assert itin.code == "3176422893"
assert itin.birth_date == ~D[1986-12-19]
assert itin.number == 2289
assert itin.gender == 1
assert itin.check_sum == 3
assert itin.check_digit == 3
end
test "returns error for string shorter than required length" do
assert {:error, :length_too_short} = Parser.parse("123456")
end
test "returns error for string longer than required length" do
assert {:error, :length_too_long} = Parser.parse("12345678901")
end
test "returns error for invalid checksum" do
assert {:error, :invalid_checksum} = Parser.parse("1234567890")
end
end
describe "birth_date/1" do
test "calculates correct birth date from digits" do
digits = [3, 7, 5, 8, 0, 0, 5, 3, 6, 4]
assert Parser.birth_date(digits) == ~D[2002-11-21]
digits = [3, 0, 4, 3, 2, 1, 6, 8, 2, 4]
assert Parser.birth_date(digits) == ~D[1983-04-27]
digits = [2, 8, 8, 1, 2, 2, 0, 4, 5, 2]
assert Parser.birth_date(digits) == ~D[1978-11-19]
end
end
describe "number/1" do
test "extracts correct number from digits" do
digits = [2, 8, 8, 1, 2, 2, 0, 4, 5, 2]
assert Parser.number(digits) == 2045
digits = [3, 5, 8, 9, 4, 1, 2, 3, 7, 1]
assert Parser.number(digits) == 1237
digits = [3, 2, 1, 5, 6, 0, 5, 0, 5, 3]
assert Parser.number(digits) == 505
end
end
describe "gender/1" do
test "determines correct gender from digits" do
# Even number in the second-to-last position (gender 1)
digits = [2, 5, 8, 5, 1, 1, 9, 1, 1, 5]
assert Parser.gender(digits) == 1
digits = [2, 6, 0, 3, 8, 2, 1, 5, 3, 1]
assert Parser.gender(digits) == 1
# Odd number in the second-to-last position (gender 0)
digits = [2, 2, 0, 7, 3, 1, 5, 4, 2, 8]
assert Parser.gender(digits) == 0
digits = [3, 1, 2, 6, 7, 0, 7, 0, 2, 6]
assert Parser.gender(digits) == 0
end
end
describe "base_date/0" do
test "returns the correct base date" do
assert Parser.base_date() == ~D[1899-12-31]
end
end
end