Validate International Securities Identification Number

From Rosetta Code
Task
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.

Task[edit]

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.

Details

The format of an ISIN is as follows:

┌───────────── a 2-character ISO country code (A-Z)
 ┌─────────── a 9-character security code (A-Z, 0-9)
         ┌── a checksum digit (0-9)
AU0000XVGZA3

For this task, you may assume that any 2-character alphabetic sequence is a valid country code.

The checksum can be validated as follows:

  1. Replace letters with digits, by converting each character from base 36 to base 10, e.g. AU0000XVGZA31030000033311635103.
  2. 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.)
Test-cases
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.)

See also

Useful resources:


Related tasks:



Ada[edit]

Calling the existing Luhn algorithm implementation from the Luhn test of credit card numbers task.

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;

Output:

US0378331005:TRUE
US0373831005:FALSE
U50378331005:FALSE
AU0000XVGZA3:TRUE
AU0000VXGZA3:TRUE
FR0000988040:TRUE
US03378331005:Exception occured

ALGOL W[edit]

Uses the LuhnTest procedure from the Luhn test of credit card numbers task.

begin
 % external procedure that returns true if ccNumber passes the Luhn test, false otherwise %
logical procedure LuhnTest ( string(32) value ccNumber
 ; integer value ccLength
) ; algol "LUHN" ;
 
 % returns true if isin is a valid ISIN, false otherwise  %
logical procedure isIsin ( string(32) value isin ) ;
if isin( 12 // 20 ) not = "" then false % code is too long %
else begin
 % the first two characters must be upper-case letters %
 
 % returns the digit corresponding to a character of an ISIN %
integer procedure isinDigit ( string(1) value iChar ) ;
if iChar >= "0" and iChar <= "9" then ( decode( iChar ) - decode( "0" ) )
else if iChar >= "A" and iChar <= "Z" then ( decode( iChar ) - decode( "A" ) ) + 10
else begin % invalid digit %
isValid := false;
-1
end isinDigit ;
 
integer d1, d2;
logical isValid;
isValid := true;
d1  := isinDigit( isin( 0 // 1 ) );
d2  := isinDigit( isin( 1 // 1 ) );
if d1 < 10 or d1 > 35 or d2 < 10 or d2 > 35 then false % invalid first two characters %
else begin
 % ok so far - conveet from base 36 to base 10 %
string(24) base10Isin;
integer b10Pos;
base10Isin := "";
b10Pos  := 0;
for cPos := 0 until 10 do begin
integer digit;
digit := isinDigit( isin( cPos // 1 ) );
if isValid then begin
 % valid digit %
if digit > 9 then begin
base10Isin( b10Pos // 1 ) := code( ( digit div 10 ) + decode( "0" ) );
b10Pos  := b10Pos + 1;
end if_digit_gt_9 ;
base10Isin( b10Pos // 1 )  := code( ( digit rem 10 ) + decode( "0" ) );
b10Pos  := b10Pos + 1
end if_isValid
end for_cPos ;
 % add the check digit as is %
base10Isin( b10Pos // 1 ) := isin( 11 // 1 );
isValid and LuhnTest( base10Isin, b10Pos + 1 )
end
end isIsin ;
 
 % task test cases %
 
procedure testIsIsin ( string(32) value isin
 ; logical value expected
) ;
begin
logical isValid;
isValid := isIsin( isin );
write( s_w := 0
, isin
, if isValid then " is valid" else " is invalid"
, if isValid = expected then "" else " NOT as expected ??"
)
end testIsin ;
 
testIsIsin( "US0378331005", true );
testIsIsin( "US0373831005", false );
testIsIsin( "U50378331005", false );
testIsIsin( "US03378331005", false );
testIsIsin( "AU0000XVGZA3", true );
testIsIsin( "AU0000VXGZA3", true );
testIsIsin( "FR0000988040", true );
end.
Output:
US0378331005                     is valid
US0373831005                     is invalid
U50378331005                     is invalid
US03378331005                    is invalid
AU0000XVGZA3                     is valid
AU0000VXGZA3                     is valid
FR0000988040                     is valid

C[edit]

#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 */

Caché ObjectScript[edit]

Class Utils.Check [ Abstract ]
{
 
ClassMethod ISIN(x As %String) As %Boolean
{
// https://en.wikipedia.org/wiki/International_Securities_Identification_Number
IF x'?2U9UN1N QUIT 0
SET cd=$EXTRACT(x,*), x=$EXTRACT(x,1,*-1)
FOR i=1:1 {
SET n=$EXTRACT(x,i) IF n="" QUIT
IF n'=+n SET $EXTRACT(x,i)=$CASE(n,"*":36,"@":37,"#":38,:$ASCII(n)-55)
}
// call into luhn check, appending check digit
QUIT ..Luhn(x_cd)
}
 
ClassMethod Luhn(x As %String) As %Boolean
{
// https://www.simple-talk.com/sql/t-sql-programming/calculating-and-verifying-check-digits-in-t-sql/
SET x=$TRANSLATE(x," "), cd=$EXTRACT(x,*)
SET x=$REVERSE($EXTRACT(x,1,*-1)), t=0
FOR i=1:1:$LENGTH(x) {
SET n=$EXTRACT(x,i)
IF i#2 SET n=n*2 IF $LENGTH(n)>1 SET n=$EXTRACT(n,1)+$EXTRACT(n,2)
SET t=t+n
}
QUIT cd=((t*9)#10)
}
 
}
Examples:
USER>For  { Read isin Quit:isin=""  Write ": "_##class(Utils.Check).ISIN(isin), ! }
US0378331005: 1
US0373831005: 0
U50378331005: 0
US03378331005: 0
AU0000XVGZA3: 1
AU0000VXGZA3: 1
FR0000988040: 1

USER>

COBOL[edit]

By Steve Williams
Works with: GnuCOBOL
        >>SOURCE FORMAT FREE
*> this is gnucobol 2.0
identification division.
program-id. callISINtest.
data division.
working-storage section.
01 ISINtest-result binary-int.
procedure division.
start-callISINtest.
display 'should be valid ' with no advancing
call 'ISINtest' using 'US0378331005' ISINtest-result
perform display-ISINtest-result
display 'should not be valid ' with no advancing
call 'ISINtest' using 'US0373831005' ISINtest-result
perform display-ISINtest-result
display 'should not be valid ' with no advancing
call 'ISINtest' using 'U50378331005' ISINtest-result
perform display-ISINtest-result
display 'should not be valid ' with no advancing
call 'ISINtest' using 'US03378331005' ISINtest-result
perform display-ISINtest-result
display 'should be valid ' with no advancing
call 'ISINtest' using 'AU0000XVGZA3' ISINtest-result
perform display-ISINtest-result
display 'should be valid ' with no advancing
call 'ISINtest' using 'AU0000VXGZA3' ISINtest-result
perform display-ISINtest-result
display 'should be valid ' with no advancing
call 'ISINtest' using 'FR0000988040' ISINtest-result
perform display-ISINtest-result
stop run
.
display-ISINtest-result.
evaluate ISINtest-result
when 0
display ' is valid'
when -1
display ' invalid length '
when -2
display ' invalid countrycode '
when -3
display ' invalid base36 digit '
when -4
display ' luhn test failed'
when other
display ' invalid return code ' ISINtest-result
end-evaluate
.
end program callISINtest.
 
identification division.
program-id. ISINtest.
data division.
working-storage section.
01 country-code-values value
'ADAEAFAGAIALAMAOAQARASATAUAWAXAZBABBBDBEBFBGBHBIBJBLBMBNBOBQBRBS'
& 'BTBVBWBYBZCACCCDCFCGCHCICKCLCMCNCOCRCUCVCWCXCYCZDEDJDKDMDODZECEE'
& 'EGEHERESETFIFJFKFMFOFRGAGBGDGEGFGGGHGIGLGMGNGPGQGRGSGTGUGWGYHKHM'
& 'HNHRHTHUIDIEILIMINIOIQIRISITJEJMJOJPKEKGKHKIKMKNKPKRKWKYKZLALBLC'
& 'LILKLRLSLTLULVLYMAMCMDMEMFMGMHMKMLMMMNMOMPMQMRMSMTMUMVMWMXMYMZNA'
& 'NCNENFNGNINLNONPNRNUNZOMPAPEPFPGPHPKPLPMPNPRPSPTPWPYQARERORSRURW'
& 'SASBSCSDSESGSHSISJSKSLSMSNSOSRSSSTSVSXSYSZTCTDTFTGTHTJTKTLTMTNTO'
& 'TRTTTVTWTZUAUGUMUSUYUZVAVCVEVGVIVNVUWFWSYEYTZAZMZW'.
03 country-codes occurs 249
ascending key country-code
indexed by cc-idx.
05 country-code pic xx.
 
01 b pic 99.
01 base36-digits pic x(36) value
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
 
01 i pic 99.
01 p pic 99.
01 luhn-number pic x(20).
01 luhntest-result binary-int.
 
linkage section.
01 test-number any length.
01 ISINtest-result binary-int.
 
procedure division using test-number ISINtest-result.
start-ISINtest.
display space test-number with no advancing
 
*> format test
if function length(test-number) <> 12
move -1 to ISINtest-result
goback
end-if
 
*> countrycode test
search all country-codes
at end
move -2 to ISINtest-result
goback
when test-number(1:2) = country-code(cc-idx)
continue
end-search
 
*> convert each character from base 36 to base 10
*> and add to the luhn-number
move 0 to p
perform varying i from 1 by 1 until i > 12
if test-number(i:1) >= '0' and <= '9'
move test-number(i:1) to luhn-number(p + 1:1)
add 1 to p
else
perform varying b from 9 by 1 until b > 35
or base36-digits(b + 1:1) = test-number(i:1)
continue
end-perform
if b > 35
move -3 to ISINtest-result
goback
end-if
move b to luhn-number(p + 1:2)
add 2 to p
end-if
end-perform
 
call 'luhntest' using luhn-number(1:p) luhntest-result
if luhntest-result <> 0
move -4 to ISINtest-result
goback
end-if
 
move 0 to ISINtest-result
goback
.
end program ISINtest.
 
identification division.
program-id. luhntest.
data division.
working-storage section.
01 i pic S99.
01 check-sum pic 999.
linkage section.
01 test-number any length.
01 luhntest-result binary-int.
procedure division using test-number luhntest-result.
start-luhntest.
display space test-number with no advancing
move 0 to check-sum
 
*> right to left sum the odd numbered digits
compute i = function length(test-number)
perform varying i from i by -2 until i < 1
add function numval(test-number(i:1)) to check-sum
end-perform
display space check-sum with no advancing
 
*> right to left double sum the even numbered digits
compute i = function length(test-number) - 1
perform varying i from i by -2 until i < 1
add function numval(test-number(i:1)) to check-sum
add function numval(test-number(i:1)) to check-sum
*> convert a two-digit double sum number to a single digit
if test-number(i:1) >= '5'
subtract 9 from check-sum
end-if
end-perform
display space check-sum with no advancing
 
if function mod(check-sum,10) = 0
move 0 to luhntest-result *> success
else
move -1 to luhntest-result *> failure
end-if
goback
.
end program luhntest.
Output:
prompt$ cobc -xj ISINTest.cbl
should be valid  US0378331005 30280378331005 027 050 is valid
should not be valid  US0373831005 30280373831005 022 046 luhn test failed
should not be valid  U50378331005 invalid countrycode
should not be valid  US03378331005 invalid length
should be valid  AU0000XVGZA3 1030000033311635103 018 030 is valid
should be valid  AU0000VXGZA3 1030000031331635103 018 030 is valid
should be valid  FR0000988040 15270000988040 020 050 is valid

Common Lisp[edit]

(defun alphap (char)
(char<= #\A char #\Z))
 
(defun alpha-digit-char-p (char)
(or (alphap char) (digit-char-p char)))
 
(defun valid-isin-format-p (isin)
(and (= (length isin) 12)
(alphap (char isin 0))
(alphap (char isin 1))
(loop for i from 2 to 10
always (alpha-digit-char-p (char isin i)))
(digit-char-p (char isin 11))))
 
(defun isin->digits (isin)
(apply #'concatenate 'string
(loop for c across isin
collect (princ-to-string (digit-char-p c 36)))))
 
(defun luhn-test (string)
(loop for c across (reverse string)
for oddp = t then (not oddp)
if oddp
sum (digit-char-p c) into result
else
sum (let ((n (* 2 (digit-char-p c))))
(if (> n 9) (- n 9) n))
into result
finally (return (zerop (mod result 10)))))
 
(defun valid-isin-p (isin)
(and (valid-isin-format-p isin)
(luhn-test (isin->digits isin))))
 
(defun test ()
(dolist (isin '("US0378331005" "US0373831005" "U50378331005" "US03378331005"
"AU0000XVGZA3" "AU0000VXGZA3" "FR0000988040"))
(format t "~A: ~:[invalid~;valid~]~%" isin (valid-isin-p isin))))
Output:
US0378331005: valid
US0373831005: invalid
U50378331005: invalid
US03378331005: invalid
AU0000XVGZA3: valid
AU0000VXGZA3: valid
FR0000988040: valid

Elixir[edit]

used Luhn module from here

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)}")
Output:
    ISIN        Valid?
US0378331005    true
US0373831005    false
U50378331005    false
US03378331005   false
AU0000XVGZA3    true
AU0000VXGZA3    true
FR0000988040    true

Fortran[edit]

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

FreeBASIC[edit]

' 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
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[edit]

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
}
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)
}
}
}

Groovy[edit]

This example needs updating due to a modification in the task. Please examine and fix the code if needed, then remove this message.
Details: Use the new test-cases, and consider calling the existing Luhn algorithm implementation from the Luhn test of credit card numbers task instead of duplicating it.
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

Haskell[edit]

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 :: String
capitalLetters = ['A','B' .. 'Z']
 
numbers :: String
numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
 
correctFormat :: String -> Bool
correctFormat isin =
(length isin == 12) &&
all (`elem` 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 = concatMap convert
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
then putStrLn " valid"
else putStrLn " not valid"
 
main :: IO ()
main = do
let isinnumbers =
[ "US0378331005"
, "US0373831005"
, "U50378331005"
, "US03378331005"
, "AU0000XVGZA3"
, "AU0000VXGZA3"
, "FR0000988040"
]
mapM_ printSolution isinnumbers
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

Or, making alternative choices from standard libraries:

import qualified Data.Map as M
import Control.Monad (sequence, liftM2)
import Data.Maybe (fromMaybe)
 
validISIN, isinPattern, luhn :: String -> Bool
validISIN = liftM2 (&&) isinPattern (luhn . (show =<<) . stringInts)
 
isinPattern s =
12 == length s &&
let [l, m, r] = bites [2, 9, 1] s
in all (`elem` capitals) l &&
all (`elem` (capitals ++ digits)) m && head r `elem` digits
 
luhn x =
let odd = [(: []), const []]
even = reverse odd
stream f = concat $ zipWith ($) (cycle f) (stringInts $ reverse x)
s1 = sum (stream odd)
s2 = sum $ sum . stringInts . show . (2 *) <$> stream even
in rem (s1 + s2) 10 == 0
 
charMap :: M.Map Char Int
charMap = M.fromList $ zip (digits ++ capitals) [0 ..]
 
stringInts :: String -> [Int]
stringInts = fromMaybe [] . sequence . fmap (`M.lookup` charMap)
 
bites :: [Int] -> [a] -> [[a]]
bites ns xs =
reverse . fst $
foldr
(\x (a, r) ->
let (b, r_) = splitAt x r
in (b : a, r_))
([], xs)
(reverse ns)
 
capitals, digits :: String
capitals = ['A' .. 'Z']
 
digits = ['0' .. '9']
 
main :: IO ()
main =
mapM_
(print . ((,) <*> validISIN))
[ "US0378331005"
, "US0373831005"
, "U50378331005"
, "US03378331005"
, "AU0000XVGZA3"
, "AU0000VXGZA3"
, "FR0000988040"
]
Output:
("US0378331005",True)
("US0373831005",False)
("U50378331005",False)
("US03378331005",False)
("AU0000XVGZA3",True)
("AU0000VXGZA3",True)
("FR0000988040",True)

J[edit]

Solution:

require'regex'
validFmt=: 0 -: '^[A-Z]{2}[A-Z0-9]{9}[0-9]{1}$'&rxindex
 
df36=: ;@([: <@":"0 '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'&i.) NB. decimal from base 36
luhn=: 0 = 10 (| +/@,) 10 #.inv 1 2 *&|: _2 "."0\ |. NB. as per task Luhn_test_of_credit_card_numbers#J
 
validISIN=: validFmt *. [email protected]

Required Examples:

   Tests=: 'US0378331005';'US0373831005';'U50378331005';'US03378331005';'AU0000XVGZA3';'AU0000VXGZA3';'FR0000988040'
validISIN&> Tests
1 0 0 0 1 1 1

Java[edit]

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.

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());
}
}
US0378331005 is valid
US0373831009 is valid
D56000543287 is not valid
AU0000XVGZA3 is valid
AU0000VXGZA3 is valid
GB0002634946 is valid
US0373831005 is not valid

Kotlin[edit]

As the Luhn test method is only a few lines, it's reproduced here for convenience:

// version 1.1
 
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, _ -> i % 2 == 0 }.sumBy { it - '0' }
val s2 = t.filterIndexed { i, _ -> 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"}")
}
}
Output:
US0378331005     -> valid
US0373831005     -> not valid
U50378331005     -> not valid
US03378331005    -> not valid
AU0000XVGZA3     -> valid
AU0000VXGZA3     -> valid
FR0000988040     -> valid

Lua[edit]

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
Output:
US0378331005    true
US0373831005    false
US0373831005    false
US03378331005   false
AU0000XVGZA3    true
AU0000VXGZA3    true
FR0000988040    true

Perl[edit]

We reuse the luhn_test() function from Luhn test of credit card numbers#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);
}
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[edit]

Using the luhn-test function defined at Luhn test of credit card numbers#Perl 6:

Works with: Rakudo version 2016.07
my $ISIN = /
^ <[A..Z]>**2 <[A..Z0..9]>**9 <[0..9]> $
<?{ luhn-test $/.comb.map({ :36($_) }).join }>
/;

Testing:

say "$_ is {$ISIN ?? "valid" !! "not valid"}" for <
US0378331005
US0373831005
U50378331005
US03378331005
AU0000XVGZA3
AU0000VXGZA3
FR0000988040
>;
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

Phix[edit]

Note this (slightly better) version of Luhn() has the reverse() inside it, whereas the original did not.

function Luhn(string st)
integer s=0, d
st = reverse(st)
for i=1 to length(st) do
d = st[i]-'0'
s += iff(mod(i,2)?d,d*2-(d>4)*9)
end for
return remainder(s,10)=0
end function
 
function valid_ISIN(string st)
-- returns 1 if valid, else 0/2/3/4.
-- (feel free to return 0 instead of 2/3/4)
if length(st)!=12 then return 2 end if
for i=length(st) to 1 by -1 do
integer ch = st[i]
if ch>='A' then
if ch>'Z' then return 3 end if
st[i..i] = sprintf("%d",ch-55)
elsif i<=2 then
return 4
elsif ch<'0' or ch>'9' then
return 3
end if
end for
return Luhn(st)
end function
 
sequence tests = {"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
 
constant reasons = {"wrong checksum","valid","wrong length","bad char","wrong country"}
 
for i=1 to length(tests) do
string ti = tests[i]
printf(1,"%s : %s\n",{ti,reasons[valid_ISIN(ti)+1]})
end for
Output:
US0378331005 : valid
US0373831005 : wrong checksum
U50378331005 : wrong country
US03378331005 : wrong length
AU0000XVGZA3 : valid
AU0000VXGZA3 : valid
FR0000988040 : valid

PicoLisp[edit]

Using the luhn function defined at Luhn test of credit card numbers#PicoLisp:

(de isin (Str)
(let Str (mapcar char (chop Str))
(and
(= 12 (length Str))
(<= 65 (car Str) 90)
(<= 65 (cadr Str) 90)
(luhn
(pack
(mapcar
'((N)
(- N (if (<= 48 N 57) 48 55)) )
Str ) ) ) ) ) )
(println
(mapcar
isin
(quote
"US0378331005"
"US0373831005"
"U50378331005"
"US03783310005"
"AU0000XVGZA3"
"AU0000VXGZA3"
"FR0000988040" ) ) )
Output:
(0 NIL NIL NIL 0 0 0)

PowerShell[edit]

 
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
}
 
 
"US0378331005","US0373831005","US0337833103","AU0000XVGZA3","AU0000VXGZA3","FR0000988040" | ForEach-Object {
[PSCustomObject]@{
ISIN = $_
IsValid = Test-ISIN -Number $_
}
}
 
