<?php

# PHP IBAN - http://code.google.com/p/php-iban - LGPLv3

# Verify an IBAN number.  Returns true or false.
#  NOTE: Input can be printed 'IBAN xx xx xx...' or machine 'xxxxx' format.
function verify_iban($iban) {

 # First convert to machine format.
 $iban = iban_to_machine_format($iban);

 # Get country of IBAN
 $country = iban_get_country_part($iban);

 # Test length of IBAN
 if(strlen($iban)!=iban_country_get_iban_length($country)) { return false; }

 # Get checksum of IBAN
 $checksum = iban_get_checksum_part($iban);

 # Get country-specific IBAN format regex
 $regex = '/'.iban_country_get_iban_format_regex($country).'/';

 # Check regex
 if(preg_match($regex,$iban)) {
  # Regex passed, check checksum
  if(!iban_verify_checksum($iban)) {
   return false;
  }
 }
 else {
  return false;
 }

 # Otherwise it 'could' exist
 return true;
}

# Convert an IBAN to machine format.  To do this, we
# remove IBAN from the start, if present, and remove
# non basic roman letter / digit characters
function iban_to_machine_format($iban) {
 # Uppercase and trim spaces from left
 $iban = ltrim(strtoupper($iban));
 # Remove IBAN from start of string, if present
 $iban = preg_replace('/^IBAN/','',$iban);
 # Remove all non basic roman letter / digit characters
 $iban = preg_replace('/[^a-zA-Z0-9]/','',$iban);
 return $iban;
}

# Convert an IBAN to human format. To do this, we
# simply insert spaces right now, as per the ECBS
# (European Committee for Banking Standards) 
# recommendations available at:
# http://www.europeanpaymentscouncil.eu/knowledge_bank_download.cfm?file=ECBS%20standard%20implementation%20guidelines%20SIG203V3.2.pdf 
function iban_to_human_format($iban) {
 # First verify validity, or return
 if(!verify_iban($iban)) { return false; }
 # Add spaces every four characters
 $human_iban = '';
 for($i=0;$i<strlen($iban);$i++) {
  $human_iban .= substr($iban,$i,1);
  if(($i>0) && (($i+1)%4==0)) { $human_iban .= ' '; }
 }
 return $human_iban;
}

# Get the country part from an IBAN
function iban_get_country_part($iban) {
 $iban = iban_to_machine_format($iban);
 return substr($iban,0,2);
}

# Get the checksum part from an IBAN
function iban_get_checksum_part($iban) {
 $iban = iban_to_machine_format($iban);
 return substr($iban,2,2);
}

# Get the BBAN part from an IBAN
function iban_get_bban_part($iban) {
 $iban = iban_to_machine_format($iban);
 return substr($iban,4);
}

# Check the checksum of an IBAN - code modified from Validate_Finance PEAR class
function iban_verify_checksum($iban) {
 # convert to machine format
 $iban = iban_to_machine_format($iban);
 # move first 4 chars (countrycode and checksum) to the end of the string
 $tempiban = substr($iban, 4).substr($iban, 0, 4);
 # subsitutute chars
 $tempiban = iban_checksum_string_replace($tempiban);
 # mod97-10
 $result = iban_mod97_10($tempiban);
 # checkvalue of 1 indicates correct IBAN checksum
 if ($result != 1) {
  return false;
 }
 return true;
}

# Find the correct checksum for an IBAN
#  $iban  The IBAN whose checksum should be calculated
function iban_find_checksum($iban) {
 $iban = iban_to_machine_format($iban);
 # move first 4 chars to right
 $left = substr($iban,0,2) . '00'; # but set right-most 2 (checksum) to '00'
 $right = substr($iban,4);
 # glue back together
 $tmp = $right . $left;
 # convert letters using conversion table
 $tmp = iban_checksum_string_replace($tmp);
 # get mod97-10 output
 $checksum = iban_mod97_10_checksum($tmp);
 # return 98 minus the mod97-10 output, left zero padded to two digits
 return str_pad((98-$checksum),2,'0',STR_PAD_LEFT);
}

# Set the correct checksum for an IBAN
#  $iban  IBAN whose checksum should be set
function iban_set_checksum($iban) {
 $iban = iban_to_machine_format($iban);
 return substr($iban,0,2) . iban_find_checksum($iban) . substr($iban,4);
}

# Character substitution required for IBAN MOD97-10 checksum validation/generation
#  $s  Input string (IBAN)
function iban_checksum_string_replace($s) {
 $iban_replace_chars = range('A','Z');
 foreach (range(10,35) as $tempvalue) { $iban_replace_values[]=strval($tempvalue); }
 return str_replace($iban_replace_chars,$iban_replace_values,$s);
}

# Same as below but actually returns resulting checksum
function iban_mod97_10_checksum($numeric_representation) {
 $checksum = intval(substr($numeric_representation, 0, 1));
 for ($position = 1; $position < strlen($numeric_representation); $position++) {
  $checksum *= 10;
  $checksum += intval(substr($numeric_representation,$position,1));
  $checksum %= 97;
 }
 return $checksum;
}

# Perform MOD97-10 checksum calculation ('Germanic-level effiency' version - thanks Chris!)
function iban_mod97_10($numeric_representation) {
 # prefer php5 gmp extension if available
 if(function_exists('gmp_intval')) { return gmp_intval(gmp_mod(gmp_init($numeric_representation, 10),'97')) === 1; }

/*
 # old manual processing (~16x slower)
 $checksum = intval(substr($numeric_representation, 0, 1));
 for ($position = 1; $position < strlen($numeric_representation); $position++) {
  $checksum *= 10;
  $checksum += intval(substr($numeric_representation,$position,1));
  $checksum %= 97;
 }
 return $checksum;
 */

 # new manual processing (~3x slower)
 $length = strlen($numeric_representation);
 $rest = "";
 $position = 0;
 while ($position < $length) {
        $value = 9-strlen($rest);
        $n = $rest . substr($numeric_representation,$position,$value);
        $rest = $n % 97;
        $position = $position + $value;
 }
 return ($rest === 1);
}

# Get an array of all the parts from an IBAN
function iban_get_parts($iban) {
 return array(
         'country'	=>      iban_get_country_part($iban),
 	 'checksum'	=>	iban_get_checksum_part($iban),
	 'bban'		=>	iban_get_bban_part($iban),
 	 'bank'		=>	iban_get_bank_part($iban),
	 'country'	=>	iban_get_country_part($iban),
	 'branch'	=>	iban_get_branch_part($iban),
	 'account'	=>	iban_get_account_part($iban)
        );
}

# Get the Bank ID (institution code) from an IBAN
function iban_get_bank_part($iban) {
 $iban = iban_to_machine_format($iban);
 $country = iban_get_country_part($iban);
 $start = iban_country_get_bankid_start_offset($country);
 $stop = iban_country_get_bankid_stop_offset($country);
 if($start!=''&&$stop!='') {
  $bban = iban_get_bban_part($iban);
  return substr($bban,$start,($stop-$start+1));
 }
 return '';
}

# Get the Branch ID (sort code) from an IBAN
function iban_get_branch_part($iban) {
 $iban = iban_to_machine_format($iban);
 $country = iban_get_country_part($iban);
 $start = iban_country_get_branchid_start_offset($country);
 $stop = iban_country_get_branchid_stop_offset($country);
 if($start!=''&&$stop!='') {
  $bban = iban_get_bban_part($iban);
  return substr($bban,$start,($stop-$start+1));
 }
 return '';
}

# Get the (branch-local) account ID from an IBAN
function iban_get_account_part($iban) {
 $iban = iban_to_machine_format($iban);
 $country = iban_get_country_part($iban);
 $start = iban_country_get_branchid_stop_offset($country);
 if($start!='') {
  $bban = iban_get_bban_part($iban);
  return substr($bban,$start+1);
 }
 return '';
}

# Get the name of an IBAN country
function iban_country_get_country_name($iban_country) {
 return _iban_country_get_info($iban_country,'country_name');
}

# Get the domestic example for an IBAN country
function iban_country_get_domestic_example($iban_country) {
 return _iban_country_get_info($iban_country,'domestic_example');
}

# Get the BBAN example for an IBAN country
function iban_country_get_bban_example($iban_country) {
 return _iban_country_get_info($iban_country,'bban_example');
}

# Get the BBAN format (in SWIFT format) for an IBAN country
function iban_country_get_bban_format_swift($iban_country) {
 return _iban_country_get_info($iban_country,'bban_format_swift');
}

# Get the BBAN format (as a regular expression) for an IBAN country
function iban_country_get_bban_format_regex($iban_country) {
 return _iban_country_get_info($iban_country,'bban_format_regex');
}

