ISBN
[ class tree: ISBN ] [ index: ISBN ] [ all elements ]

Source for file ISBN.php

Documentation is available at ISBN.php

  1. <?php
  2. /**
  3.  * ISBN
  4.  *
  5.  * Handle, Convert and Validate ISBN Numbers
  6.  *
  7.  * PHP version 5
  8.  *
  9.  * LICENSE: LGPL (In cases LGPL is not appropriate, it is licensed under GPL)
  10.  *
  11.  * Package to handle, convert and validate ISBN numbers. It includes:
  12.  *
  13.  *  - ISBN specifics: EAN/PrefixArrayAccess (integer)
  14.  *  - ISBN specifics: Group/Registration Group [2001: Group identifier] (integer)
  15.  *  - ISBN specifics: GroupTitle/Registration Group Title (string)
  16.  *  - ISBN specifics: Publisher/Registrant [2001: Publisher identifier] (string)
  17.  *  - ISBN specifics: Title/Publication [2001: Title identifier] (string)
  18.  *  - ISBN specifics: Checkdigit (string)
  19.  *  - ISBN specifics: 'ISBNBody' (string)
  20.  *  - ISBN specifics: 'ISBNSubbody' (string)
  21.  *  - ISBN Version handling
  22.  *  - Syntactical Validation plus Validation based on real ISBN Data
  23.  *  - ISBN-10 (ISO 2108) checksum calculation
  24.  *  - Validation (ISBN-10 and ISBN-13-978)
  25.  *  - Conversion to ISBN-13-978
  26.  *  - ISBN-13-978 (2005 Handbook, ISO pending; ISBN-13)
  27.  *  - ISBN-13 checksum calculation (EAN)
  28.  *
  29.  * Based on standards published by international ISBN Agency
  30.  * http://www.isbn-international.org/
  31.  *
  32.  * @category  Pending
  33.  * @package   ISBN
  34.  * @author    Tom Klingenberg <tkli-php@lastflood.net>
  35.  * @copyright 2006-2007 Tom Klingenberg
  36.  * @license   LGPL http://www.gnu.org/licenses/lgpl.txt
  37.  * @version   v 0.1.6 CVS: <cvs_id>
  38.  * @link      http://isbn.lastflood.com online docs
  39.  * 
  40.  * @todo      License for .js file or remove it
  41.  * @todo      GroupTitle
  42.  * @todo      PEAR Package.xml
  43.  *
  44.  */
  45.  
  46. // {{{ constants
  47. /**
  48.  * ISBN Versions supported
  49.  */
  50. define('ISBN_VERSION_NONE'false);
  51. /**
  52.  * VERSION_UNKNOWN is by the caller only, this shall never
  53.  * be a value returned by a public function or getter
  54.  */
  55. define('ISBN_VERSION_UNKNOWN'0);
  56. define('ISBN_VERSION_ISBN_10'10);
  57. define('ISBN_VERSION_ISBN_13'13978);
  58. define('ISBN_VERSION_ISBN_13_978'ISBN_VERSION_ISBN_13);
  59. define('ISBN_VERSION_ISBN_13_979'13979);  /* reserved */
  60.  
  61. /*
  62.  * Default ISBN Version for class input / usage
  63.  */
  64. define('ISBN_DEFAULT_INPUTVERSION'ISBN_VERSION_UNKNOWN);
  65. /*
  66.  * Default ISBN Seperator string
  67.  */
  68. define('ISBN_DEFAULT_COSMETIC_SEPERATOR''-');
  69.  
  70. /*
  71.  * ISBN_DEFAULT_PRINT_LANG_SPECIFIC_PREFIX
  72.  *
  73.  * When printed, the ISBN is always preceded by the letters "ISBN".
  74.  * Note: In countries where the Latin alphabet is not used, an abbreviation
  75.  * in the characters of the local script may be used in addition to the
  76.  * Latin letters "ISBN".
  77.  * This can be defined as a default value wihtin this constant.
  78.  */
  79. define('ISBN_DEFAULT_PRINT_LANG_SPECIFIC_PREFIX''');
  80. // }}}
  81.  
  82. require_once 'PEAR/Exception.php';
  83.  
  84. // {{{ ISBN_Exception
  85. /**
  86.  * ISBN_Exception class
  87.  *
  88.  * @category  Pending
  89.  * @package   ISBN
  90.  * @author    Tom Klingenberg <tkli-php@lastflood.net>
  91.  * @copyright 2006-2007 Tom Klingenberg
  92.  * @license   LGPL http://www.gnu.org/licenses/lgpl.txt
  93.  * @link      http://isbn.lastflood.com/
  94.  * @since     Class available since Release 0.1.3
  95.  */
  96. class ISBN_Exception Extends PEAR_Exception
  97. {
  98. }
  99. // }}}
  100.  
  101. // {{{ ISBN
  102. /**
  103.  * ISBN class
  104.  *
  105.  * Class to Handle, Convert and Validate ISBN Numbers
  106.  *
  107.  * @category  Pending
  108.  * @package   ISBN
  109.  * @author    Tom Klingenberg <tkli-php@lastflood.net>
  110.  * @copyright 2006-2007 Tom Klingenberg
  111.  * @license   LGPL http://www.gnu.org/licenses/lgpl.txt
  112.  * @link      http://isbn.lastflood.com/
  113.  * @since     Class available since Release 0.0.0
  114.  */
  115. class ISBN
  116. {
  117.     /**
  118.      * @var string ISBN Registration Group
  119.      */
  120.     private $isbn_group '';
  121.     /**
  122.      * @var string ISBN Publisher
  123.      */
  124.     private $isbn_publisher '';
  125.     /**
  126.      * @var string ISBN Title
  127.      */
  128.     private $isbn_title '';
  129.  
  130.     /**
  131.      * @var mixed ISBN number version
  132.      */
  133.     private $ver ISBN_VERSION_NONE;
  134.     
  135.     /**
  136.      * @var array ISBN Groups Data acting as cache
  137.      * @see _getISBN10Groups()
  138.      */    
  139.     private static $varISBN10Groups array();    
  140.  
  141.     // {{{ __construct
  142.         /**
  143.      * Constructor
  144.      *
  145.      * @param array $isbn String of ISBN Value to use
  146.      * @param mixed $ver  Optional Version Constant
  147.      *
  148.      * @access public
  149.      * 
  150.      * @throws ISBN_Exception in case it fails
  151.      */
  152.     function __construct($isbn ''$ver ISBN_DEFAULT_INPUTVERSION)
  153.     {
  154.         /* validate & handle optional isbn parameter */
  155.         if (is_string($isbn== false {
  156.             throw new ISBN_Exception('ISBN parameter must be a string');
  157.         }
  158.         if (strlen($isbn== 0{
  159.             $this->setISBN($isbn);
  160.             return;
  161.         }
  162.  
  163.         /* validate version parameter */
  164.         if (self::_isbnVersionIs($ver== false{
  165.             throw new ISBN_Exception(
  166.                 'ISBN Version parameter is not an ISBN Version'
  167.             );
  168.         }
  169.  
  170.         /* ISBN has been passed, check the version now:
  171.          *  if it is unknown, try to dertine it, if this fails
  172.          *  throw an exception
  173.          */
  174.         if ($ver == ISBN_VERSION_UNKNOWN{
  175.             $verguess self::_isbnVersionGuess($isbn);
  176.             if (self::_isbnVersionIsValid($verguess)) {
  177.                 $ver $verguess;
  178.             else {
  179.                 /* throw new ISBN_Exception(
  180.                  *'ISBN Version couldn\'t determined.');
  181.                  */                                  
  182.                 $ver ISBN_VERSION_NONE;
  183.             }
  184.         }
  185.         /* version determined */
  186.         $this->ver $ver;
  187.  
  188.         /* handle a complete invalid ISBN of which a version could
  189.          * not be determined. */
  190.         if ($ver === ISBN_VERSION_NONE{
  191.             $this->setISBN('');
  192.             return;
  193.         }
  194.  
  195.         try {
  196.             $this->setISBN($isbn);
  197.         catch(Exception $e{
  198.             /* the isbn is invalid and not set, sothat this
  199.              * ISBN object will be set to a blank value. */
  200.             $this->setISBN('');
  201.         }
  202.  
  203.     }
  204.     // }}}
  205.  
  206.     // {{{ _extractCheckdigit()
  207.         /**
  208.      * extract Checkdigit of an ISBN-Number
  209.      *
  210.      * @param string $isbnn normalized ISBN string
  211.      *
  212.      * @return string|falseISBN-Body or false if failed
  213.      *
  214.      */
  215.     private static function _extractCheckdigit($isbnn)
  216.     {
  217.         $checkdigit false;
  218.         $checkdigit substr($isbnn-1);
  219.         return (string) $checkdigit;
  220.     }
  221.     // }}}
  222.  
  223.     // {{{ _extractEANPrefix()
  224.         /**
  225.      * extracts EAN-Prefix of a normalized isbn string
  226.      *
  227.      * @param string $isbnn normalized isbn string
  228.      *
  229.      * @return string|falsePrefix or false if failed
  230.      */
  231.     private static function _extractEANPrefix($isbnn)
  232.     {
  233.         $r settype($isbnn'string');
  234.         if ($r === false{
  235.             return false;
  236.         }
  237.         if (strlen($isbnn3{
  238.             return false;
  239.         }
  240.         $prefix substr($isbnn03);
  241.         return $prefix;
  242.     }
  243.     // }}}
  244.  
  245.     // {{{ _extractGroup()
  246.         /**
  247.      * extract Registration Group of an ISBN-Body
  248.      *
  249.      * @param string $isbnbody ISBN-Body
  250.      *
  251.      * @return integer|false   Registration Group or false if failed
  252.      */
  253.     private static function _extractGroup($isbnbody)
  254.     {
  255.         $group   '';
  256.         $subbody '';
  257.  
  258.         $r self::_isbnBodyParts($isbnbody$group$subbody);
  259.         if ($r === false{
  260.             return false;
  261.         }
  262.         return $group;
  263.     }
  264.     // }}}
  265.  
  266.     // {{{ _extractISBNBody()
  267.         /**
  268.      * extract ISBN-Body of an ISBN-Number
  269.      *
  270.      * @param string $isbnn normalized ISBN string
  271.      *
  272.      * @return string|falseISBN-Body or false if failed
  273.      */
  274.     private static function _extractISBNBody($isbnn)
  275.     {
  276.         /* extract */
  277.         $body  false;
  278.         $isbnn = (string) $isbnn;
  279.  
  280.         $l strlen($isbnn);
  281.         if ($l == 10{
  282.             $body =  substr($isbnn0-1);
  283.         elseif ($l == 13{
  284.             $body =  substr($isbnn3-1);
  285.         else {
  286.             return false;
  287.         }
  288.         /* verify */
  289.         $r settype($body'string');
  290.         if ($r === false{
  291.             return false;
  292.         }
  293.         if (strlen($body!= 9{
  294.             return false;
  295.         }
  296.         if (ctype_digit($body=== false{
  297.             return false;
  298.         }
  299.         return $body;
  300.     }
  301.     // }}}
  302.  
  303.     // {{{ _isbnBodyParts()
  304.         /**
  305.      * Get the 2 Parts of the ISBN-Body (ISBN-10/ISBN-13-978)
  306.      *
  307.      * @param string $isbnbody           ISBN-Body
  308.      * @param string &$registrationgroup Registration Group
  309.      * @param string &$isbnsubbody       ISBN-Subbody
  310.      *
  311.      * @return boolean  False if failed, True on success
  312.      *
  313.      * @access private
  314.      */
  315.     private static function _isbnBodyParts($isbnbody
  316.                                            &$registrationgroup
  317.                                            &$isbnsubbody)
  318.     {
  319.         /* validate input (should not be needed, @access private) */
  320.         $r settype($isbnbody'string');
  321.         if ($r === false{
  322.             return false;
  323.         }
  324.         if (strlen($isbnbody!= 9{
  325.             return false;
  326.         }
  327.         if (ctype_digit($isbnbody=== false{
  328.             return false;
  329.         }
  330.         /* extract registraion group
  331.          * boundaries see p.13 2005 handbook
  332.          */
  333.         $boundaries array();
  334.  
  335.         $boundaries[array(    0599991);
  336.         $boundaries[array(60000600993)// Iran 2006-12-05
  337.         $boundaries[array(60100699990);
  338.         $boundaries[array(70000799991);
  339.         $boundaries[array(80000949992);
  340.         $boundaries[array(95000989993);
  341.         $boundaries[array(99000998994);
  342.         $boundaries[array(99900999995);
  343.         /* segment value */
  344.         $segment      substr($isbnbody05);
  345.         $segmentvalue intval($segment);
  346.         /* test segment value against boundaries */
  347.         $r false;
  348.         foreach ($boundaries as $boundary{
  349.             if ($segmentvalue >= $boundary[0&& $segmentvalue <= $boundary[1]{
  350.                 $r $boundary[2];
  351.             }
  352.         }
  353.         if ($r === false{
  354.             return false;
  355.         }
  356.         /* $r is 0 when the boundary is not defined */
  357.         if ($r === 0{
  358.             return false;
  359.         }
  360.         $registrationgroup substr($isbnbody0$r);
  361.         $isbnsubbody       substr($isbnbody$r);
  362.         return true;
  363.     }
  364.     // }}}
  365.  
  366.     // {{{ _isbnSubbodyParts()
  367.         /**
  368.      * Get the 2 Parts of the ISBN-Subbody (ISBN-10/ISBN-13)
  369.      *
  370.      * @param string  $isbnsubbody  ISBN-Subbody
  371.      * @param integer $groupid      Registrationgroup
  372.      * @param string  &$registrant  Registrant
  373.      * @param string  &$publication Publication
  374.      *
  375.      * @return boolean  False if failed, true on success
  376.      *
  377.      * @access private
  378.      */
  379.     private static function _isbnSubbodyParts($isbnsubbody
  380.                                               $groupid
  381.                                               &$registrant
  382.                                               &$publication)
  383.     {
  384.         /* validate input (should not be needed, @access private) */
  385.         $r settype($isbnsubbody'string');
  386.         if ($r === false{
  387.             return false;
  388.         }
  389.         $l strlen($isbnsubbody);
  390.         if $l || $l 8{
  391.             return false;
  392.         }
  393.         if (ctype_digit($isbnsubbody=== false{
  394.             return false;
  395.         }
  396.         $r settype($groupid'integer');
  397.         if ($r === false{
  398.             return false;
  399.         }
  400.         if ($groupid || $groupid 99999{
  401.             return false;
  402.         }
  403.         /* extract registrant based on group and registrant range
  404.          * parse this specific group format: 
  405.          *  array(
  406.          *      'English speaking area',
  407.          *      '00-09;10-19;200-699;7000-8499;85000-89999;' .
  408.          *         '900000-949999;9500000-9999999'
  409.          *      );
  410.          */ 
  411.         
  412.         $group self::_getISBN10Group($groupid);
  413.         if ($group === false{
  414.             return false;
  415.         }
  416.  
  417.         $len self::_getRegistrantLength($isbnsubbody$group[1]);
  418.         if ($len === false{
  419.             return false;
  420.         }        
  421.         $registrant  substr($isbnsubbody0$len);
  422.         $publication substr($isbnsubbody$len);
  423.         return true;
  424.     }
  425.     // }}}
  426.  
  427.     // {{{ _getRegistrantLength()
  428.         /**
  429.      * Return Length of Registrant part within an ISBNSubbody in a specific
  430.      * grouprange in this specific format:
  431.      *
  432.      * '00-09;10-19;200-699;7000-8499;85000-89999;900000-949999;9500000-9999999'
  433.      *
  434.      * Info: This function is compatible with Groupranges formatted in the
  435.      * .js file and might become obsolete if new formats are more fitting.
  436.      *
  437.      * @param string $isbnsubbody ISBN-Subbody
  438.      * @param string $grouprange  Grouprange in the Format '#a1-#z1;#a2-z2[...]'
  439.      *
  440.      * @return boolean|int False if failed or Length (in chars) of Registrant
  441.      *
  442.      * @access private
  443.      */
  444.     private static function _getRegistrantLength($isbnsubbody$grouprange)
  445.     {
  446.         $r settype($grouprange'string');
  447.         if ($r === false{
  448.             return false;
  449.         }
  450.         if (strlen($grouprange3{
  451.             return false;
  452.         }
  453.         
  454.         $sl     strlen($isbnsubbody);
  455.         $ranges explode(';'$grouprange);
  456.         foreach ($ranges as $range{
  457.             $range  trim($range);
  458.             $fromto explode('-'$range);
  459.             if (count($fromto!== 2{
  460.                 return false;
  461.             }
  462.             /* validation:
  463.              * from and to need to be in the same class,
  464.              * having the same length.
  465.              * registrant can not be bigger or same then the
  466.              * whole subbody, at least there is one digit for
  467.              * the publication.
  468.              */
  469.  
  470.             $l strlen($fromto[0]);
  471.             if ($l != strlen($fromto[1])) {
  472.                 return false;
  473.             }
  474.             if ($l >= $sl{
  475.                 return false;
  476.             }
  477.  
  478.             /* check that from/to values are in order */
  479.             if (strcmp($fromto[0]$fromto[1]>= 0{
  480.                 return false;
  481.             }
  482.  
  483.             /* compare and fire if matched */
  484.             $comparec substr($isbnsubbody0$l);
  485.  
  486.             if (strcmp($fromto[0]$comparec&& 
  487.                 strcmp($fromto[1]$comparec> -1{
  488.                 return $l;
  489.             }
  490.         }        
  491.         return false;
  492.     }
  493.     // }}}
  494.  
  495.     // {{{ _getISBN10Group()
  496.         /**
  497.      * Get ISBN-10 Registration Group Data by its numeric ID
  498.      *
  499.      * @param integer $id Registration Group Identifier
  500.      *
  501.      * @return mixed    array:   group array
  502.      *                   boolean: False if failed
  503.      */
  504.     private static function _getISBN10Group($id)
  505.     {
  506.         $r settype($id'integer');
  507.         if ($r === false{
  508.             return false;
  509.         }
  510.         $groups self::_getISBN10Groups();
  511.         if ($groups === false{
  512.             return false;
  513.         }
  514.         if (isset($groups[$id]=== false{
  515.             return false;
  516.         }
  517.         $group $groups[$id];
  518.         return $group;
  519.     }
  520.     // }}}
  521.  
  522.     // {{{ _getISBN10Groups()
  523.         /**
  524.      * Get all ISBN-10 Registration Groups
  525.      *
  526.      * @return array    groups array
  527.      *
  528.      *  Info: This function connects outer world data into this class logic
  529.      *        which can be generated with the supplied tools.
  530.      *        A user should not alter the array data. This data should be altered
  531.      *        together with the international ISBN Agency only.
  532.      */
  533.     private static function _getISBN10Groups()
  534.     {
  535.         /* check if data has been already loaded */
  536.         if (sizeof(self::$varISBN10Groups{
  537.                 return self::$varISBN10Groups;      
  538.         }
  539.         
  540.         /* load external data */
  541.         try {       
  542.             $t file_get_contents('data/groups.csv'truenull);
  543.         catch(Exception $e{
  544.             throw new ISBN_Exception(
  545.                 'Unable to read ISBN Groups Data file.'
  546.             );          
  547.         }
  548.         
  549.         /* parse external data */
  550.         $groups array();      
  551.         $tls    split("\n"$t);
  552.         $line   0;
  553.         foreach ($tls as $tl{
  554.             $line++;            
  555.             $tlp split(','$tl);
  556.             if (sizeof($tlp== 3{
  557.                 $index intval($tlp[0]);
  558.                 if (isset($groups[$index])) {
  559.                     throw new ISBN_Exception(
  560.                         'ISBN Groups Data is invalid, Group ' .
  561.                         $index 'is a duplicate.'
  562.                     );                  
  563.                 }
  564.                 /* edit+ mature: sanitize external 
  565.                    data */
  566.                 $groups[$indexarray($tlp[1],$tlp[2]);                       
  567.             else {
  568.                 throw new ISBN_Exception(
  569.                     'ISBN Groups Data is malformed on line #' $line 
  570.                     ' (' sizeof($tlp').'
  571.                 );
  572.             }               
  573.