Skip to content

Commit 2078614

Browse files
fix(finance): isin() actually validates the check digit
_isin_checksum never accumulated into `check` (the accumulation line present in _cusip_checksum was missing), and the per-character loop it copied is not the ISIN scheme anyway. `check` stayed 0, so isin() returned True for every 12-character string -- e.g. isin('US0378331004') (wrong check digit) and isin('XX0000000000'). Reimplement with the documented ISIN algorithm: expand letters to digits (A=10..Z=35) and run Luhn over the result. Verified against real ISINs (US0378331005, AU0000XVGZA3, GB0002634946). The existing 'valid' fixture 'JP000K0VF054' was itself invalid (its correct check digit is 5) -- corrected to 'JP000K0VF055'. Added a wrong-check-digit case to the invalid fixtures and a valid example to the docstring.
1 parent 70de324 commit 2078614

2 files changed

Lines changed: 20 additions & 14 deletions

File tree

src/validators/finance.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,27 @@ def _cusip_checksum(cusip: str):
3232

3333

3434
def _isin_checksum(value: str):
35-
check, val = 0, None
35+
if not (value[:2].isalpha() and value[-1].isdigit()):
36+
return False
3637

37-
for idx in range(12):
38-
c = value[idx]
39-
if c >= "0" and c <= "9" and idx > 1:
40-
val = ord(c) - ord("0")
41-
elif c >= "A" and c <= "Z":
42-
val = 10 + ord(c) - ord("A")
43-
elif c >= "a" and c <= "z":
44-
val = 10 + ord(c) - ord("a")
38+
# Expand letters to numbers (A=10, ..., Z=35), then run the Luhn algorithm.
39+
digits = ""
40+
for char in value:
41+
if char.isdigit():
42+
digits += char
43+
elif char.isalpha():
44+
digits += str(10 + ord(char.upper()) - ord("A"))
4545
else:
4646
return False
4747

48+
check = 0
49+
for idx, digit in enumerate(reversed(digits)):
50+
val = int(digit)
4851
if idx & 1:
49-
val += val
52+
val *= 2
53+
if val > 9:
54+
val -= 9
55+
check += val
5056

5157
return (check % 10) == 0
5258

@@ -82,8 +88,8 @@ def isin(value: str):
8288
[1]: https://en.wikipedia.org/wiki/International_Securities_Identification_Number
8389
8490
Examples:
85-
>>> isin('037833DP2')
86-
ValidationError(func=isin, args={'value': '037833DP2'})
91+
>>> isin('US0378331005')
92+
True
8793
>>> isin('037833DP3')
8894
ValidationError(func=isin, args={'value': '037833DP3'})
8995

tests/test_finance.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ def test_returns_failed_validation_on_invalid_cusip(value: str):
2424
# ==> ISIN <== #
2525

2626

27-
@pytest.mark.parametrize("value", ["US0004026250", "JP000K0VF054", "US0378331005"])
27+
@pytest.mark.parametrize("value", ["US0004026250", "JP000K0VF055", "US0378331005"])
2828
def test_returns_true_on_valid_isin(value: str):
2929
"""Test returns true on valid isin."""
3030
assert isin(value)
3131

3232

33-
@pytest.mark.parametrize("value", ["010378331005", "XCVF", "00^^^1234", "A000009"])
33+
@pytest.mark.parametrize("value", ["010378331005", "XCVF", "00^^^1234", "A000009", "US0378331004"])
3434
def test_returns_failed_validation_on_invalid_isin(value: str):
3535
"""Test returns failed validation on invalid isin."""
3636
assert isinstance(isin(value), ValidationError)

0 commit comments

Comments
 (0)