Last active
April 8, 2024 22:56
-
-
Save pmarreck/643a2b1a7dcddf104ca68a7e166ff198 to your computer and use it in GitHub Desktop.
CUSIP Validator in Elixir
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule CUSIP do | |
@spec valid?(String.t) :: boolean | |
def valid?(cusip) when is_binary(cusip) do | |
validate_format(cusip) && validate_checksum(cusip) | |
end | |
@spec validate_format(String.t) :: boolean | |
def validate_format(cusip) when is_binary(cusip) do | |
Regex.match?(~r/^[a-zA-Z0-9]{5}[a-zA-Z0-9\*@\#]{3}[0-9]?$/, cusip) | |
end | |
def generate_random_valid_cusip() do | |
# 5 alphanumeric characters | |
alnum_character_set = Enum.to_list(?0..?9) ++ Enum.to_list(?A..?Z) ++ Enum.to_list(?a..?z) | |
# 3 alphanumeric characters or 3 special characters | |
alnum_plus_special_character_set = alnum_character_set ++ [?*, ?@, ?#] | |
random_cusip = Enum.map(1..5, fn _ -> Enum.random(alnum_character_set) end) ++ Enum.map(1..3, fn _ -> Enum.random(alnum_plus_special_character_set) end) | |
random_cusip_binary = to_string(random_cusip) | |
# A checksum digit | |
checksum = compute_checksum(random_cusip_binary) | |
random_cusip_binary <> to_string(checksum) | |
end | |
def compute_checksum(<<_::binary-size(8)>> = cusip) do | |
checksum = | |
cusip | |
|> String.slice(0..-2) | |
|> String.reverse() | |
|> String.codepoints() | |
|> Enum.with_index() | |
|> Enum.map(fn {char, index} -> | |
value = char_value(char) | |
if rem(index, 2) == 0, do: double_and_split(value), else: value | |
end) | |
|> Enum.sum() | |
checksum = 10 - rem(checksum, 10) | |
if checksum == 10, do: 0, else: checksum | |
end | |
@spec validate_checksum(String.t) :: boolean | |
# missing checksum is invalid | |
def validate_checksum(<<_::binary-size(8)>>), do: false | |
def validate_checksum(<<cusip::binary-size(9)>>) do | |
expected_checksum = compute_checksum(String.slice(cusip, 0..-2)) | |
actual_checksum = | |
cusip | |
|> String.at(-1) | |
|> char_value() | |
expected_checksum == actual_checksum | |
end | |
defp char_value(<<char::utf8>>), do: char_value([char]) | |
defp char_value([char]) when char in ?0..?9, do: char - 48 | |
defp char_value([char]) when char in ?A..?Z, do: char - 55 | |
defp char_value([char]) when char in ?a..?z, do: char - 87 | |
defp char_value([?*]), do: 36 | |
defp char_value([?@]), do: 37 | |
defp char_value([?#]), do: 38 | |
defp double_and_split(value) do | |
doubled = value * 2 | |
if doubled >= 10, do: div(doubled, 10) + rem(doubled, 10), else: doubled | |
end | |
end | |
# Tests | |
[AAPL: "037833100", GOOG: "02079K107", ALK: "011659109", WMT: "931142103"] | |
|> Enum.each(fn {symbol, cusip} -> | |
unless CUSIP.valid?(cusip), do: raise "CUSIP for #{symbol} failed" | |
end) | |
[AAPL_bad_checksum: "037833105", GOOG_missing_checksum: "02079K10", ALK_bad_char: "0116%9109", WMT_missing_char: "93142103"] | |
|> Enum.each(fn {symbol, cusip} -> | |
if CUSIP.valid?(cusip), do: raise "CUSIP expected fail for #{symbol} failed" | |
end) | |
Enum.each(1..10, fn _ -> | |
rand_cusip = CUSIP.generate_random_valid_cusip() | |
unless CUSIP.valid?(rand_cusip), do: raise "randomized CUSIP invalid: #{rand_cusip}" | |
end) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment