<?php
//  ISBN-ISSN.php  - ISBN/ISSN processing functions in PHP4.
//  Version 0.97
//  Robert D. Cameron, April 16, 2001.
//  Copyright (c) 2001,  GNU Public License, Version 2, applies.
//    Authoritative country/group code data needed.
//
//  This library includes functions for the following tasks.
//
//  (A) Checksum Calculation and Validation
//  (B) Hyphenation
//  (C) ISBN Country/Group look-up
//  (D) Error correction (single-digit errors)
//
//  General note: because of the similar processing logic,
//  generic ISN processing functions have been written to
//  handle both ISBN and ISSN.   These functions are instantiated
//  to ISBN with a length parameter of 10 and ISSN with a length
//  parameter of 8.   
//
//  -------------------------------------------------------------
//  ISN Checksum Calculation
//
//  ISBNs and ISSNs use similar modulus 11 checksum calculations.
//  For an ISBN, a weighted sum of the first nine digits is computed
//  with digit weights descending from 10 to 2.  Based on this
//  checksum, a check digit of 0 to 9 or X (representing 10) is 
//  determined as the value to be added to checksum to bring its
//  value to 0 modulus 11.   The calculation for ISSNs is similar,
//  except that the calculation is based on the weighted sum of the
//  first seven digits with weights descending from 8 to 2.
//
//  The mod11_checksum function provides for fault-tolerant
//  ISN checksum calculations starting with a given initial
//  weight.  Given the seven significant digits of an ISSN
//  or the nine significant digits of an ISBN, it calculates
//  the modulus 11 checksum that can be used for check digit
//  determination as above.  Given a complete ISN with check digit,
//  (that is, 8 digit ISSN or 10 digit ISBN) it confirms check
//  digit validity with a report of 0 as the modulus 11 checksum.
//  For applications involving ISN error correction, it also
//  computes a checksum for erroneous ISN strings, simply ignoring
//  extraneous or invalid characters.
//
function mod11_checksum ($protostring$weight) {
  
$checksum 0;
  
$pos 0;
  while ((
$pos strlen($protostring)) && ($weight >= 1)) {
    if ((
$protostring[$pos] >= '0') && ($protostring[$pos] <= '9')) {
      
$checksum += $weight $protostring[$pos];
      
$weight--;
    }
    elseif ((
$weight == 1) && 
            ((
$protostring[$pos] == 'X') || ($protostring[$pos] == 'x'))) {
      
$checksum += 10;
      
$weight--;
    }
    
$pos++;
  }
  return 
$checksum 11;
}

// Check Digit Determinatoin
//
// Given a checksum for an incomplete ISN, determine the check
// digit that must be added.
//
function make_checkdigit($checksum) {
  
$checkdigit = (11 $checksum) % 11;
  return (
$checkdigit == 10) ? "X" : (string) $checkdigit;
}

// ISN Validation
//
// Count bad ISN characters.  (ISSN or ISBN)
//
function bad_ISN_char_count ($proto_string) {
  
$count 0;
  for (
$i 0$i strlen($proto_string); $i++) {
    
$c $proto_string[$i];
    if (
$c == '-') continue;
    if ((
$c '0') || ($c '9')) {$count++;}
  }
  if ((
$c == 'X') || ($c == 'x')) {
    return 
$count 1;   // adjustment for a check digit
  
}
  else {return 
$count;}
}

//
// ISN_clean: remove hyphens and transform check digit 'x' to 'X'. )
//
function ISN_clean ($isbn_proto) {
  return 
str_replace("x""X"str_replace("-"""$isbn_proto));
}

//
// ISN Checksum Validity
// An ISN is checksum-valid if, after removal of hyphens, (a) it is
// a complete ISN of the proper length, (b) consists only of the
// digits [0-9], except for the final check digit which may be X,
// and (c) has a weighted mod 11 checksum of 0.
//
function ISN_checksum_OK($isn_proto$length) {
  
$isn_proto ISN_clean($isn_proto);
  return (
strlen($isn_proto) == $length) &&
         (
bad_ISN_char_count($isn_proto) == 0) &&
         (
mod11_checksum($isn_proto$length) == 0);
}

function 
ISBN_checksum_OK ($isbn_proto) {
  return 
ISN_checksum_OK($isbn_proto10);
}

function 
ISSN_checksum_OK ($issn_proto) {
  return 
ISN_checksum_OK($issn_proto8);
}

//
//  ISBN Hyphenation Conventions
//
//  ISBN hyphenation uses variable-length codes for county/group code,
//  publisher code and book code, with a total length of 9.  
//
//  The hyphenation rules can be described using codespace partition maps
//  structured as follows.  A code space partition map is an ordered list
//  of code prefixes that each establish the lower value of a range of 
//  codes having the same length.   For example, consider the ISBN
//  $country_group_partition following.

$country_group_partition = array('0''80''950''9960''99900');

//  This map specifies the following ranges for country/group prefixes:
//  0-7, 80-94, 950-995, 9960-9989, 99900-99999.  An ISBN beginning with
//  0 through 7 is thus hyphenated after the first character, while an
//  ISBN beginning with 950 through 995 is hyphenated after the third.

function prefix_length_from_map ($s$map) {
  for (
$i 1$i sizeof($map); $i++) {
    if (
strcmp($s$map[$i]) < 0) return strlen($map[$i-1]);
  }
  return 
strlen($map[$i-1]);
}

function 
country_group_code ($isbn_proto) {
  global 
$country_group_partition;
  
$isbn_proto ISN_clean($isbn_proto);
  
$cglen prefix_length_from_map($isbn_proto$country_group_partition);
  return 
substr($isbn_proto0$cglen);
}