Output:
ISIN         IsValid
----         -------
US0378331005    True
US0373831005   False
US0337833103   False
AU0000XVGZA3    True
AU0000VXGZA3    True
FR0000988040    True

PureBasic[edit]

EnableExplicit
 
Procedure.b Check_ISIN(*c.Character)
Define count.i=0, Idx.i=1, v.i=0, i.i
Dim s.i(24)
 
If MemoryStringLength(*c) > 12 : ProcedureReturn #False : EndIf
 
While *c\c
count+1
If *c\c>='0' And *c\c<='9'
If count<=2 : ProcedureReturn #False : EndIf
s(Idx)= *c\c - '0'
Idx+1
ElseIf *c\c>='A' And *c\c<='Z'
s(Idx)= (*c\c - ('A'-10)) / 10
Idx+1
s(Idx)= (*c\c - ('A'-10)) % 10
Idx+1
Else
ProcedureReturn #False
EndIf
*c + SizeOf(Character)
Wend
 
For i=Idx-2 To 0 Step -2
If s(i)*2 > 9
v+ s(i)*2 -9
Else
v+ s(i)*2
EndIf
v+s(i+1)
Next
 
ProcedureReturn Bool(v%10=0)
EndProcedure
 
Define.s s
OpenConsole("Validate_International_Securities_Identification_Number (ISIN)")
 
