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
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>sub is-valid-ISIN (Str $ISIN --> Bool) {
$ISIN ~~ /^ <[A..Z]>**2 <[A..Z0..9]>**9 <[0..9]> $/ or return False; my $base10 = $ISIN.comb.map({ :36($_) }).join; return luhn-test $base10;
}</lang>
Testing:
<lang perl6>say "$_ is{' not' unless validate-ISIN $_} 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]] }
proc cksum {isin} { set isin [normalize $isin] assert {[string is digit -strict $isin]} set digits [split $isin ""] if {[llength $digits] % 2} { set digits [list 0 {*}$digits] } foreach {o e} $digits { incr sum [expr {$o + ($e * 2) % 9}] } expr {(10 - ($sum % 10)) % 10} }
proc validate {isin} { set isin [normalize $isin] regexp {^(.*)(.)$} $isin -> body sum expr {$sum eq [cksum $body]} }
}</lang>
Finally, some tcltests pinched from other examples in this page:
<lang Tcl>package require tcltest tcltest::test isin-1 "Test isin validation" -body {
foreach {str sum} { US037833100 5 US037383100 9 SU037833100 5 AU0000XVGZA 3 AU0000VXGZA 3 GB000263494 6 } { assert {[isin::cksum $str] eq $sum} assert {![isin::validate $str$sum]}
set err [expr {1+int(rand()*8)}] ;# create a random checksum error set sum [expr {$sum + $err % 10}] assert {![isin::validate $str$sum]} } 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
- Programming Tasks
- Solutions by Programming Task
- Ada
- C
- Elixir
- Fortran
- FreeBASIC
- Go
- Groovy
- Groovy examples needing attention
- Examples needing attention
- Haskell
- J
- J examples needing attention
- Java
- Lua
- Perl
- Perl 6
- PowerShell
- Python
- Racket
- REXX
- Ruby
- SAS
- Tcl
- Tcl examples needing attention
- Visual Basic
- Visual Basic examples needing attention
- Zkl