//
// For each country group, the remaining code space may similarly
// be described by a partition map.  This allows the length of
// the publisher code to be determined.  This in turn allows for
// the complete determination of proper ISBN hyphenation, since
// the check digit must be of length 1 and the book code is of
// length 10 - the lengths of the other elements.
//
// Note 1: this table uses a special convention for coding unassigned
// ranges with the space of a particular country-group code: the
// length of the "publisher code" is set to consume all the remaining
// digits of the ISBN except for the check digit.  This leaves
// no digits for a separate book code.  Hyphenation of such an
// "unassigned" ISBN will have only three hyphens, e.g., 
// 1-00000000-1, 93-0000000-4.
//
// Note 2: a method for keeping this table current is needed. 
//
// Note 3: the present table has been constructed using various
// sources on the web, including the ISBN.el module of Nelson
// Beebe (for Emacs Lisp) and the Business::ISBN.pm module of
// Brian D. Foy of Smith Renaud, Inc.  More work is needed.
//
// Note 4: Country names replaced by ISO3166 country codes,
// Apr. 16, 2001.   Multiple countries separated by ":" after
// a group description string.  INT used as the code for the
// international organizations group. 
//
$country_group_map =
  array(
    
=> array(array('00',200,7000,85000,900000,9500000), 
                     
"English group 0:AU:CA:GI:IE:NZ:PR:ZA:SZ:GB:US:ZW"),
    
=> array(array('00000000',55000,869800,9999900), 
                     
"English group 1:AU:CA:GI:IE:NZ:PR:ZA:SZ:GB:US:ZW"),
    
=> array(array('00',200,40000000,500,7000,84000,900000,9500000), 
                     
"French group:FR:BE:CA:LU:CH"),
    
=> array(array('00',200,7000,85000,900000,9500000), 
                     
"German group:AT:DE:CH"),
    
=> array(array('00',200,7000,85000,900000,9500000), "JP"),
    
=> array(array('00',200,7000,85000,900000,9500000), 
         
"Former USSR group:RU:AM:AZ:BY:EE:GE:KZ:KG:LV:LT:MD:TJ:TM:UA:UZ"),
    
=> array(array('00',100,5000,80000,900000), "CN"),
    
80 => array(array('00',200,7000,85000,900000), "Czech/Slovak:CZ:SK"),
    
81 => array(array('00',200,7000,85000,900000), "IN"),
    
82 => array(array('00',200,7000,90000,990000), "NO"),
    
83 => array(array('00',200,7000,85000,900000), "PL"),
    
84 => array(array('00',200,7000,85000,900000,95000,9700), "ES"),
    
85 => array(array('00',200,7000,85000,900000), "BR"),
    
86 => array(array('00',300,7000,80000,900000), "Balkans:YU:BA:HR:MK:SI"),
    
87 => array(array('00',400,7000,85000,970000), "DK"),
    
88 => array(array('00',200,7000,85000,900000), "Italian group:IT:CH"),
    
89 => array(array('00',300,7000,85000,950000), "Korean group:KP:KR"),
    
90 => array(array('00',200,5000,70000,800000,9000000), "Dutch group:NL:BE"),
    
91 => array(array('0',20,500,6500000,7000,8000000,85000,9500000,970000), "SE"),
    
92 => array(array('0',60,800,9000), "INT"), // International organizations
    
93 => array(array('0000000'), "IN"),
    
950 => array(array('00',500,9000,99000), "AR"),
    
951 => array(array('0',20,550,8900,95000), "FI"),
    
952 => array(array('00',200,5000,89,9500,99000), "FI"),
    
953 => array(array('0',10,150,6000,96000), "HR"),
    
954 => array(array('00',400,8000,90000), "BG"),
    
955 => array(array('0',20,550,800000,9000,95000), "LK"),
    
956 => array(array('00',200,7000), "CL"),
    
957 => array(array('00',440,8500,97000), "TW"),
    
958 => array(array('0',600,9000,95000), "CO"),
    
959 => array(array('00',200,7000), "CU"),
    
960 => array(array('00',200,7000,85000), "GR"),
    
961 => array(array('00',200,6000,90000), "SI"),
    
962 => array(array('00',200,7000,85000), "HK"),
    
963 => array(array('00',200,7000,85000), "HU"),
    
964 => array(array('00',300,5500,90000), "IR"),
    
965 => array(array('00',200,7000,90000), "IL"),
    
966 => array(array('00',500,7000,90000), "UA"),
    
967 => array(array('0',60,900,9900,99900), "MY"),
    
968 => array(array('000000',10,400,500000,6000,800,900000), "MX"),
    
969 => array(array('0',20,400,8000), "PK"),
    
970 => array(array('00',600,9000,91000), "MX"),
    
971 => array(array('00',500,8500,91000), "PH"),
    
972 => array(array('0',20,550,8000,95000), "PT"),
    
973 => array(array('0',20,550,9000,95000), "RO"),
    
974 => array(array('00',200,7000,85000,900000), "TH"),
    
975 => array(array('00',300,6000,92000,980000), "TR"),
    
976 => array(array('0',40,600,8000,95000), 
         
"Caribbean Community:AG:BS:BB:BZ:KY:DM:GD:GY:JM:MS:KN:LC:VC:TT:VG"),
    
977 => array(array('00',200,5000,70000), "EG"),
    
978 => array(array('000',2000,30000), "NG"),
    
979 => array(array('0',20,300000,400,700000,8000,95000), "ID"),
    
980 => array(array('00',200,6000), "VE"),
    
981 => array(array('00',200,3000), "SG"),
    
982 => array(array('00',100,500000), 
                 
"South Pacific:CK:FJ:KI:MH:NR:NU:SB:TK:TO:TV:VU:WS"),
    
983 => array(array('000',2000,300000,50,800,9000,99000), "MY"),
    
984 => array(array('00',400,8000,90000), "BD"),
    
985 => array(array('00',400,6000,90000), "BY"),
    
986 => array(array('000000'), "TW"),
    
987 => array(array('00',500,9000,99000), "AR"),
    
9952 => array(array('00000'), "AZ"),
    
9953 => array(array('0',20,9000), "LB"),
    
9954 => array(array('00',8000), "MA"),
    
9955 => array(array('00',400), "LT"),
    
9956 => array(array('00000'), "CM"),
    
9957 => array(array('00',8000), "JO"),
    
9958 => array(array('0',10,500,7000,9000), "BA"),
    
9959 => array(array('00'), "Libya"),
    
9960 => array(array('00',600,9000), "SA"),
    
9961 => array(array('0',50,800,9500), "DZ"),
    
9962 => array(array('00000'), "PA"),
    
9963 => array(array('0',30,550,7500), "CY"),
    
9964 => array(array('0',70,950), "GH"),
    
9965 => array(array('00',400,9000), "KZ"),
    
9966 => array(array('00',70000,800,9600), "KE"),
    
9967 => array(array('00000'), "KG"),
    
9968 => array(array('0',10,700,9700), "CR"),
    
9970 => array(array('00',400,9000), "UG"),
    
9971 => array(array('0',60,900,9900), "SG"),
    
9972 => array(array('0',40,600,9000), "PE"),
    
9973 => array(array('0',10,700,9700), "TN"),
    
9974 => array(array('0',30,550,7500), "UY"),
    
9975 => array(array('0',50,900,9500), "MD"),
    
9976 => array(array('0',60,900,99000,9990), "TZ"),
    
9977 => array(array('00',900,9900), "CR"),
    
9978 => array(array('00',950,9900), "EC"),
    
9979 => array(array('0',50,800,9000), "IS"),
    
9980 => array(array('0',40,900,9900), "PG"),
    
9981 => array(array('0',20,800,9500), "MA"),
    
9982 => array(array('00',40000,800,9900), "ZM"),
    
9983 => array(array('00',500,80,950,9900), "GM"),
    
9984 => array(array('00',500,9000), "LV"),
    
9985 => array(array('0',50,800,9000), "EE"),
    
9986 => array(array('00',400,9000), "LT"),
    
9987 => array(array('00',400,8800), "TZ"),
    
9988 => array(array('0',30,550,7500), "GH"),
    
9989 => array(array('0',30,600,9700), "MK"),
    
99901 => array(array('00'), "BH"),   
    
99903 => array(array('0',20,900), "MU"),
    
99904 => array(array('0',60,900), "AN"),
    
99905 => array(array('0',60,900), "BO"),
    
99906 => array(array('0',60,900), "KW"),
    
99908 => array(array('0',10,900), "MW"),
    
99909 => array(array('0',40,950), "MT"),
    
99910 => array(array('0000'), "SL"),
    
99911 => array(array('00',600), "LS"),
    
99912 => array(array('0',60,900), "BW"),
    
99913 => array(array('0',30,600), "AD"),
    
99914 => array(array('0',50,900), "SR"),
    
99915 => array(array('0',50,800), "FK"),
    
99916 => array(array('0',30,700), "NA"),
    
99917 => array(array('0',30), "BN"),
    
99918 => array(array('0',40,900), "FO"),
    
99919 => array(array('0',40,900), "BJ"),
    
99920 => array(array('0',50,900), "AD"),
    
99921 => array(array('0',20,700), "QA"),
    
99922 => array(array('0',50), "GT"),
    
99923 => array(array('0',20,800), "SV"),
    
99924 => array(array('0',30), "NI"),
    
99925 => array(array('0',40,800), "PY"),
    
99926 => array(array('0000',600), "HN"),
    
99927 => array(array('0',30,600), "AL"),
    
99928 => array(array('0',50,800), "GE"),
    
99929 => array(array('0000'), "MN"),
    
99930 => array(array('0',50,800), "AM"),
    
99931 => array(array('0000'), "SC"),
    
99932 => array(array('0',10), "MT"),
    
99933 => array(array('00',300), "NP"),
    
99934 => array(array('0'), "DO"),
    
99935 => array(array('0000'), "HT"),
    
99936 => array(array('0000'), "BT"),
    
99937 => array(array('0',20), "MO")
  );

function 
country_group_name ($isbn_proto) {
  global 
$country_group_map;
  
$cg country_group_code($isbn_proto);
  if (
is_array($country_group_map[$cg])) return $country_group_map[$cg][1];
  else return 
"unassigned country/group";
}

//
//  Determine whether an ISBN is within an assigned range of publisher 
//  codes for its country/group.
//
function is_assigned ($isbn_proto) {
  
$isbn_proto ISN_clean($isbn_proto);
  global 
$country_group_map;
  
$cg country_group_code($isbn_proto);
  
$cglen strlen($cg);
  
$after_cg substr($isbn_proto$cglen);
  if (
is_array($country_group_map[$cg])) {
    
$publen prefix_length_from_map($after_cg$country_group_map[$cg][0]);
    return (
$cglen $publen 9);
  }
  else return 
false;
}

//
// Generate the canonical hyphenated form of an ISBN.
// (Checksum-valid ISBN assumed as input).
//
function canonical_ISBN ($isbn_proto) {
  
$isbn_proto ISN_clean($isbn_proto);
  global 
$country_group_map;
  
$cg country_group_code($isbn_proto);
  
$cglen strlen($cg);
  
$pubandbook substr($isbn_proto$cglen9-$cglen);
  
$checkdigit $isbn_proto[9];
  if (
is_array($country_group_map[$cg])) {
    
$publen prefix_length_from_map($pubandbook$country_group_map[$cg][0]);
    if (
$cglen $publen == 9) return "$cg-$pubandbook-$checkdigit";
    else {
      
$pubcode substr($pubandbook0$publen);
      
$bookno substr($pubandbook$publen);
      return 
"$cg-$pubcode-$bookno-$checkdigit";
    }
  }
  else return 
"$cg-$pubandbook-$checkdigit";
}