# Get the BBAN length for an IBAN country
function iban_country_get_bban_length($iban_country) {
 return _iban_country_get_info($iban_country,'bban_length');
}

# Get the IBAN example for an IBAN country
function iban_country_get_iban_example($iban_country) {
 return _iban_country_get_info($iban_country,'iban_example');
}

# Get the IBAN format (in SWIFT format) for an IBAN country
function iban_country_get_iban_format_swift($iban_country) {
 return _iban_country_get_info($iban_country,'iban_format_swift');
}

# Get the IBAN format (as a regular expression) for an IBAN country
function iban_country_get_iban_format_regex($iban_country) {
 return _iban_country_get_info($iban_country,'iban_format_regex');
}

# Get the IBAN length for an IBAN country
function iban_country_get_iban_length($iban_country) {
 return _iban_country_get_info($iban_country,'iban_length');
}

# Get the BBAN Bank ID start offset for an IBAN country
function iban_country_get_bankid_start_offset($iban_country) {
 return _iban_country_get_info($iban_country,'bban_bankid_start_offset');
}

# Get the BBAN Bank ID stop offset for an IBAN country
function iban_country_get_bankid_stop_offset($iban_country) {
 return _iban_country_get_info($iban_country,'bban_bankid_stop_offset');
}

# Get the BBAN Branch ID start offset for an IBAN country
function iban_country_get_branchid_start_offset($iban_country) {
 return _iban_country_get_info($iban_country,'bban_branchid_start_offset');
}

# Get the BBAN Branch ID stop offset for an IBAN country
function iban_country_get_branchid_stop_offset($iban_country) {
 return _iban_country_get_info($iban_country,'bban_branchid_stop_offset');
}

# Get the registry edition for an IBAN country
function iban_country_get_registry_edition($iban_country) {
 return _iban_country_get_info($iban_country,'registry_edition');
}

# Is the IBAN country a SEPA member?
function iban_country_is_sepa($iban_country) {
 return _iban_country_get_info($iban_country,'country_sepa');
}

# Get the list of all IBAN countries
function iban_countries() {
 global $_iban_registry;
 return array_keys($_iban_registry);
}

# Given an incorrect IBAN, return an array of zero or more checksum-valid
# suggestions for what the user might have meant, based upon common
# mistranscriptions.
function iban_mistranscription_suggestions($incorrect_iban) {
 
 # abort on ridiculous length input (but be liberal)
 $length = strlen($incorrect_iban);
 if($length<5 || $length>34) { return array('(length bad)'); }

 # abort if mistranscriptions data is unable to load
 if(!_iban_load_mistranscriptions()) { return array('(failed to load)'); }

 # init
 global $_iban_mistranscriptions;
 $suggestions = array();

 # we have a string of approximately IBAN-like length.
 # ... now let's make suggestions.
 $numbers = array('0','1','2','3','4','5','6','7','8','9');
 for($i=0;$i<$length;$i++) {
  # get the character at this position
  $character = substr($incorrect_iban,$i,1);
  # for each known transcription error resulting in this character
  foreach($_iban_mistranscriptions[$character] as $possible_origin) {
   # if we're:
   #  - in the first 2 characters (country) and the possible replacement
   #    is a letter
   #  - in the 3rd or 4th characters (checksum) and the possible
   #    replacement is a number
   #  - later in the string
   if(($i<2 && !in_array($possible_origin,$numbers)) ||
      ($i>=2 && $i<=3 && in_array($possible_origin,$numbers)) ||
      $i>3) {
    # construct a possible IBAN using this possible origin for the
    # mistranscribed character, replaced at this position only
    $possible_iban = substr($incorrect_iban,0,$i) . $possible_origin .  substr($incorrect_iban,$i+1);
    # if the checksum passes, return it as a possibility
    if(verify_iban($possible_iban)) {
     array_push($suggestions,$possible_iban);
    }
   }
  }
 }

 # now we check for the type of mistransposition case where all of
 # the characters of a certain type within a string were mistransposed.
 #  - first generate a character frequency table
 $char_freqs = array();
 for($i=0;$i<strlen($incorrect_iban);$i++) {
  if(!isset($char_freqs[substr($incorrect_iban,$i,1)])) {
   $char_freqs[substr($incorrect_iban,$i,1)] = 1;
  }
  else {
   $char_freqs[substr($incorrect_iban,$i,1)]++;
  }
 }
 #  - now, for each of the characters in the string...
 foreach($char_freqs as $char=>$freq) {
  # if the character occurs more than once
  if($freq>1) {
   # check the 'all occurrences of <char> were mistranscribed' case
   foreach($_iban_mistranscriptions[$char] as $possible_origin) {
    $possible_iban = str_replace($char,$possible_origin,$incorrect_iban);
    if(verify_iban($possible_iban)) {
     array_push($suggestions,$possible_iban);
    }
   }
  }
 }

 return $suggestions;
}


