Validate International Securities Identification Number

From Rosetta Code
Revision as of 13:30, 22 November 2016 by Oenone (talk | contribs) (→‎{{header|Ada}}: update to use algorithm from luhn task and newer test cases)
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.

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

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

  1. Ifndef TRUE ' define true and false for older freebasic versions
   #Define FALSE 0
   #Define TRUE Not FALSE
  1. 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

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.

<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

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.

<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

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:

Works with: Rakudo version 2016.07

<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
  1. 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"]]
  1. [True, False, False, False, True, True, True]</lang>

Racket

<lang racket>

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

  1. => [true, false, false, false, true, true, true]</lang>

Tcl

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.

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

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>

Visual Basic

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

<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