Based on examples from others, here is a C # implementation that will check both ISIN and CUSIP (and possibly some other Luhn options).
Using:
foreach (var isin in ValidIsins) { var calculatedChecksum = SecuritiesValidation.CalculateChecksum(isin.Substring(0, 11)); var actualChecksum = (isin.Last() - '0'); Assert.AreEqual(calculatedChecksum, actualChecksum); } foreach (var cusip in ValidCusips) { var calculatedChecksum = SecuritiesValidation.CalculateChecksum(cusip.Substring(0, 8), true, true); var actualChecksum = (cusip.Last() - '0'); Assert.AreEqual(calculatedChecksum, actualChecksum); }
Implementation:
public static class SecuritiesValidation { public static int CalculateChecksum(IEnumerable<char> codeWithoutChecksum, bool reverseLuhn = false, bool allowSymbols = false) { return reverseLuhn ? codeWithoutChecksum .Select((c, i) => c.OrdinalPosition(allowSymbols).ConditionalMultiplyByTwo(i.IsOdd()).SumDigits()) .Sum() .TensComplement() : codeWithoutChecksum .ToArray() .ToDigits(allowSymbols) .Select((d, i) => d.ConditionalMultiplyByTwo(i.IsEven()).SumDigits()) .Sum() .TensComplement(); } public static bool IsChecksumCorrect(string code, bool reverseLuhn = false, bool allowSymbols = false) { try { var checksum = code.Last().ToInt(); return checksum == CalculateChecksum(code.Take(code.Length - 1), reverseLuhn, allowSymbols); } catch { return false; } } private static int OrdinalPosition(this char c, bool allowSymbols = false) { if (char.IsLower(c)) return char.ToUpper(c) - 'A' + 10; if (char.IsUpper(c)) return c - 'A' + 10; if (char.IsDigit(c)) return c.ToInt(); if (allowSymbols) switch (c) { case '*': return 36; case '@': return 37; case '#': return 38; } throw new ArgumentOutOfRangeException("Specified character is not a letter, digit or allowed symbol."); } private static bool IsEven(this int x) { return (x % 2 == 0); } private static bool IsOdd(this int x) { return !IsEven(x); } private static int ToInt(this char digit) { if (char.IsDigit(digit)) return digit - '0'; throw new ArgumentOutOfRangeException("Specified character is not a digit."); } private static IEnumerable<int> ToDigits(this char[] s, bool allowSymbols = false) { var digits = new List<int>(); for (var i = s.Length - 1; i >= 0; i--) { var ordinalPosition = s[i].OrdinalPosition(allowSymbols); digits.Add(ordinalPosition % 10); if (ordinalPosition > 9) digits.Add(ordinalPosition / 10); } return digits; } private static int SumDigits(this int value) {
It probably makes sense to use a checksum check in conjunction with a regular expression pattern match. This is the regex that I use:
ISIN: ^(XS|AD|AE|AF|AG|AI|AL|AM|AO|AQ|AR|AS|AT|AU|AW|AX|AZ|BA|BB|BD|BE|BF|BG|BH|BI|BJ|BL|BM|BN|BO|BQ|BR|BS|BT|BV|BW|BY|BZ|CA|CC|CD|CF|CG|CH|CI|CK|CL|CM|CN|CO|CR|CU|CV|CW|CX|CY|CZ|DE|DJ|DK|DM|DO|DZ|EC|EE|EG|EH|ER|ES|ET|FI|FJ|FK|FM|FO|FR|GA|GB|GD|GE|GF|GG|GH|GI|GL|GM|GN|GP|GQ|GR|GS|GT|GU|GW|GY|HK|HM|HN|HR|HT|HU|ID|IE|IL|IM|IN|IO|IQ|IR|IS|IT|JE|JM|JO|JP|KE|KG|KH|KI|KM|KN|KP|KR|KW|KY|KZ|LA|LB|LC|LI|LK|LR|LS|LT|LU|LV|LY|MA|MC|MD|ME|MF|MG|MH|MK|ML|MM|MN|MO|MP|MQ|MR|MS|MT|MU|MV|MW|MX|MY|MZ|NA|NC|NE|NF|NG|NI|NL|NO|NP|NR|NU|NZ|OM|PA|PE|PF|PG|PH|PK|PL|PM|PN|PR|PS|PT|PW|PY|QA|RE|RO|RS|RU|RW|SA|SB|SC|SD|SE|SG|SH|SI|SJ|SK|SL|SM|SN|SO|SR|SS|ST|SV|SX|SY|SZ|TC|TD|TF|TG|TH|TJ|TK|TL|TM|TN|TO|TR|TT|TV|TW|TZ|UA|UG|UM|US|UY|UZ|VA|VC|VE|VG|VI|VN|VU|WF|WS|YE|YT|ZA|ZM|ZW)([0-9A-Z]{9})([0-9]{1})$
CUSIP: ^[A-Z0-9]{8}[0-9]$
source share