//
// Generate the canonical hyphenated form of an ISSN.
// (Checksum-valid ISSN assumed as input).
//
function canonical_ISSN ($issn_proto) {
  
$issn_proto ISN_clean($issn_proto);
  return 
substr($issn_proto04) . "-" substr($issn_proto44);
}

//
//  Length-based canonical ISN function for use within generic ISN
//  error correction function.
//
function canonical_ISN ($isn_proto$length) {
  if (
$length == 8) return canonical_ISSN($isn_proto);
  else return 
canonical_ISBN($isn_proto);
}

//
//  ISN classification.
//
//  Given a dehyphenated ISN protostring and an expected length
//  (ISSN: 8, ISBN: 10), analyze it and return one
//  of the following classification strings:
//    "checksumOK" - a proper length ISN with valid check digit
//    "single_error" - an ISN with a single digit error.
//    "short" - a string that is too short, but has only valid ISN chars.
//    "short/invalid" - a string that is too short and has invalid chars.
//    "invalid" - a string with more than 2 errors.
//    "long" - a string that is too long, but has only valid ISN chars.
//    "long/invalid" - a string that is too long and has invalid chars.
//
function ISN_classifier($clean$length) {
  
$bad_chars bad_ISN_char_count($clean);
  if (
strlen($clean) < $length-1)
    return (
$bad_chars == 0) ? "short" "short/invalid";
  elseif (
strlen($clean) == $length-1)
    return (
$bad_chars == 0) ? "single_error" "short/invalid";
  elseif (
strlen($clean) == $length) {
    if (
$bad_chars == 0)
      if (
mod11_checksum($clean$length) == 0) return "checksumOK";
      else return 
"single_error";
    else
      return (
$bad_chars == 1) ? "single_error" "invalid";
  }
  elseif (
strlen($clean) == $length+1) {
    if (
$bad_chars == 0) return "single_error";
    elseif ((
$bad_chars == 1) && (mod11_checksum($clean$length) == 0)) 
      return 
"single_error";
    else return 
"long/invalid";
  }
  else return (
$bad_chars == 0) ? "long" "long/invalid";
}

