#!/usr/local/bin/perl -w # Sample code for processing ISAN and V-ISAN identifiers # File at: http://www.finseth.com/isan.pl # Written October 2000 by Craig A. Finseth, craig@firwood.net # Revised March 2001 by Craig A. Finseth to remove variable allocations. # Revised August 2001 by Craig A. Finseth to use lid: and V-ISAN # Revised June 2004 by Craig A. Finseth to include XML # Revised 5 January 2006 by Craig A. Finseth to adjust V-ISAN label and correct # check digit calculations. # This code is NOT copyright. # The purpose of this entire file is to encourage the complete and # correct implementation of ISAN or V-ISAN handling code by providing # a working example. # The program as a whole is not intended to be used as is. Rather, it # exists as a framework for exercising the individual modules. Each # module can be used as is, with the exception that all other modules # assume that ISAN_Normalize has been called on the input data. # Note that these modules are written for clarity, not efficiency. # For reference, here is the digit allocation table. # # digits of root 12 # digits of episode 4 # digits of minor version(in V-ISAN portion) 8 @TESTDATA = ( "b159-d8fa-0124-0000-k", "1881-66c7-3420-0000-3", "1881-66c7-3420-0000-3-9f3a-0245-q", "1234567890abcdef", "1234567890abcdefj", "0123456789abcdef01234567", "0123456789abcdef5012345679", "0 1 23 4,./<>?56!@#^&*()_-+=|\\7890AbCdEc[]{}", ); $ISAN_PRINT_PREFIX = "isan: "; $VISAN_PRINT_PREFIX = "isan: "; # ------------------------------------------------------------ if ($#ARGV < -1) { USAGE: print "usage: isan [options] displays test data if no identifiers are supplied options: -h|human human input; check digit required -v|verbose be verbose in output\n"; exit 1; } $opt_h = 0; $opt_v = 0; @ids = (); while ($#ARGV >= 0) { $x = shift(@ARGV); $arg = $x; $arg =~ tr/A-Z/a-z/; if ($arg eq "-h" || $arg eq "-human") { $opt_h = 1; } elsif ($arg eq "-v" || $arg eq "-verbose") { $opt_v = 1; } elsif ($arg =~ /^\-/) { goto USAGE; } else { push(@ids, $x); } } $| = 1 if ($opt_v); # -------------------- # do each supplied value if ($#ids < 0) { foreach $t (@TESTDATA) { &Do($t, 0); } } else { foreach $i (@ids) { &Do($i, $opt_h); } } exit 0; # ------------------------------------------------------------ # Do one value sub Do ($$) { my ($x, $is_human) = @_; my ($status) = undef; print "\n----------------------------------------\n"; print "Supplied ID:\t\t'$x'\n"; if (($status = &ISAN_Normalize($x, $is_human)) ne "") { print "$status\n\n"; return; } $x = $result; print "Normalized value:\t'$x'\n"; $savex = $x; $x = &ISAN_AddCheckDigits($x), print "Printed:\n"; print "\tISAN:\t\t", &ISAN_ToPrinted(&ISAN_ExtractISANCheck($x)), "\n"; print "\tV-ISAN:\t\t", &ISAN_ToPrinted($x), "\n"; print "XML:\n"; print "\tISAN:\t\t", &ISAN_ToXML(&ISAN_ExtractISANCheck($x)), "'\n"; print "\tV-ISAN:\t\t", &ISAN_ToXML($x), "'\n"; print "Basic:\n"; print "\troot:\t\t", &ISAN_ExtractRoot($x), "\n"; print "\tepisode/part:\t", &ISAN_ExtractEpisode($x), "\n"; print "\tversion:\t", &ISAN_ExtractVersion($x), "\n"; print "Identifiers w/o check character:\n"; print "\tISAN:\t\t", &ISAN_ExtractISANNoCheck($x), "\n"; print "\tV-ISAN:\t\t", &ISAN_ExtractVISANNoCheck($x), "\n"; print "Check characters:\n"; print "\tISAN portion:\t\t", &ISAN_ComputeISANCheckDigit($x), "\n"; print "\tV-ISAN portion:\t\t", &ISAN_ComputeVISANCheckDigit($x), "\n"; print "Verify:\n"; print "\tISAN check digit:\t", &ISAN_VerifyISANCheckDigit($savex), "\n"; print "\tV-ISAN check digit:\t", &ISAN_VerifyVISANCheckDigit($savex), "\n"; } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and computes and # adds the check digit(s). If the check digit(s) are already # present, it returns the supplied identifier. It returns the # empty string if the argument is not an ISAN or V-ISAN. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_AddCheckDigits ($) { my ($x) = @_; my ($len) = length($x); if ($len == 16) { return $x . &ISAN_ComputeISANCheckDigit($x); } elsif ($len == 17) { return $x; } elsif ($len == 24) { return substr($x, 0, 16) . &ISAN_ComputeISANCheckDigit($x) . substr($x, 16, 8) . &ISAN_ComputeVISANCheckDigit($x); } elsif ($len == 26) { return $x; } else { return ""; } } # ------------------------------------------------------------ # This module accepts a string of hexadecimal digits and computes # and returns the check digit. It assumes the string has been # normalized and performs no error checking. # The algorithm is from ISO 7064. The name is MOD 37,36. sub ISAN_ComputeCheckDigit ($) { my ($x) = @_; my ($len1) = length($x) - 1; my ($chars) = "0123456789abcdefghijklmnopqrstuvwxyz"; my ($S, $AS, $P, $AP) = undef; my ($MOD1) = 37; my ($MOD2) = 36; print "Checksum, string '$x':\nindex char S/adjS P/adjP\n" if ($opt_v); for $i (0..$len1) { if ($i == 0) { # ISAN Handbook says to add $MOD2 as in the first step and the ISO # standard says to start at 0. Since we will be doing a mod 36 later, # it doesn't matter which we do. # $S = index($chars, substr($x, $i, 1)) + $MOD2; $S = index($chars, substr($x, $i, 1)); } else { $S = index($chars, substr($x, $i, 1)) + $AP; } if ($S > $MOD2) { $AS = $S - $MOD2; } else { $AS = $S; } if ($AS == 0) { $AS = $MOD2; } $P = $AS * 2; if ($P >= $MOD1) { $AP = $P - $MOD1; } else { $AP = $P; } print "$i ", substr($x, $i, 1), " $S/$AS $P/$AP\n" if ($opt_v); } if ($AP == 1) { print "returning '", substr($chars, 0, 1), "'\n" if ($opt_v); return substr($chars, 0, 1); } else { print "returning '", substr($chars, $MOD1 - $AP, 1), "'\n" if ($opt_v); return substr($chars, $MOD1 - $AP, 1); } } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and computes the # first (ISAN) check digit. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ComputeISANCheckDigit ($) { my ($x) = @_; &ISAN_ComputeCheckDigit(substr($x, 0, 16)); } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and computes the # second (V-ISAN) check digit. The empty string is returned if # there is no V-ISAN portion. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ComputeVISANCheckDigit ($) { my ($x) = @_; my ($len) = length($x); if ($len == 16) { return ""; } elsif ($len == 17) { return ""; } elsif ($len == 24) { return &ISAN_ComputeCheckDigit($x); } elsif ($len == 26) { # do not include the ISAN check digit return &ISAN_ComputeCheckDigit( substr($x, 0, 16) . substr($x, 17, 8)); } else { return ""; } } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and extracts # and returns the episode identifier portion. The empty string # is returned if there is no episode portion. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ExtractEpisode ($) { my ($x) = @_; substr($x, 12, 4); } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and extracts # and returns the ISAN portion. If check digits are present, # they are retained. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ExtractISANCheck ($) { my ($x) = @_; my ($len) = length($x); if ($len == 16) { return $x; } elsif ($len == 17) { return $x } elsif ($len == 24) { return substr($x, 0, 16); } elsif ($len == 26) { return substr($x, 0, 17); } else { return ""; } } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and extracts # and returns the ISAN without check digits. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ExtractISANNoCheck ($) { my ($x) = @_; my ($len) = length($x); if ($len == 16) { return $x; } elsif ($len == 17) { return substr($x, 0, 16); } elsif ($len == 24) { return substr($x, 0, 16); } elsif ($len == 26) { return substr($x, 0, 16); } else { return ""; } } # ------------------------------------------------------------ # This module accepts an ISAN V-ISAN identifier and extracts # and returns the root identifier portion. The empty string # is returned if there is no root identifier portion. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ExtractRoot ($) { my ($x) = @_; substr($x, 0, 12); } # ------------------------------------------------------------ # This module accepts an ISAN V-ISAN identifier and extracts # and returns the root identifier portion with dashes. The empty # string is returned if there is no root identifier portion. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ExtractRootDash ($) { my ($x) = @_; my ($r) = &ISAN_ExtractRoot($x); substr($r, 0, 4) . "-" . substr($r, 4, 4) . "-" . substr($r, 8, 4); } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and extracts # and returns the version portion. The empty string is returned # if there is no major version portion. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ExtractVersion ($) { my ($x) = @_; my ($len) = length($x); if ($len == 16) { return ""; } elsif ($len == 17) { return ""; } elsif ($len == 24) { return substr($x, 16, 8); } elsif ($len == 26) { return substr($x, 17, 8); } else { return ""; } } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and extracts # and returns the version portion with a dash. The empty string # is returned if there is no major version portion. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ExtractVersionDash ($) { my ($x) = @_; my ($v) = &ISAN_ExtractVersion($x); substr($v, 0, 4) . "-" . substr($v, 4, 4); } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and extracts # and returns the V-ISAN portion without check digits. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ExtractVISANNoCheck ($) { my ($x) = @_; my ($len) = length($x); if ($len == 16) { return $x; } elsif ($len == 17) { return substr($x, 0, 16); } elsif ($len == 24) { return $x; } elsif ($len == 26) { return substr($x, 0, 16) . substr($x, 17, 8); } else { return ""; } } # ------------------------------------------------------------ # This module accepts a string and prepares it for use by the # other modules. It performs these operations: # # 1) Remove all characters that are not the letters A-Z or the digits 0-9. # 2) Convert all letters to lower case. # 3) Verify that the string is exactly 16, 17, 24, or 26 characters long. # 4) Verify that all characters other than the check digit(s) are # hexadecimal values (i.e., not the letters G-Z). # 5) If the string is 17 or 26 characters long, verify the check digit(s). # 6) Verify that the first character is in the range 0 to 9. # # If the normalized string is a valid ISAN or V-ISAN, this module returns # the null string ("") and sets the global variable $result to the # normalized string. # # Otherwise, the module returns a string describing the problem and # does not alter the value of the $result variable. # # If $is_human is true, the module assumes that input is from a human # and requires the check digit. sub ISAN_Normalize ($$) { my ($x, $is_human) = @_; print "Normalize, starting '$x'\n" if ($opt_v); # remove all non-letters and digits $x =~ tr/A-Za-z0-9//cd; print "Normalize, bad chars gone '$x'\n" if ($opt_v); # convert to lower case $x =~ tr/A-Z/a-z/; print "Normalize, lower case '$x'\n" if ($opt_v); # verify length, digits, and checksum my ($len) = length($x); if ($len == 16) { return "digit characters must be 0-9,a-f" if ($x =~ /g-z/); return "check digit required" if ($is_human); } elsif ($len == 17) { return "digit characters must be 0-9,a-f" if (substr($x, 0, 16) =~ /g-z/); return "bad checksum" if (&ISAN_VerifyISANCheckDigit($x) ne "true"); } elsif ($len == 24) { return "digit characters must be 0-9,a-f" if (substr($x, 0, 16) =~ /g-z/); return "digit characters must be 0-9,a-f" if (substr($x, 16, 8) =~ /g-z/); return "check digits required" if ($is_human); } elsif ($len == 26) { return "digit characters must be 0-9,a-f" if (substr($x, 0, 16) =~ /g-z/); return "digit characters must be 0-9,a-f" if (substr($x, 17, 8) =~ /g-z/); return "bad checksum" if (&ISAN_VerifyISANCheckDigit($x) ne "true"); return "bad checksum" if (&ISAN_VerifyVISANCheckDigit($x) ne "true"); } else { return "invalid length"; } $result = $x; ""; } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and returns identifier # formatted for printing. # # The empty string is returned if there is no identifier # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ToString ($) { my ($x) = @_; my ($len) = length($x); if ($len == 16) { return substr($x, 0, 4) . "-" . substr($x, 4, 4) . "-" . substr($x, 8, 4) . "-" . substr($x, 12, 4); } elsif ($len == 17) { return substr($x, 0, 4) . "-" . substr($x, 4, 4) . "-" . substr($x, 8, 4) . "-" . substr($x, 12, 4) . "-" . substr($x, 16, 1); } elsif ($len == 24) { return substr($x, 0, 4) . "-" . substr($x, 4, 4) . "-" . substr($x, 8, 4) . "-" . substr($x, 12, 4) . "-" . substr($x, 16, 4) . "-" . substr($x, 20, 4); } elsif ($len == 26) { return substr($x, 0, 4) . "-" . substr($x, 4, 4) . "-" . substr($x, 8, 4) . "-" . substr($x, 12, 4) . "-" . substr($x, 16, 1) . "-" . substr($x, 17, 4) . "-" . substr($x, 21, 4) . "-" . substr($x, 25, 1); } else { return ""; } } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and returns the # identifier formatted for printing and always with a check digit. # # The empty string is returned if there is no identifier # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ToStringCheck ($) { my ($x) = @_; my ($len) = length($x); if ($len == 16) { return substr($x, 0, 4) . "-" . substr($x, 4, 4) . "-" . substr($x, 8, 4) . "-" . substr($x, 12, 4) . "-" . &ISAN_ComputeISANCheckDigit($x); } elsif ($len == 17) { return substr($x, 0, 4) . "-" . substr($x, 4, 4) . "-" . substr($x, 8, 4) . "-" . substr($x, 12, 4) . "-" . substr($x, 16, 1); } elsif ($len == 24) { return substr($x, 0, 4) . "-" . substr($x, 4, 4) . "-" . substr($x, 8, 4) . "-" . substr($x, 12, 4) . "-" . &ISAN_ComputeISANCheckDigit($x) . "-" . substr($x, 16, 4) . "-" . substr($x, 20, 4) . "-" . &ISAN_ComputeVISANCheckDigit($x); } elsif ($len == 26) { return substr($x, 0, 4) . "-" . substr($x, 4, 4) . "-" . substr($x, 8, 4) . "-" . substr($x, 12, 4) . "-" . substr($x, 16, 1) . "-" . substr($x, 17, 4) . "-" . substr($x, 21, 4) . "-" . substr($x, 25, 1); } else { return ""; } } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and returns the # equivalent printed form. # # The empty string is returned if there is no root or episode portion. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ToPrinted ($) { my ($x) = @_; my ($len) = length($x); if ($len == 16) { return $ISAN_PRINT_PREFIX . &ISAN_ToString($x); } elsif ($len == 17) { return $ISAN_PRINT_PREFIX . &ISAN_ToString($x); } elsif ($len == 24) { return $VISAN_PRINT_PREFIX . &ISAN_ToString($x); } elsif ($len == 26) { return $VISAN_PRINT_PREFIX . &ISAN_ToString($x); } else { return ""; } } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and returns the # equivalent XML form. # # The empty string is returned if there is no root or episode portion. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_ToXML ($) { my ($x) = @_; my ($len) = length($x); if ($len == 16) { return ""; } elsif ($len == 17) { return ""; } elsif ($len == 24) { return ""; } elsif ($len == 26) { return ""; } else { return ""; } } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and verifies the # first (ISAN) check digit. It returns the string "true" if the # check digit matches, "false" if not, or some other string if # the verification was unable to take place. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_VerifyISANCheckDigit ($) { my ($x) = @_; my ($len) = length($x); if ($len == 16) { return "no check digit" } elsif ($len == 17) { my ($ci) = &ISAN_ComputeISANCheckDigit($x); return ($ci eq substr($x, 16, 1)) ? "true" : "false"; } elsif ($len == 24) { return "no check digit"; } elsif ($len == 26) { my ($ci) = &ISAN_ComputeISANCheckDigit($x); return ($ci eq substr($x, 16, 1)) ? "true" : "false"; } else { return "no check digit"; } } # ------------------------------------------------------------ # This module accepts an ISAN or V-ISAN identifier and verifies the # second (V-ISAN) check digit. It returns the string "true" if the # check digit matches, "false" if not, or some other string if # the verification was unable to take place. # # This module assumes that its argument is the $result string # from an ISAN_Normalize call that returned the empty string. sub ISAN_VerifyVISANCheckDigit ($) { my ($x) = @_; my ($len) = length($x); if ($len == 16) { return "no check digit" } elsif ($len == 17) { return "no check digit" } elsif ($len == 24) { return "no check digit"; } elsif ($len == 26) { my ($cu) = &ISAN_ComputeVISANCheckDigit($x); return ($cu eq substr($x, 25, 1)) ? "true" : "false"; } else { return "no check digit"; } }