##### internal use functions - safe to ignore ######

# Load the IBAN registry from disk.
global $_iban_registry;
$_iban_registry = array();
_iban_load_registry();
function _iban_load_registry() {
 global $_iban_registry;
 # if the registry is not yet loaded, or has been corrupted, reload
 if(!is_array($_iban_registry) || count($_iban_registry)<1) {
  $data = file_get_contents(dirname(__FILE__) . '/registry.txt');
  $lines = explode("\n",$data);
  array_shift($lines); # drop leading description line
  # loop through lines
  foreach($lines as $line) {
   if($line!='') {
    # split to fields
    list($country,$country_name,$domestic_example,$bban_example,$bban_format_swift,$bban_format_regex,$bban_length,$iban_example,$iban_format_swift,$iban_format_regex,$iban_length,$bban_bankid_start_offset,$bban_bankid_stop_offset,$bban_branchid_start_offset,$bban_branchid_stop_offset,$registry_edition,$country_sepa) = explode('|',$line);
    # assign to registry
    $_iban_registry[$country] = array(
                                'country'			=>	$country,
 				'country_name'			=>	$country_name,
				'country_sepa'			=>	$country_sepa,
 				'domestic_example'		=>	$domestic_example,
				'bban_example'			=>	$bban_example,
				'bban_format_swift'		=>	$bban_format_swift,
				'bban_format_regex'		=>	$bban_format_regex,
				'bban_length'			=>	$bban_length,
				'iban_example'			=>	$iban_example,
				'iban_format_swift'		=>	$iban_format_swift,
				'iban_format_regex'		=>	$iban_format_regex,
				'iban_length'			=>	$iban_length,
				'bban_bankid_start_offset'	=>	$bban_bankid_start_offset,
				'bban_bankid_stop_offset'	=>	$bban_bankid_stop_offset,
				'bban_branchid_start_offset'	=>	$bban_branchid_start_offset,
				'bban_branchid_stop_offset'	=>	$bban_branchid_stop_offset,
				'registry_edition'		=>	$registry_edition
                               );
   }
  }
 }
}

# Get information from the IBAN registry by example IBAN / code combination
function _iban_get_info($iban,$code) {
 $country = iban_get_country_part($iban);
 return _iban_country_get_info($country,$code);
}

# Get information from the IBAN registry by country / code combination
function _iban_country_get_info($country,$code) {
 global $_iban_registry;
 $country = strtoupper($country);
 $code = strtolower($code);
 if(array_key_exists($country,$_iban_registry)) {
  if(array_key_exists($code,$_iban_registry[$country])) {
   return $_iban_registry[$country][$code];
  }
 }
 return false;
}

# Load common mistranscriptions from disk.
function _iban_load_mistranscriptions() {
 global $_iban_mistranscriptions;
 # do not reload if already present
 if(is_array($_iban_mistranscriptions) && count($_iban_mistranscriptions) == 36) { return true; }
 $_iban_mistranscriptions = array();
 $file = dirname(__FILE__) . '/mistranscriptions.txt';
 if(!file_exists($file) || !is_readable($file)) { return false; }
 $data = file_get_contents($file);
 $lines = explode("\n",$data);
 foreach($lines as $line) {
  # match lines with ' c-<x> = <something>' where x is a word-like character
  if(preg_match('/^ *c-(\w) = (.*?)$/',$line,$matches)) {
   # normalize the character to upper case
   $character = strtoupper($matches[1]);
   # break the possible origins list at '/', strip quotes & spaces
   $chars = explode(' ',str_replace('"','',preg_replace('/ *?\/ *?/','',$matches[2])));
   # assign as possible mistranscriptions for that character
   $_iban_mistranscriptions[$character] = $chars;
  }
 }
 return true;
}

?>