function 
ISSN_classifier($clean) {
  return 
ISN_classifier($clean8);
}

//
//  ISBN classification has an additional category "bookland" - an apparent 
//  Bookland encoded EAN.
//
function ISBN_classifier($clean) {
  if ((
strlen($clean) == 13) && (substr($clean03) == '978')
      && (
bad_ISN_char_count($clean) == 0))
    return 
"bookland";
  else return 
ISN_classifier($clean10);
}

//
// ISN Error Correction
//
// Given an erroneous ISN protostring, generate possible correct ISN strings
// of the given $length (8 => ISSN, 10 => ISBN) with single character corrections.  
//  - For protostrings with $length-1 significant digits, try adding a 
//    correct character at each possible position.
//  - For protostrings with $length digits, generate all possible single character
//    replacements and pairwise interchanges that yield a valid ISN.
//  - For protostrings of $length+1 digits, generate all valid ISNs formed 
//    by deletion of a single character.

function generate_ISN_corrections($ISN_proto$length) {
  
$corrections = array();
  
$ISN_proto ISN_clean($ISN_proto);
  if (
strlen($ISN_proto) == $length-1) {
    
// First try possible insertions in the first $length-1 positions.
    
for ($pos 0$pos $length-1$pos++) {
      for (
$i 0$i <= 9$i++) {
        
$ISN_new substr($ISN_proto0$pos) . (string) $i 
                    
substr($ISN_proto$pos);
        if (
ISN_checksum_OK($ISN_new$length)) {
          
$corrections[canonical_ISN($ISN_new$length)] = true;
        }
      }
    }
    if (
$ISN_proto[$length-2] != 'X') {
      
$ISN_new $ISN_proto .  
                  
make_checkdigit(mod11_checksum($ISN_proto$length));
      
$corrections[canonical_ISN($ISN_new$length)] = true;
    }
  }
  elseif (
strlen($ISN_proto) == $length) {
    
// Try replacements and interchanges for first nine positions.
    
for ($pos 0$pos $length-1$pos++) {
      
$current $ISN_proto[$pos];
      
// try all possible replacements
      
for ($i 0$i <= 9$i++) {
        
$ISN_proto[$pos] = (string) $i;
        if (
ISN_checksum_OK($ISN_proto$length)) {
          
$corrections[canonical_ISN($ISN_proto$length)] = true;
        }
      }
      
// try the next character interchange (unless there is a final "X").
      
$ISN_proto[$pos] = $ISN_proto[$pos+1];
      
$ISN_proto[$pos+1] = $current;
      if ((
$ISN_proto[$pos] != 'X') && ISN_checksum_OK($ISN_proto$length)) {
        
$corrections[canonical_ISN($ISN_proto$length)] = true;
      }
      
// reset to the original values before moving on to the next position.
      
$ISN_proto[$pos+1] = $ISN_proto[$pos];
      
$ISN_proto[$pos] = $current;
    }
    
// Now replace the check digit with the correct value.
    
$ISN_proto[$length-1] = 
      
make_checkdigit(mod11_checksum(substr($ISN_proto0$length-1), $length));
    if (
bad_ISN_char_count($ISN_proto) == 0) {
      
$corrections[canonical_ISN($ISN_proto$length)] = true;
    }
  }
  elseif (
strlen($ISN_proto) == $length+1) {
    for (
$pos 0$pos <= $length$pos++) {
      
$ISN_new substr($ISN_proto0$pos) . substr($ISN_proto$pos+1);
      if (
ISN_checksum_OK($ISN_new$length)) {
        
$corrections[canonical_ISN($ISN_new$length)] = true;
      }
    }
  }
  return 
$corrections;
}


function 
generate_ISBN_corrections($isbn_proto) {
  return 
generate_ISN_corrections($isbn_proto10);
}

function 
generate_ISSN_corrections($issn_proto) {
  return 
generate_ISN_corrections($issn_proto8);
}
?>