Validate International Securities Identification Number
You are encouraged to solve this task according to the task description, using any language you may know.
An International Securities Identification Number (ISIN) is a unique international identifier for a financial security such as a stock or bond.
Write a function or program that takes a string as input, and checks whether it is a valid ISIN.
It is only valid if it has the correct format, and the embedded checksum is correct.
Demonstrate that your code passes the test-cases listed below.
The format of an ISIN is as follows:
For this task, you may assume that any 2-character alphabetic sequence is a valid country code.
The checksum can be validated as follows:
- Replace letters with digits, by converting each character from base 36 to base 10, e.g.
AU0000XVGZA3
→1030000033311635103
. - Perform the Luhn test on this base-10 number.
There is a separate task for this test: Luhn test of credit card numbers.
You don't have to replicate the implementation of this test here – you can just call the existing function from that task. (Add a comment stating if you did this.)
ISIN | Validity | Comment |
---|---|---|
US0378331005 | valid | |
US0373831005 | not valid | The transposition typo is caught by the checksum constraint. |
U50378331005 | not valid | The substitution typo is caught by the format constraint. |
US03378331005 | not valid | The duplication typo is caught by the format constraint. |
AU0000XVGZA3 | valid | |
AU0000VXGZA3 | valid | Unfortunately, not all transposition typos are caught by the checksum constraint. |
FR0000988040 | valid |
(The comments are just informational. Your function should simply return a Boolean result. See #Perl_6 for a reference solution.)
Useful resources:
- Interactive online ISIN validator
- Wikipedia article: International Securities Identification Number
Related tasks:
Ada
Calling the existing Luhn algorithm implementation from the Luhn test of credit card numbers task.
<lang Ada>procedure ISIN is
-- Luhn_Test copied from other Task function Luhn_Test (Number: String) return Boolean is Sum : Natural := 0; Odd : Boolean := True; Digit: Natural range 0 .. 9; begin for p in reverse Number'Range loop Digit := Integer'Value (Number (p..p)); if Odd then Sum := Sum + Digit; else Sum := Sum + (Digit*2 mod 10) + (Digit / 5); end if; Odd := not Odd; end loop; return (Sum mod 10) = 0; end Luhn_Test; subtype Decimal is Character range '0' .. '9'; subtype Letter is Character range 'A' .. 'Z'; subtype ISIN_Type is String(1..12); -- converts a string of decimals and letters into a string of decimals function To_Digits(S: String) return String is -- Character'Pos('A')-Offset=10, Character'Pos('B')-Offset=11, ... Offset: constant Integer := Character'Pos('A')-10; Invalid_Character: exception; begin if S = "" then return ""; elsif S(S'First) = ' ' then -- skip blanks return To_Digits(S(S'First+1 .. S'Last)); elsif S(S'First) in Decimal then return S(S'First) & To_Digits(S(S'First+1 .. S'Last)); elsif S(S'First) in Letter then return To_Digits(Integer'Image(Character'Pos(S(S'First))-Offset)) & To_Digits(S(S'First+1 .. S'Last)); else raise Invalid_Character; end if; end To_Digits; function Is_Valid_ISIN(S: ISIN_Type) return Boolean is Number : String := To_Digits(S); begin return S(S'First) in Letter and S(S'First+1) in Letter and S(S'Last) in Decimal and Luhn_Test(Number); end Is_Valid_ISIN;
Test_Cases : constant Array(1..6) of ISIN_Type := ("US0378331005", "US0373831005", "U50378331005", -- excluded by type with fixed length -- "US03378331005", "AU0000XVGZA3", "AU0000VXGZA3", "FR0000988040");
begin
for I in Test_Cases'Range loop Ada.Text_IO.Put_Line(Test_Cases(I) & ":" & Boolean'Image(Is_Valid_ISIN(Test_Cases(I)))); end loop; -- using wrong length will result in an exception: Ada.Text_IO.Put("US03378331005:"); Ada.Text_IO.Put_Line(Boolean'Image(Is_Valid_Isin("US03378331005")));
exception
when others => Ada.Text_IO.Put_Line("Exception occured");
end ISIN;</lang>
Output:
US0378331005:TRUE US0373831005:FALSE U50378331005:FALSE AU0000XVGZA3:TRUE AU0000VXGZA3:TRUE FR0000988040:TRUE US03378331005:Exception occured
C
<lang c>#include <stdio.h>
int check_isin(char *a) {
int i, j, k, v, s[24]; j = 0; for(i = 0; i < 12; i++) { k = a[i]; if(k >= '0' && k <= '9') { if(i < 2) return 0; s[j++] = k - '0'; } else if(k >= 'A' && k <= 'Z') { if(i == 11) return 0; k -= 'A' - 10; s[j++] = k / 10; s[j++] = k % 10; } else { return 0; } } if(a[i]) return 0; v = 0; for(i = j - 2; i >= 0; i -= 2) { k = 2 * s[i]; v += k > 9 ? k - 9 : k; } for(i = j - 1; i >= 0; i -= 2) { v += s[i]; } return v % 10 == 0;
}
int main() {
char *test[7] = {"US0378331005", "US0373831005", "U50378331005", "US03378331005", "AU0000XVGZA3", "AU0000VXGZA3", "FR0000988040"}; int i; for(i = 0; i < 7; i++) printf("%c%c", check_isin(test[i]) ? 'T' : 'F', i == 6 ? '\n' : ' '); return 0;
}
/* will print: T F F F T T T */</lang>
Elixir
used Luhn module from here <lang elixir>isin? = fn str ->
if str =~ ~r/\A[A-Z]{2}[A-Z0-9]{9}\d\z/ do String.codepoints(str) |> Enum.map_join(&String.to_integer(&1, 36)) |> Luhn.valid? else false end end
IO.puts " ISIN Valid?" ~w(US0378331005
US0373831005 U50378331005 US03378331005 AU0000XVGZA3 AU0000VXGZA3 FR0000988040)
|> Enum.each(&IO.puts "#{&1}\t#{isin?.(&1)}")</lang>
- Output:
ISIN Valid? US0378331005 true US0373831005 false U50378331005 false US03378331005 false AU0000XVGZA3 true AU0000VXGZA3 true FR0000988040 true
Fortran
<lang fortran>program isin
use ctype implicit none character(20) :: test(7) = ["US0378331005 ", & "US0373831005 ", & "U50378331005 ", & "US03378331005 ", & "AU0000XVGZA3 ", & "AU0000VXGZA3 ", & "FR0000988040 "] print *, check_isin(test)
contains
elemental logical function check_isin(a) character(*), intent(in) :: a integer :: s(24) integer :: i, j, k, n, v
check_isin = .false.
n = len_trim(a) if (n /= 12) return ! Convert to an array of digits j = 0 do i = 1, n k = iachar(a(i:i)) if (k >= 48 .and. k <= 57) then if (i < 3) return k = k - 48 j = j + 1 s(j) = k else if (k >= 65 .and. k <= 90) then if (i == 12) return k = k - 65 + 10 j = j + 1 s(j) = k / 10 j = j + 1 s(j) = mod(k, 10) else return end if end do
! Compute checksum v = 0 do i = j - 1, 1, -2 k = 2 * s(i) if (k > 9) k = k - 9 v = v + k end do do i = j, 1, -2 v = v + s(i) end do check_isin = 0 == mod(v, 10) end function
end program</lang>
FreeBASIC
<lang freebasic>' version 27-10-2016 ' compile with: fbc -s console
- Ifndef TRUE ' define true and false for older freebasic versions
#Define FALSE 0 #Define TRUE Not FALSE
- EndIf
Function luhntest(cardnr As String) As Long
cardnr = Trim(cardnr) ' remove spaces
Dim As String reverse_nr = cardnr Dim As Long i, j, s1, s2, l = Len(cardnr) -1
' reverse string For i = 0 To l reverse_nr[i] = cardnr[l - i] Next ' sum odd numbers For i = 0 To l Step 2 s1 = s1 + (reverse_nr[i] - Asc("0")) Next ' sum even numbers For i = 1 To l Step 2 j = reverse_nr[i] - Asc("0") j = j * 2 If j > 9 Then j = j Mod 10 +1 s2 = s2 + j Next
If (s1 + s2) Mod 10 = 0 Then Return TRUE Else Return FALSE End If
End Function
' ------=< MAIN >=-----
Dim As String test_str Dim As String test_set(1 To ...) = { "US0378331005", "US0373831005", _
"U50378331005", "US03378331005", "AU0000XVGZA3", _ "AU0000VXGZA3", "FR0000988040" }
Dim As Long i, l, n, x
For i = 1 To UBound(test_set)
test_str = "" l = Len(test_set(i)) If l <> 12 Then Print test_set(i), "Invalid, length <> 12 char." Continue For End If If test_set(i)[0] < Asc("A") Or test_set(i)[1] < Asc("A") Then Print test_set(i), "Invalid, number needs to start with 2 characters" Continue For End If For n = 0 To l -1 x = test_set(i)[n] - Asc("0") ' if test_set(i)[i] is a letter we to correct for that If x > 9 Then x = x -7 If x < 10 Then test_str = test_str + Str(x) Else ' two digest number test_str = test_str + Str(x \ 10) + Str(x Mod 10) End If Next Print test_set(i), IIf(luhntest(test_str) = TRUE, "Valid","Invalid, checksum error")
Next
' empty keyboard buffer
While InKey <> "" : Wend
Print : Print "hit any key to end program"
Sleep
End</lang>
- Output:
US0378331005 Valid US0373831005 Invalid, checksum error U50378331005 Invalid, number needs to start with 2 characters US03378331005 Invalid, length <> 12 char. AU0000XVGZA3 Valid AU0000VXGZA3 Valid FR0000988040 Valid
Go
<lang go>package main
import "regexp"
var r = regexp.MustCompile(`^[A-Z]{2}[A-Z0-9]{9}\d$`)
var inc = [2][10]int{ {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, {0, 2, 4, 6, 8, 1, 3, 5, 7, 9}, }
func ValidISIN(n string) bool { if !r.MatchString(n) { return false } var sum, p int for i := 10; i >= 0; i-- { p = 1 - p if d := n[i]; d < 'A' { sum += inc[p][d-'0'] } else { d -= 'A' sum += inc[p][d%10] p = 1 - p sum += inc[p][d/10+1] } } sum += int(n[11] - '0') return sum%10 == 0 }</lang>
<lang go>package main
import "testing"
func TestValidISIN(t *testing.T) { testcases := []struct { isin string valid bool }{ {"US0378331005", true}, {"US0373831005", false}, {"U50378331005", false}, {"US03378331005", false}, {"AU0000XVGZA3", true}, {"AU0000VXGZA3", true}, {"FR0000988040", true}, }
for _, testcase := range testcases { actual := ValidISIN(testcase.isin) if actual != testcase.valid { t.Errorf("expected %v for %q, got %v", testcase.valid, testcase.isin, actual) } } }</lang>
Groovy
<lang groovy>CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
int checksum(String prefix) {
def digits = prefix.toUpperCase().collect { CHARS.indexOf(it).toString() }.sum() def groups = digits.collect { CHARS.indexOf(it) }.inject([[], []]) { acc, i -> [acc[1], acc[0] + i] } def ds = groups[1].collect { (2 * it).toString() }.sum().collect { CHARS.indexOf(it) } + groups[0] (10 - ds.sum() % 10) % 10
}
assert checksum('AU0000VXGZA') == 3 assert checksum('GB000263494') == 6 assert checksum('US037833100') == 5 assert checksum('US037833107') == 0</lang>
Haskell
<lang Haskell>module ISINVerification2
where
import Data.Char ( isUpper , isDigit , digitToInt )
verifyISIN :: String -> Bool verifyISIN isin = correctFormat isin && mod (oddsum + multiplied_even_sum) 10 == 0
where reverted = reverse $ convertToNumber isin theOdds = fst $ collectOddandEven reverted theEvens = snd $ collectOddandEven reverted oddsum = sum $ map digitToInt theOdds multiplied_even_sum = addUpDigits $ map ( (* 2 ) . digitToInt ) theEvens
capitalLetters :: [Char] capitalLetters = ['A' , 'B'..'Z']
numbers :: [Char] numbers = ['0' , '1' , '2', '3' , '4' , '5', '6' , '7' , '8' , '9' ]
correctFormat :: String -> Bool correctFormat isin = (length isin == 12 ) && ( all (\b -> elem b capitalLetters ) $ take 2 isin)
&& (all (\c -> elem c capitalLetters || elem c numbers) $ drop 2 $ take 11 isin)
&& (elem ( last isin ) numbers)
convertToNumber :: String -> String convertToNumber str = concat $ map convert str
where convert :: Char -> String convert c = if isDigit c then show $ digitToInt c else show ( fromEnum c - 55 )
collectOddandEven :: String -> (String , String ) collectOddandEven term
|odd $ length term = (concat [take 1 $ drop n term | n <- [0,2..length term - 1]] ,
concat [take 1 $ drop d term | d <- [1,3..length term - 2]] )
|otherwise = (concat [take 1 $ drop n term | n <- [0,2..length term -2]] ,
concat [take 1 $ drop d term | d <- [1,3..length term - 1]] )
addUpDigits :: [Int] -> Int
addUpDigits list = sum $ map (\d -> if d > 9 then sum $ map digitToInt $ show d else d ) list
printSolution :: String -> IO ( ) printSolution str = do
putStr $ str ++ " is" if verifyISIN str == True then putStrLn " valid" else putStrLn " not valid"
main :: IO ( ) main = do
let isinnumbers = ["US0378331005" , "US0373831005" , "US03378331005" , "AU0000XVGZA3" ,
"AU0000VXGZA3" , "FR0000988040"]
mapM_ printSolution isinnumbers</lang>
- Output:
US0378331005 is valid US0373831005 is not valid US03378331005 is not valid AU0000XVGZA3 is valid AU0000VXGZA3 is valid FR0000988040 is valid
J
<lang j>splt=: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' i. ' ' -.~ ": checksum=: 3 : '10| - +/ splt (* 2 1 $~ #) |. splt splt y'
assert 5 = checksum 'US037833100' assert 0 = checksum 'US037833107' assert 3 = checksum 'AU0000VXGZA' assert 6 = checksum 'GB000263494'</lang>
Java
This now assumes that the existing Luhn algorithm implementation from the Luhn test of credit card numbers task is available in the same (default) package.
<lang java>public class ISIN {
public static void main(String[] args) { String[] isins = { "US0378331005", "US0373831005", "U50378331005", "US03378331005", "AU0000XVGZA3", "AU0000VXGZA3", "FR0000988040", }; for (String isin : isins) System.out.printf("%s is %s%n\n", isin, ISINtest(isin) ? "valid" : "not valid"); }
static boolean ISINtest(String isin) { isin = isin.trim().toUpperCase();
if (!isin.matches("^[A-Z]{2}[A-Z0-9]{9}\\d$")) return false;
StringBuilder sb = new StringBuilder(); for (char c : isin.substring(0, 12).toCharArray()) sb.append(Character.digit(c, 36));
return Luhn.luhnTest(sb.toString()); }
}</lang>
US0378331005 is valid US0373831009 is valid D56000543287 is not valid AU0000XVGZA3 is valid AU0000VXGZA3 is valid GB0002634946 is valid US0373831005 is not valid
Kotlin
As the Luhn test method is only a few lines, it's reproduced here for convenience: <lang scala>// version 1.0.6
object Isin {
val r = Regex("^[A-Z]{2}[A-Z0-9]{9}[0-9]$")
fun isValid(s: String): Boolean { // check format if (!s.matches(r)) return false // validate checksum val sb = StringBuilder() for (c in s) when(c) { in '0'..'9' -> sb.append(c) in 'A'..'Z' -> sb.append((c.toInt() - 55).toString().padStart(2, '0')) } return luhn(sb.toString()) }
private fun luhn(s: String): Boolean { fun sumDigits(n : Int) = n / 10 + n % 10 val t = s.reversed() val s1 = t.filterIndexed { i, it -> i % 2 == 0 }.sumBy { it - '0' } val s2 = t.filterIndexed { i, it -> i % 2 == 1 }.map { sumDigits((it - '0') * 2) }.sum() return (s1 + s2) % 10 == 0 }
}
fun main(args: Array<String>) {
val isins = arrayOf("US0378331005", "US0373831005", "U50378331005", "US03378331005", "AU0000XVGZA3", "AU0000VXGZA3", "FR0000988040") for (isin in isins) println("$isin\t -> ${if (Isin.isValid(isin)) "valid" else "not valid"}")
}</lang>
- Output:
US0378331005 -> valid US0373831005 -> not valid U50378331005 -> not valid US03378331005 -> not valid AU0000XVGZA3 -> valid AU0000VXGZA3 -> valid FR0000988040 -> valid
Lua
<lang Lua>function luhn (n)
local revStr, s1, s2, digit, mod = n:reverse(), 0, 0 for pos = 1, #revStr do digit = tonumber(revStr:sub(pos, pos)) if pos % 2 == 1 then s1 = s1 + digit else digit = digit * 2 if digit > 9 then mod = digit % 10 digit = mod + ((digit - mod) / 10) end s2 = s2 + digit end end return (s1 + s2) % 10 == 0
end
function checkISIN (inStr)
if #inStr ~= 12 then return false end local numStr = "" for pos = 1, #inStr do numStr = numStr .. tonumber(inStr:sub(pos, pos), 36) end return luhn(numStr)
end
local testCases = {
"US0378331005", "US0373831005", "US0373831005", "US03378331005", "AU0000XVGZA3", "AU0000VXGZA3", "FR0000988040"
} for _, ISIN in pairs(testCases) do print(ISIN, checkISIN(ISIN)) end</lang>
- Output:
US0378331005 true US0373831005 false US0373831005 false US03378331005 false AU0000XVGZA3 true AU0000VXGZA3 true FR0000988040 true
Perl
We reuse the luhn_test() function from Luhn test of credit card numbers#Perl. <lang perl>use strict; use English; use POSIX; use Test::Simple tests => 7;
ok( validate_isin('US0378331005'), 'Test 1'); ok( ! validate_isin('US0373831005'), 'Test 2'); ok( ! validate_isin('U50378331005'), 'Test 3'); ok( ! validate_isin('US03378331005'), 'Test 4'); ok( validate_isin('AU0000XVGZA3'), 'Test 5'); ok( validate_isin('AU0000VXGZA3'), 'Test 6'); ok( validate_isin('FR0000988040'), 'Test 7'); exit 0;
sub validate_isin {
my $isin = shift; $isin =~ /\A[A-Z]{2}[A-Z\d]{9}\d\z/s or return 0; my $base10 = join(q{}, map {scalar(POSIX::strtol($ARG, 36))} split(//s, $isin)); return luhn_test($base10);
}</lang>
- Output:
1..7 ok 1 - Test 1 ok 2 - Test 2 ok 3 - Test 3 ok 4 - Test 4 ok 5 - Test 5 ok 6 - Test 6 ok 7 - Test 7
Perl 6
Using the luhn-test function defined at Luhn test of credit card numbers#Perl 6:
<lang perl6>my $ISIN = /
<[A..Z]>**2 <[A..Z0..9]>**9 <[0..9]> <?{ luhn-test $/.comb.map({ :36($_) }).join }>
/;</lang>
Testing:
<lang perl6>say "$_ is {$ISIN ?? "valid" !! "not valid"}" for < US0378331005 US0373831005 U50378331005 US03378331005 AU0000XVGZA3 AU0000VXGZA3 FR0000988040 >;</lang>
- Output:
US0378331005 is valid US0373831005 is not valid U50378331005 is not valid US03378331005 is not valid AU0000XVGZA3 is valid AU0000VXGZA3 is valid FR0000988040 is valid
PowerShell
<lang PowerShell> function Test-ISIN {
[CmdletBinding()] [OutputType([bool])] Param ( [Parameter(Mandatory=$true, Position=0)] [ValidatePattern("[A-Z]{2}\w{9}\d")] [ValidateScript({$_.Length -eq 12})] [string] $Number )
function Split-Array { $array = @(), @() $input | ForEach-Object {$array[($index = -not $index)] += $_} $array[1], $array[0] }
filter ConvertTo-Digit { if ($_ -gt 9) { $_.ToString().ToCharArray() | ForEach-Object -Begin {$n = 0} -Process {$n += [Char]::GetNumericValue($_)} -End {$n} } else { $_ } }
$checkDigit = $Number[-1]
$digits = ($Number -replace ".$").ToCharArray() | ForEach-Object { if ([Char]::IsDigit($_)) { [Char]::GetNumericValue($_) } else { [int][char]$_ - 55 } }
$odds, $evens = ($digits -join "").ToCharArray() | Split-Array
if ($odds.Count -gt $evens.Count) { $odds = $odds | ForEach-Object {[Char]::GetNumericValue($_) * 2} | ConvertTo-Digit $evens = $evens | ForEach-Object {[Char]::GetNumericValue($_)} } else { $odds = $odds | ForEach-Object {[Char]::GetNumericValue($_)} $evens = $evens | ForEach-Object {[Char]::GetNumericValue($_) * 2} | ConvertTo-Digit }
$sum = ($odds | Measure-Object -Sum).Sum + ($evens | Measure-Object -Sum).Sum
(10 - ($sum % 10)) % 10 -match $checkDigit
} </lang> <lang PowerShell> "US0378331005","US0373831005","US0337833103","AU0000XVGZA3","AU0000VXGZA3","FR0000988040" | ForEach-Object {
[PSCustomObject]@{ ISIN = $_ IsValid = Test-ISIN -Number $_ }
} </lang>
- Output:
ISIN IsValid ---- ------- US0378331005 True US0373831005 False US0337833103 False AU0000XVGZA3 True AU0000VXGZA3 True FR0000988040 True
Python
<lang python>def check_isin(a):
if len(a) != 12 or not all(c.isalpha() for c in a[:2]) or not all(c.isalnum() for c in a[2:]): return False s = "".join(str(int(c, 36)) for c in a) return 0 == (sum(sum(divmod(2 * (ord(c) - 48), 10)) for c in s[-2::-2]) + sum(ord(c) - 48 for c in s[::-2])) % 10
- A more readable version
def check_isin_alt(a):
if len(a) != 12: return False s = [] for i, c in enumerate(a): if c.isdigit(): if i < 2: return False s.append(ord(c) - 48) elif c.isupper(): if i == 11: return False s += divmod(ord(c) - 55, 10) else: return False v = sum(s[::-2]) for k in s[-2::-2]: k = 2 * k v += k - 9 if k > 9 else k return v % 10 == 0
[check_isin(s) for s in ["US0378331005", "US0373831005", "U50378331005", "US03378331005",
"AU0000XVGZA3", "AU0000VXGZA3", "FR0000988040"]]
- [True, False, False, False, True, True, True]</lang>
Racket
<lang racket>
- lang racket
- convert a base36 character (#\0 - #\Z) to its equivalent
- in base 10 as a string ("0" - "35")
(define (base36-char->base10-string c)
(let ([char-int (char->integer (char-upcase c))] [zero-int (char->integer #\0)] [nine-int (char->integer #\9)] [A-int (char->integer #\A)] [Z-int (char->integer #\Z)]) (cond [(and (>= char-int zero-int) (<= char-int nine-int)) (~a c)] [(and (>= char-int A-int) (<= char-int Z-int)) (~a (+ (- char-int A-int) 10))] [else null])))
- substitute equivalent base 10 numbers for base 36 characters in string
- this is a character-by-character substitution not a conversion
- of a base36 number to a base10 number
(define (base36-string-characters->base10-string-characters s)
(for/fold ([joined ""]) ([tenstr (map base36-char->base10-string (string->list (string-upcase s)))]) (values (string-append joined tenstr))))
- This uses the Racket Luhn solution
(define [isin-test? s]
(let ([RE (pregexp "^[A-Z]{2}[A-Z0-9]{9}[0-9]{1}$")]) (and (regexp-match? RE s) (luhn-test (string->number (base36-string-characters->base10-string-characters s))))))
(define test-cases '("US0378331005" "US0373831005" "U50378331005" "US03378331005" "AU0000XVGZA3" "AU0000VXGZA3" "FR0000988040"))
(map isin-test? test-cases)
- -> '(#t #f #f #f #t #t #t)
</lang>
- Output:
'(#t #f #f #f #t #t #t)
REXX
<lang rexx>/*REXX program validates the checksum digit for an International Securities ID number.*/ parse arg z /*obtain optional ISINs from the C.L.*/ if z= then z= "US0378331005 US0373831005 U50378331005 US03378331005 AU0000XVGZA3" ,
'AU0000VXGZA3 FR0000988040' /* [↑] use the default list of ISINs.*/ /* [↓] process all specified ISINs.*/ do n=1 for words(z); x=word(z, n); y=x /*obtain an ISIN from the Z list. */ $= /* [↓] construct list of ISIN digits. */ do k=1 for length(x); _=substr(x,k,1) /*the ISIN may contain alphabetic chars*/ p=pos(_, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') /*X must contain A──►Z, 0──►9.*/ if p==0 then y= /*trigger "not" valid below.*/ else $=$ || p-1 /*convert X string (base 36 ──► dec).*/ end /*k*/ /* [↑] convert alphabetic ──► digits.*/ @= /*placeholder for the "not" in message.*/ if length(y)\==12 then @= "not" /*check if the ISIN is exactly 12 chars*/ if \datatype( left(x,2),'U') then @= "not" /* " " " " 1st 2 chars cap let*/ if \datatype(right(x,1),'W') then @= "not" /* " " " " last char not digit*/ if @== then if \luhn($) then @= "not" /* " " " " passed Luhn test. */ say right(x,30) right(@, 5) "valid" /*display the yea or nay message.*/ end /*n*/ /* [↑] 1st 3 IFs could've been combined*/
exit /*stick a fork in it, we're all done. */ /*──────────────────────────────────────────────────────────────────────────────────────*/ Luhn: procedure; parse arg x; $=0 /*get credit card number; zero $ sum. */
y=reverse(left(0, length(x) // 2)x) /*add leading zero if needed, & reverse*/ do j=1 to length(y)-1 by 2; _=2 * substr(y, j+1, 1) $=$ + substr(y, j, 1) + left(_, 1) + substr(_, 2 , 1, 0) end /*j*/ /* [↑] sum the odd and even digits.*/ return right($, 1)==0 /*return "1" if number passed Luhn test*/</lang>
output when using the defaults for input:
US0378331005 valid US0373831005 not valid U50378331005 not valid US03378331005 not valid AU0000XVGZA3 valid AU0000VXGZA3 valid FR0000988040 valid
Ruby
Using a pre-existing luhn method: <lang ruby>RE = /\A[A-Z]{2}[A-Z0-9]{9}[0-9]{1}\z/
def valid_isin?(str)
return false unless str =~ RE luhn(str.chars.map{|c| c.to_i(36)}.join)
end
p %w(US0378331005 US0373831005 U50378331005 US03378331005 AU0000XVGZA3 AU0000VXGZA3 FR0000988040).map{|tc| valid_isin?(tc) }
- => [true, false, false, false, true, true, true]</lang>
SAS
<lang sas>data test; length isin $20 ok $1; input isin; keep isin ok; array s{24}; link isin; return; isin: ok="N"; n=length(isin); if n=12 then do;
j=0; do i=1 to n; k=rank(substr(isin,i,1)); if k>=48 & k<=57 then do; if i<3 then return; j+1; s{j}=k-48; end; else if k>=65 & k<=90 then do; if i=12 then return; k+-55; j+1; s{j}=int(k/10); j+1; s{j}=mod(k,10); end; else return; end;
v=sum(of s{*}); do i=j-1 to 1 by -2; v+s{i}-9*(s{i}>4); end;
if mod(v,10)=0 then ok="Y"; end; return; cards; US0378331005 US0373831005 U50378331005 US03378331005 AU0000XVGZA3 AU0000VXGZA3 FR0000988040
run;</lang>
Tcl
<lang Tcl>package require Tcl 8.6 ;# mostly needed for [assert]. Substitute a simpler one or a NOP if required.</lang>
A proc like assert is always good to have around. This one tries to report values used in its expression using subst:
<lang Tcl>proc assert {expr} { ;# for "static" assertions that throw nice errors
if {![uplevel 1 [list expr $expr]]} { set msg "{$expr}" catch {append msg " {[uplevel 1 [list subst -noc $expr]]}"} tailcall throw {ASSERT ERROR} $msg }
}</lang>
isin itself is a simple package. We compute the alphabet when the package is loaded in _init, because that's more fun than typing out the table:
<lang Tcl>namespace eval isin {
proc _init {} { ;# sets up the map used on every call variable map set alphabet abcdefghijklmnopqrstuvwxyz set n 9 lmap c [split $alphabet ""] { lappend map $c [incr n] } } _init
proc normalize {isin} { variable map string map $map [string tolower [string trim $isin]] }
# copied from "Luhn test of credit card numbers" # included here for ease of testing, and because it is short proc luhn digitString { if {[regexp {[^0-9]} $digitString]} {error "not a number"} set sum 0 set flip 1 foreach ch [lreverse [split $digitString {}]] { incr sum [lindex { {0 1 2 3 4 5 6 7 8 9} {0 2 4 6 8 1 3 5 7 9} } [expr {[incr flip] & 1}] $ch] } return [expr {($sum % 10) == 0}] }
proc validate {isin} { if {![regexp {^[A-Z]{2}[A-Z0-9]{9}[0-9]$} $isin]} {return false} luhn [normalize $isin] }
}</lang>
To run the test suite, we use the tcltest framework included with Tcl:
<lang Tcl>package require tcltest
tcltest::test isin-1 "Test isin validation" -body {
foreach {isin ok} { US0378331005 yes US0373831005 no U50378331005 no US03378331005 no AU0000XVGZA3 yes AU0000VXGZA3 yes FR0000988040 yes } { if {$ok} { assert {[isin::validate $isin]} } else { assert {![isin::validate $isin]} } } return ok
} -result ok</lang>
Visual Basic
<lang vb> Option Explicit
Function MakeIsinCode(Exchange As String, security As String)
Dim numLeadingZeroes As Integer numLeadingZeroes = 9 - Len(security) Dim leader As String leader = Exchange & String(numLeadingZeroes, "0") & security MakeIsinCode = leader & CStr(IsinCheckDigit(leader))
End Function
Function IsinCheckDigit(ByVal security As String) As Integer
Dim digits As String Dim i As Integer For i = 1 To Len(security) Dim ch As String ch = UCase(Mid(security, i, 1)) If ch >= "A" And ch <= "Z" Then ' A to Z translated to "10", "11", .. "35" digits = digits & CStr(Asc(ch) - 55) ElseIf ch >= "0" And ch <= "9" Then digits = digits & ch Else Err.Raise 50001, , "Security must contain only letters and digits" End If Next Dim total As Integer Dim tmp As Integer total = 0 'If rightmost even, "other" digits for doubling are 2,4,6. If rightmost odd, they're 1,3,5. 'rightmost digit is always doubled, so start with it and work backwards Dim other As Boolean other = True For i = Len(digits) To 1 Step -1 tmp = CInt(Mid(digits, i, 1)) If other Then If tmp < 5 Then ' 0 to 4 map to 0,2,4,6,8 total = total + (tmp * 2) Else ' 5 to 9 map to 1,3,5,7,9 total = total + ((tmp * 2) - 9) End If Else total = total + tmp End If 'Toggle doubling flag other = Not other Next 'Last Mod 10 is to wrap 10 to zero IsinCheckDigit = (10 - (total Mod 10)) Mod 10
End Function </lang>
zkl
Uses the luhn test from Luhn_test_of_credit_card_numbers#zkl (copied here as it is short). <lang zkl>fcn validateISIN(isin){
RegExp(String("^","[A-Z]"*2,"[A-Z0-9]"*9,"[0-9]$")).matches(isin) and luhnTest(isin.split("").apply("toInt",36).concat().toInt())
} fcn luhnTest(n){
0 == (n.split().reverse().reduce(fcn(s,n,clk){ s + if(clk.inc()%2) n else 2*n%10 + n/5 },0,Ref(1)) %10)
}</lang> <lang zkl>println(" ISIN Valid?"); foreach isin in (T("US0378331005","US0373831005","U50378331005", "US03378331005","AU0000XVGZA3","AU0000VXGZA3","FR0000988040")){
println(isin," --> ",validateISIN(isin));
}</lang>
- Output:
ISIN Valid? US0378331005 --> True US0373831005 --> False U50378331005 --> False US03378331005 --> False AU0000XVGZA3 --> True AU0000VXGZA3 --> True FR0000988040 --> True