If ReadFile(0,"c:\code_pb\rosettacode\data\isin.txt")
While Not Eof(0)
s=ReadString(0)
Print(s+~"\t")
If Check_ISIN(@s) : PrintN("TRUE") : Else : PrintN("FALSE") : EndIf
Wend
CloseFile(0)
EndIf
Input()
Output:
US0378331005    TRUE
US0373831005    FALSE
U50378331005    FALSE
US03378331005   FALSE
AU0000XVGZA3    TRUE
AU0000VXGZA3    TRUE
FR0000988040    TRUE

Python[edit]

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]

Racket[edit]

 
#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)
 
Output:

'(#t #f #f #f #t #t #t)

REXX[edit]

/*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*/

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[edit]

Using a pre-existing luhn method:

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]

SAS[edit]

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;

Tcl[edit]

package require Tcl 8.6   ;# mostly needed for [assert].  Substitute a simpler one or a NOP if required.

A proc like assert is always good to have around. This one tries to report values used in its expression using subst:

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
}
}

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:

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]
}
 
}

To run the test suite, we use the tcltest framework included with 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

Transact-SQL[edit]

 
CREATE FUNCTION dbo._ISINCheck( @strISIN VarChar(40) )
RETURNS bit
AS
BEGIN
--*** Test an ISIN code and return 1 if it is valid, 0 if invalid.
DECLARE @bValid bit;
 
