Skip to content

Instantly share code, notes, and snippets.

@pmarreck
Last active April 8, 2024 22:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pmarreck/643a2b1a7dcddf104ca68a7e166ff198 to your computer and use it in GitHub Desktop.
Save pmarreck/643a2b1a7dcddf104ca68a7e166ff198 to your computer and use it in GitHub Desktop.
CUSIP Validator in Elixir
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