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}