SET @bValid = CASE WHEN @strISIN LIKE '[A-Z][A-Z][A-Z,0-9][A-Z,0-9][A-Z,0-9][A-Z,0-9][A-Z,0-9][A-Z,0-9][A-Z,0-9][A-Z,0-9][A-Z,0-9][0-9]' THEN 1 ELSE 0 END
IF @bValid = 1
BEGIN
DECLARE @strTest VarChar(40) = '';
DECLARE @strAdd VarChar(2);
DECLARE @p INT = 0;
WHILE @p < LEN(@strISIN)
BEGIN
SET @p = @p+1;
SET @strAdd = SUBSTRING(@strISIN,@p,1);
IF @strAdd LIKE '[A-Z]' SET @strAdd = CONVERT(VarChar(2),ASCII(UPPER(@strAdd))-55);
SET @strTest = @strTest + @strAdd;
END;
 
-- Proceed with Luhn test
DECLARE @strLuhn VarChar(40) = REVERSE(@strTest); -- usage: set once, never changed
DECLARE @strS2Values VarChar(10) = '0246813579'; -- constant: maps digits to their S2 summed values
SET @p = 0; -- reset loop counter
DECLARE @intValue INT;
DECLARE @intSum INT = 0;
-- loop through the reversed string, get the value (even-positioned digits are mapped) and add it to @intSum
WHILE @p < LEN(@strLuhn)
BEGIN
SET @p = @p+1;
SET @intValue = CONVERT(INT, SUBSTRING(@strLuhn,@p,1) ) -- value of the digit at position @p in the string
IF @p % 2 = 0 SET @intValue = CONVERT(INT,SUBSTRING(@strS2Values,@intValue+1,1))
SET @intSum = @intSum + @intValue
END
-- If the of the digits' mapped values ends in 0 (modulo 10 = 0) then the Luhn test succeeds
SET @bValid = CASE WHEN @intSum % 10 = 0 THEN 1 ELSE 0 END
END;
 
