001package org.xbib.standardnumber; 002 003import org.xbib.standardnumber.check.iso7064.MOD9710; 004 005import java.util.Arrays; 006import java.util.regex.Matcher; 007import java.util.regex.Pattern; 008 009/** 010 * ISO 13616 International Bank Account Number (IBAN) 011 * 012 * The International Bank Account Number (IBAN) is an internationally agreed means of 013 * identifying bank accounts across national borders with a reduced risk of transcription 014 * errors. It was originally adopted by the European Committee for Banking Standards (ECBS) 015 * and later as an international standard under ISO 13616:1997. The current standard 016 * is ISO 13616:2007, which indicates SWIFT as the formal registrar. 017 * 018 * Checksum in accordance to ISO 7064 MOD-97 019 */ 020public class IBAN extends AbstractStandardNumber implements Comparable<IBAN>, StandardNumber { 021 022 /** 023 * Norway = 15, Malta = 31 + "IBAN " 024 * 025 */ 026 private static final Pattern PATTERN = Pattern.compile("[\\p{Alnum}\\-\\s]{15,36}"); 027 028 private String formatted; 029 030 private String value; 031 032 private String country; 033 034 private boolean createWithChecksum; 035 036 @Override 037 public String type() { 038 return "iban"; 039 } 040 041 @Override 042 public int compareTo(IBAN iban) { 043 return iban != null ? normalizedValue().compareTo(iban.normalizedValue()) : -1; 044 } 045 046 @Override 047 public IBAN set(CharSequence value) { 048 this.value = value != null ? value.toString() : null; 049 return this; 050 } 051 052 @Override 053 public IBAN createChecksum(boolean createWithChecksum) { 054 this.createWithChecksum = createWithChecksum; 055 return this; 056 } 057 058 @Override 059 public IBAN normalize() { 060 Matcher m = PATTERN.matcher(value); 061 if (m.find()) { 062 this.value = parse(value.substring(m.start(), m.end())); 063 } else { 064 this.value = null; 065 } 066 if (value != null && createWithChecksum) { 067 int c = check.compute(value.substring(0, value.length()-2)); 068 String chk = String.format("%02d", c); 069 this.value = value + chk; 070 this.formatted = formatted.substring(0,2) + chk + formatted.substring(4); 071 } 072 return this; 073 } 074 075 @Override 076 public boolean isValid() { 077 return value != null && !value.isEmpty() && check(); 078 } 079 080 @Override 081 public IBAN verify() throws NumberFormatException { 082 if (value == null || value.isEmpty()) { 083 throw new NumberFormatException("invalid"); 084 } 085 if (!check()) { 086 throw new NumberFormatException("bad checksum"); 087 } 088 if (formatted.length() != getLengthForCountryCode(country)) { 089 throw new NumberFormatException("invalid length for country: " 090 + formatted.length() + " " + formatted); 091 } 092 return this; 093 } 094 095 @Override 096 public String normalizedValue() { 097 return formatted; 098 } 099 100 @Override 101 public String format() { 102 return formatted; 103 } 104 105 @Override 106 public IBAN reset() { 107 this.value = null; 108 this.formatted = null; 109 this.country = null; 110 this.createWithChecksum = false; 111 return this; 112 } 113 114 private final static MOD9710 check = new MOD9710(); 115 116 private boolean check() { 117 return value != null && !value.isEmpty() && check.verify(value); 118 } 119 120 private String parse(String raw) { 121 StringBuilder sb = new StringBuilder(raw); 122 int i = sb.indexOf("-"); 123 while (i >= 0) { 124 sb.deleteCharAt(i); 125 i = sb.indexOf("-"); 126 } 127 i = sb.indexOf(" "); 128 while (i >= 0) { 129 sb.deleteCharAt(i); 130 i = sb.indexOf(" "); 131 } 132 this.formatted = sb.toString(); 133 this.country = sb.substring(0,2); 134 // move first 4 characters to last 135 sb = new StringBuilder(sb.substring(4)).append(sb.substring(0,4)); 136 // replace characters with decimal values 137 for (i = 0; i < sb.length(); i++) { 138 char ch = sb.charAt(i); 139 if (ch >= 'A' && ch <= 'Z') { 140 sb.deleteCharAt(i); 141 String s = Integer.toString(ch - 'A' + 10); 142 sb.insert(i, s); 143 } 144 } 145 return sb.toString(); 146 } 147 148 /** 149 * Known country codes, this list must be sorted to allow binary search. 150 */ 151 private static final String[] COUNTRY_CODES = { 152 "AD", "AE", "AL", "AO", "AT", "AZ", "BA", "BE", "BF", "BG", "BH", "BI", "BJ", "BR", "CG", "CH", "CI", 153 "CM", "CR", "CV", "CY", "CZ", "DE", "DK", "DO", "DZ", "EE", "EG", "ES", "FI", "FO", "FR", "GA", "GB", 154 "GE", "GI", "GL", "GR", "GT", "HR", "HU", "IE", "IL", "IR", "IS", "IT", "KW", "KZ", "LB", "LI", "LT", 155 "LU", "LV", "MC", "MD", "ME", "MG", "MK", "ML", "MR", "MT", "MU", "MZ", "NL", "NO", "PK", "PL", "PS", 156 "PT", "RO", "RS", "SA", "SE", "SI", "SK", "SM", "SN", "TN", "TR", "UA", "VG" }; 157 /** 158 * Lengths for each country's IBAN. The indices match the indices of {@link #COUNTRY_CODES}, the values are the expected length. 159 */ 160 private static final int[] COUNTRY_IBAN_LENGTHS = { 161 24 /* AD */, 23 /* AE */, 28 /* AL */, 25 /* AO */, 20 /* AT */, 28 /* AZ */, 20 /* BA */, 16 /* BE */, 162 27 /* BF */, 22 /* BG */, 22 /* BH */, 16 /* BI */, 28 /* BJ */, 29 /* BR */, 27 /* CG */, 21 /* CH */, 163 28 /* CI */, 27 /* CM */, 21 /* CR */, 25 /* CV */, 28 /* CY */, 24 /* CZ */, 22 /* DE */, 18 /* DK */, 164 28 /* DO */, 24 /* DZ */, 20 /* EE */, 27 /* EG */, 24 /* ES */, 18 /* FI */, 18 /* FO */, 27 /* FR */, 165 27 /* GA */, 22 /* GB */, 22 /* GE */, 23 /* GI */, 18 /* GL */, 27 /* GR */, 28 /* GT */, 21 /* HR */, 166 28 /* HU */, 22 /* IE */, 23 /* IL */, 26 /* IR */, 26 /* IS */, 27 /* IT */, 30 /* KW */, 20 /* KZ */, 167 28 /* LB */, 21 /* LI */, 20 /* LT */, 20 /* LU */, 21 /* LV */, 27 /* MC */, 24 /* MD */, 22 /* ME */, 168 27 /* MG */, 19 /* MK */, 28 /* ML */, 27 /* MR */, 31 /* MT */, 30 /* MU */, 25 /* MZ */, 18 /* NL */, 169 15 /* NO */, 24 /* PK */, 28 /* PL */, 29 /* PS */, 25 /* PT */, 24 /* RO */, 22 /* RS */, 24 /* SA */, 170 24 /* SE */, 19 /* SI */, 24 /* SK */, 27 /* SM */, 28 /* SN */, 24 /* TN */, 26 /* TR */, 29 /* UA */, 171 24 /* VG */ }; 172 173 /** 174 * Returns the IBAN length for a given country code. 175 * @param countryCode a non-null, uppercase, two-character country code. 176 * @return the IBAN length for the given country, or -1 if the input is not a known, two-character country code. 177 * @throws NullPointerException if the input is null. 178 */ 179 private int getLengthForCountryCode(String countryCode) { 180 int index = Arrays.binarySearch(COUNTRY_CODES, countryCode); 181 if (index > -1) { 182 return COUNTRY_IBAN_LENGTHS[index]; 183 } 184 return -1; 185 } 186 187}