diff --git a/src/validators/finance.py b/src/validators/finance.py index 9df5a970..e713d966 100644 --- a/src/validators/finance.py +++ b/src/validators/finance.py @@ -32,22 +32,30 @@ def _cusip_checksum(cusip: str): 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") + # Expand letters to two digits (A=10 .. Z=35), then apply Luhn from the right. + # ISO 6166: check digit (last char) must be decimal 0-9. + if len(value) != 12 or not value[-1].isdigit(): + return False + digits = [] + for idx, c in enumerate(value): + if "0" <= c <= "9": + if idx < 2: + return False + digits.append(ord(c) - ord("0")) + elif "A" <= c <= "Z": + n = 10 + ord(c) - ord("A") + digits.extend((n // 10, n % 10)) + elif "a" <= c <= "z": + n = 10 + ord(c) - ord("a") + digits.extend((n // 10, n % 10)) else: return False - if idx & 1: - val += val - + check = 0 + for i, d in enumerate(reversed(digits)): + if i & 1: + d *= 2 + check += d // 10 + d % 10 return (check % 10) == 0 @@ -82,10 +90,10 @@ def isin(value: str): [1]: https://en.wikipedia.org/wiki/International_Securities_Identification_Number Examples: - >>> isin('037833DP2') - ValidationError(func=isin, args={'value': '037833DP2'}) - >>> isin('037833DP3') - ValidationError(func=isin, args={'value': '037833DP3'}) + >>> isin('US0378331005') + True + >>> isin('US0378331006') + ValidationError(func=isin, args={'value': 'US0378331006'}) Args: value: ISIN string to validate. diff --git a/tests/test_finance.py b/tests/test_finance.py index a40fd333..e0a9bfc3 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -24,13 +24,30 @@ def test_returns_failed_validation_on_invalid_cusip(value: str): # ==> ISIN <== # -@pytest.mark.parametrize("value", ["US0004026250", "JP000K0VF054", "US0378331005"]) +@pytest.mark.parametrize( + "value", + ["US0004026250", "US0378331005", "US5949181045", "GB0002634946"], +) 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"]) +@pytest.mark.parametrize( + "value", + [ + "010378331005", + "XCVF", + "00^^^1234", + "A000009", + # well-formed length/charset but wrong check digit (checksum was a no-op) + "US0378331006", + "JP000K0VF054", + "AAAAAAAAAAAA", + # letter check digit can Luhn-pass; ISO 6166 requires a digit + "AA000000000G", + ], +) def test_returns_failed_validation_on_invalid_isin(value: str): """Test returns failed validation on invalid isin.""" assert isinstance(isin(value), ValidationError)