RETURN @bValid
END
 

Testing

 
-- Testing. The following tests all pass.
;WITH ISIN_Tests AS
( SELECT 'US0378331005' AS ISIN, 1 Expected
UNION SELECT 'US0373831005',0
UNION SELECT 'U50378331005',0
UNION SELECT 'US03378331005',0
UNION SELECT 'AU0000XVGZA3',1
UNION SELECT 'AU0000VXGZA3',1
UNION SELECT 'FR0000988040',1
UNION SELECT '0___garbage',0
UNION SELECT '',0
)
SELECT ISIN, Expected, dbo._ISINCheck(ISIN) AS TestResult FROM ISIN_Tests ORDER BY ISIN
 

Visual Basic[edit]

This example needs updating due to a modification in the task. Please examine and fix the code if needed, then remove this message.
Details: Use the new test-cases, and consider calling the existing Luhn algorithm implementation from the Luhn test of credit card numbers task instead of duplicating it.
Works with: VB6
 
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
 

zkl[edit]

Uses the luhn test from Luhn_test_of_credit_card_numbers#zkl (copied here as it is short).

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)
}
println("     ISIN       Valid?");
foreach isin in (T("US0378331005","US0373831005","U50378331005",
"US03378331005","AU0000XVGZA3","AU0000VXGZA3","FR0000988040")){
println(isin," --> ",validateISIN(isin));
}
Output:
     ISIN       Valid?
US0378331005 --> True
US0373831005 --> False
U50378331005 --> False
US03378331005 --> False
AU0000XVGZA3 --> True
AU0000VXGZA3 --> True
FR0000988040 --> True