From a13d53d099fa367ede1eea5157aeaa4b8e45550f Mon Sep 17 00:00:00 2001 From: Jovial Joe Jayarson Date: Wed, 3 Apr 2024 15:59:59 +0530 Subject: [PATCH] feat: adds `finance` validator > co-authored by: @rcorbish --- docs/api/finance.md | 5 ++ docs/api/finance.rst | 7 ++ mkdocs.yaml | 1 + src/validators/__init__.py | 5 ++ src/validators/finance.py | 139 +++++++++++++++++++++++++++++++++++++ tests/test_finance.py | 51 ++++++++++++++ 6 files changed, 208 insertions(+) create mode 100644 docs/api/finance.md create mode 100644 docs/api/finance.rst create mode 100644 src/validators/finance.py create mode 100644 tests/test_finance.py diff --git a/docs/api/finance.md b/docs/api/finance.md new file mode 100644 index 00000000..d197caeb --- /dev/null +++ b/docs/api/finance.md @@ -0,0 +1,5 @@ +# finance + +::: validators.finance.cusip +::: validators.finance.isin +::: validators.finance.sedol diff --git a/docs/api/finance.rst b/docs/api/finance.rst new file mode 100644 index 00000000..9a84254f --- /dev/null +++ b/docs/api/finance.rst @@ -0,0 +1,7 @@ +finance +------- + +.. module:: validators.finance +.. autofunction:: cusip +.. autofunction:: isin +.. autofunction:: sedol diff --git a/mkdocs.yaml b/mkdocs.yaml index 1770c3cb..49f1cf3d 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -77,6 +77,7 @@ nav: - api/domain.md - api/email.md - api/encoding.md + - api/finance.md - api/hashes.md - api/hostname.md - api/i18n.md diff --git a/src/validators/__init__.py b/src/validators/__init__.py index 932098cd..e8817c96 100644 --- a/src/validators/__init__.py +++ b/src/validators/__init__.py @@ -9,6 +9,7 @@ from .domain import domain from .email import email from .encoding import base58, base64 +from .finance import cusip, isin, sedol from .hashes import md5, sha1, sha224, sha256, sha512 from .hostname import hostname from .i18n import ( @@ -61,6 +62,10 @@ # encodings "base58", "base64", + # finance + "cusip", + "isin", + "sedol", # hashes "md5", "sha1", diff --git a/src/validators/finance.py b/src/validators/finance.py new file mode 100644 index 00000000..593aab9d --- /dev/null +++ b/src/validators/finance.py @@ -0,0 +1,139 @@ +"""Finance.""" + +from .utils import validator + + +def _cusip_checksum(cusip: str): + check, val = 0, None + + for idx in range(9): + c = cusip[idx] + if c >= "0" and c <= "9": + val = ord(c) - ord("0") + elif c >= "A" and c <= "Z": + val = 10 + ord(c) - ord("A") + elif c >= "a" and c <= "z": + val = 10 + ord(c) - ord("a") + elif c == "*": + val = 36 + elif c == "@": + val = 37 + elif c == "#": + val = 38 + else: + return False + + if idx & 1: + val += val + + check = check + (val // 10) + (val % 10) + + return (check % 10) == 0 + + +def _isin_checksum(value: str): + check, val = 0, None + + for idx in range(12): + c = value[idx] + if c >= "0" and c <= "9" and idx > 1: + val = ord(c) - ord("0") + elif c >= "A" and c <= "Z": + val = 10 + ord(c) - ord("A") + elif c >= "a" and c <= "z": + val = 10 + ord(c) - ord("a") + else: + return False + + if idx & 1: + val += val + + return (check % 10) == 0 + + +@validator +def cusip(value: str): + """Return whether or not given value is a valid CUSIP. + + Checks if the value is a valid [CUSIP][1]. + [1]: https://en.wikipedia.org/wiki/CUSIP + + Examples: + >>> cusip('037833DP2') + True + >>> cusip('037833DP3') + ValidationFailure(func=cusip, ...) + + Args: + value: CUSIP string to validate. + + Returns: + (Literal[True]): If `value` is a valid CUSIP string. + (ValidationError): If `value` is an invalid CUSIP string. + """ + return len(value) == 9 and _cusip_checksum(value) + + +@validator +def isin(value: str): + """Return whether or not given value is a valid ISIN. + + Checks if the value is a valid [ISIN][1]. + [1]: https://en.wikipedia.org/wiki/International_Securities_Identification_Number + + Examples: + >>> isin('037833DP2') + True + >>> isin('037833DP3') + ValidationFailure(func=isin, ...) + + Args: + value: ISIN string to validate. + + Returns: + (Literal[True]): If `value` is a valid ISIN string. + (ValidationError): If `value` is an invalid ISIN string. + """ + return len(value) == 12 and _isin_checksum(value) + + +@validator +def sedol(value: str): + """Return whether or not given value is a valid SEDOL. + + Checks if the value is a valid [SEDOL][1]. + [1]: https://en.wikipedia.org/wiki/SEDOL + + Examples: + >>> sedol('2936921') + True + >>> sedol('29A6922') + ValidationFailure(func=sedol, ...) + + Args: + value: SEDOL string to validate. + + Returns: + (Literal[True]): If `value` is a valid SEDOL string. + (ValidationError): If `value` is an invalid SEDOL string. + """ + if len(value) != 7: + return False + + weights = [1, 3, 1, 7, 3, 9, 1] + check = 0 + for idx in range(7): + c = value[idx] + if c in "AEIOU": + return False + + val = None + if c >= "0" and c <= "9": + val = ord(c) - ord("0") + elif c >= "A" and c <= "Z": + val = 10 + ord(c) - ord("A") + else: + return False + check += val * weights[idx] + + return (check % 10) == 0 diff --git a/tests/test_finance.py b/tests/test_finance.py new file mode 100644 index 00000000..7beff7fc --- /dev/null +++ b/tests/test_finance.py @@ -0,0 +1,51 @@ +"""Test Finance.""" + +# external +import pytest + +# local +from validators import ValidationError, cusip, isin, sedol + +# ==> CUSIP <== # + + +@pytest.mark.parametrize("value", ["912796X38", "912796X20", "912796x20"]) +def test_returns_true_on_valid_cusip(value: str): + """Test returns true on valid cusip.""" + assert cusip(value) + + +@pytest.mark.parametrize("value", ["912796T67", "912796T68", "XCVF", "00^^^1234"]) +def test_returns_failed_validation_on_invalid_cusip(value: str): + """Test returns failed validation on invalid cusip.""" + assert isinstance(cusip(value), ValidationError) + + +# ==> ISIN <== # + + +@pytest.mark.parametrize("value", ["US0004026250", "JP000K0VF054", "US0378331005"]) +def test_returns_true_on_valid_isin(value: str): + """Test returns true on valid isin.""" + assert isin(value) + + +@pytest.mark.parametrize("value", ["010378331005" "XCVF", "00^^^1234", "A000009"]) +def test_returns_failed_validation_on_invalid_isin(value: str): + """Test returns failed validation on invalid isin.""" + assert isinstance(isin(value), ValidationError) + + +# ==> SEDOL <== # + + +@pytest.mark.parametrize("value", ["0263494", "0540528", "B000009"]) +def test_returns_true_on_valid_sedol(value: str): + """Test returns true on valid sedol.""" + assert sedol(value) + + +@pytest.mark.parametrize("value", ["0540526", "XCVF", "00^^^1234", "A000009"]) +def test_returns_failed_validation_on_invalid_sedol(value: str): + """Test returns failed validation on invalid sedol.""" + assert isinstance(sedol(value), ValidationError)