IBAN.java

// SECTION-START[License Header]
// <editor-fold defaultstate="collapsed" desc=" Generated License ">
/*
 *   jDTAUS ⁑ ISO-13616
 *   Copyright (C) Christian Schulte, 2013-222
 *
 *   Permission to use, copy, modify, and/or distribute this software for any
 *   purpose with or without fee is hereby granted, provided that the above
 *   copyright notice and this permission notice appear in all copies.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 *   WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 *   MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 *   ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 *   WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 *   ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 *   OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 *   $JDTAUS: IBAN.java 8925 2017-09-03 04:47:22Z schulte $
 *
 */
// </editor-fold>
// SECTION-END
package org.jdtaus.iso13616;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.math.BigInteger;
import java.net.URL;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Formattable;
import java.util.Formatter;
import java.util.HashMap;
import java.util.IllegalFormatFlagsException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

import static java.util.FormattableFlags.ALTERNATE;
import static java.util.FormattableFlags.LEFT_JUSTIFY;
import static java.util.FormattableFlags.UPPERCASE;

// SECTION-START[Documentation]
// <editor-fold defaultstate="collapsed" desc=" Generated Documentation ">
/**
 *
 * International Bank Account Number.
 * <p>The IBAN structure is defined in ISO 13616-1 and consists of a two-letter ISO 3166-1 country code, followed by two
 * check digits and up to thirty alphanumeric characters for a BBAN (Basic Bank Account Number) which has a fixed length
 * per country and, included within it, a bank identifier with a fixed position and a fixed length per country. The
 * check digits are calculated based on the scheme defined in ISO/IEC 7064 (MOD97-10). The Society for Worldwide
 * Interbank Financial Telecommunication SCRL, SWIFT, has been designated by the ISO Technical Management Board to act
 * as the Registration Authority for ISO 13616. Nationally-agreed, ISO 13616-compliant IBAN formats are submitted to the
 * registration authority exclusively by the National Standards Body or the National Central Bank of the country. For
 * further information see the <a href="../../../doc-files/IBAN-Registry_Release-78-August-2017.pdf">IBAN REGISTRY</a>.
 * An updated version of the document may be found at <a href="http://www.swift.com">SWIFT</a>.</p>
 *
 * <dl>
 *   <dt><b>Identifier:</b></dt><dd>jDTAUS ⁑ ISO-13616 ⁑ IBAN</dd>
 *   <dt><b>Name:</b></dt><dd>jDTAUS ⁑ ISO-13616 ⁑ IBAN</dd>
 *   <dt><b>Abstract:</b></dt><dd>No</dd>
 *   <dt><b>Final:</b></dt><dd>Yes</dd>
 *   <dt><b>Stateless:</b></dt><dd>No</dd>
 * </dl>
 *
 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
 * @version 1.1
 */
// </editor-fold>
// SECTION-END
// SECTION-START[Annotations]
// <editor-fold defaultstate="collapsed" desc=" Generated Annotations ">
@javax.annotation.Generated( value = "org.jomc.tools.SourceFileProcessor 1.9", comments = "See http://www.jomc.org/jomc/1.9/jomc-tools-1.9" )
// </editor-fold>
// SECTION-END
public final class IBAN implements CharSequence, Comparable<IBAN>, Formattable, Serializable
{
    // SECTION-START[IBAN]

    /**
     * International bank account number structure.
     * <p>The following character representations are used:
     * <table border="0">
     * <tr><td>a</td><td>Upper case letters (alphabetic characters A-Z only)</td></tr>
     * <tr><td>c</td><td>Upper and lower case alphanumeric characters (A-Z, a-z and 0-9)</td></tr>
     * <tr><td>e</td><td>Blank space</td></tr>
     * <tr><td>n</td><td>Digits (numeric characters 0 to 9 only)</td></tr>
     * </table></p>
     * <p>The following length indications are used:
     * <table border="0">
     * <tr><td>nn!</td><td>fixed length</td></tr>
     * <tr><td>nn</td><td>maximum length</td></tr>
     * </table></p>
     *
     * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
     * @version $JDTAUS: IBAN.java 8925 2017-09-03 04:47:22Z schulte $
     */
    private static final class Structure
    {

        /**
         * Part of a structure.
         *
         * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
         * @version $JDTAUS: IBAN.java 8925 2017-09-03 04:47:22Z schulte $
         */
        private static final class Part
        {

            /** Type of the part. */
            private final char type;

            /** The literal of the part or {@code null}. */
            private final String literal;

            /** The fixed length of the part or {@code null}. */
            private final Integer fixedLength;

            /** The maximum length of the part or {@code null}. */
            private final Integer maximumLength;

            /**
             * Creates a new {@code Part} instance.
             *
             * @param type The type of the part.
             * @param literal The literal of the part or {@code null}.
             * @param fixedLength The fixed length of the part or {@code null}.
             * @param maximumLength The maximum length of the part or {@code null}.
             */
            private Part( final char type, final String literal, final Integer fixedLength,
                          final Integer maximumLength )
            {
                super();
                this.type = type;
                this.literal = literal;
                this.fixedLength = fixedLength;
                this.maximumLength = maximumLength;
            }

            /**
             * Gets the type of the part.
             * <p>The following types are used:
             * <table border="0">
             * <tr><td>a</td><td>Upper case letters (alphabetic characters A-Z only)</td></tr>
             * <tr><td>c</td><td>Upper and lower case alphanumeric characters (A-Z, a-z and 0-9)</td></tr>
             * <tr><td>e</td><td>Blank space</td></tr>
             * <tr><td>n</td><td>Digits (numeric characters 0 to 9 only)</td></tr>
             * <tr><td>l</td><td>Literal</td></tr>
             * </table></p>
             *
             * @return The type of the part.
             */
            private char getType()
            {
                return this.type;
            }

            /**
             * Gets the literal of the part.
             *
             * @return The literal of the part or {@code null}, if the type of the part is not equal to {@code l}.
             */
            private String getLiteral()
            {
                return this.literal;
            }

            /**
             * Gets the fixed length of the part.
             *
             * @return The fixed length of the part or {@code null}, if the part has no fixed length but a maximum
             * length.
             */
            private Integer getFixedLength()
            {
                return this.fixedLength;
            }

            /**
             * Gets the maximum length of the part.
             *
             * @return The maximum length of the part or {@code null}, if the part has no maximum length but a fixed
             * length.
             */
            private Integer getMaximumLength()
            {
                return this.maximumLength;
            }

        }

        /** The parts of the structure. */
        private final List<Part> parts;

        /**
         * Creates a new {@code Structure} instance.
         *
         * @param parts The parts of the structure.
         */
        private Structure( final List<Part> parts )
        {
            super();
            this.parts = parts;
        }

        /**
         * Gets the parts of the structure.
         *
         * @return The parts of the structure.
         */
        private List<Part> getParts()
        {
            return this.parts;
        }

    }

    /**
     * Context of a parse operation.
     *
     * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
     * @version $JDTAUS: IBAN.java 8925 2017-09-03 04:47:22Z schulte $
     */
    private static final class ParseContext
    {

        /** The text to parse. */
        private final String text;

        /** The {@code ParsePosition} of the context. */
        private final ParsePosition parsePosition;

        /** The format to parse. */
        private final IbanFormat format;

        /** The number of electronic format characters parsed. */
        private int length;

        /** The previous character parsed. */
        private Character previous;

        /**
         * Creates a new {@code ParseContext} instance.
         *
         * @param text The text to parse.
         * @param pos The {@code ParsePosition} backing the parse.
         * @param format The format to parse.
         */
        private ParseContext( final String text, final ParsePosition pos, final IbanFormat format )
        {
            super();
            this.text = text;
            this.parsePosition = pos;
            this.format = format;
            this.length = 0;
        }

    }

    /** Serial version UID for backwards compatibility with 2007.45.x classes. */
    private static final long serialVersionUID = -8123668345632147105L;

    /** Used to cache instances. */
    private static volatile Reference<Map<String, IBAN>> cacheReference = new SoftReference<Map<String, IBAN>>( null );

    /** Mappings of two-letter ISO 3166-1 country codes to SEPA country flags. */
    private static final Map<String, Boolean> SEPA_COUNTRY_FLAGS = new HashMap<String, Boolean>( 128 );

    /** Mappings of two-letter ISO 3166-1 country codes to BBAN structures. */
    private static final Map<String, Structure> BBAN_STRUCTURES = new HashMap<String, Structure>( 128 );

    /** Mappings of two-letter ISO 3166-1 country codes to BBAN bank identifier indices. */
    private static final Map<String, List<Integer>> BBAN_BANK_ID_INDICES = new HashMap<String, List<Integer>>( 128 );

    /** Mappings of two-letter ISO 3166-1 country codes to BBAN branch identifier indices. */
    private static final Map<String, List<Integer>> BBAN_BRANCH_ID_INDICES = new HashMap<String, List<Integer>>( 128 );

    /** Mappings of two-letter ISO 3166-1 country codes to IBAN structures. */
    private static final Map<String, Structure> IBAN_STRUCTURES = new HashMap<String, Structure>( 128 );

    /** Mappings of two-letter ISO 3166-1 country codes to IBAN country codes. */
    private static final Map<String, String> IBAN_COUNTRY_CODES = new HashMap<String, String>( 128 );

    /** List of supported two-letter ISO 3166-1 country codes. */
    private static final List<String> COUNTRIES = new ArrayList<String>( 128 );

    /** {@code BigInteger} constant {@code 97}. */
    private static final BigInteger INTEGER_97 = BigInteger.valueOf( 97L );

    /** {@code BigInteger} constant {@code 98}. */
    private static final BigInteger INTEGER_98 = BigInteger.valueOf( 98L );

    static
    {
        InputStream in = null;

        try
        {
            final Properties properties = new Properties();
            final URL resource = IBAN.class.getResource( "IBAN.properties" );
            assert resource != null : "Expected resource 'IBAN.properties' to exist.";

            if ( resource != null )
            {
                in = resource.openStream();
                properties.load( in );
                in.close();
                in = null;

                if ( properties.containsKey( "countries" ) )
                {
                    final String[] countries = properties.getProperty( "countries" ).split( "\\|" );
                    COUNTRIES.addAll( Arrays.asList( countries ) );

                    for ( int i = 0, s0 = countries.length; i < s0; i++ )
                    {
                        if ( countries[i].length() != 2 )
                        {
                            throw new AssertionError( countries[i] );
                        }

                        final String bbanStructure = properties.getProperty( countries[i] + ".bban.structure" );
                        final String bbanBankIds = properties.getProperty( countries[i] + ".bban.bankid.indices" );
                        final String bbanBranchIds = properties.getProperty( countries[i] + ".bban.branchid.indices" );
                        final String ibanCountryCode = properties.getProperty( countries[i] + ".iban.countrycode" );
                        final String ibanStructure = properties.getProperty( countries[i] + ".iban.structure" );
                        final String sepa = properties.getProperty( countries[i] + ".sepa" );

                        SEPA_COUNTRY_FLAGS.put( countries[i], Boolean.valueOf( sepa ) );

                        if ( ibanCountryCode != null )
                        {
                            IBAN_COUNTRY_CODES.put( countries[i], ibanCountryCode );
                        }

                        if ( bbanStructure != null )
                        {
                            BBAN_STRUCTURES.put( countries[i], parseStructure( bbanStructure ) );

                            if ( bbanBankIds != null )
                            {
                                BBAN_BANK_ID_INDICES.put( countries[i], parseIndices( bbanBankIds ) );
                            }

                            if ( bbanBranchIds != null )
                            {
                                BBAN_BRANCH_ID_INDICES.put( countries[i], parseIndices( bbanBranchIds ) );
                            }
                        }

                        if ( ibanStructure != null )
                        {
                            IBAN_STRUCTURES.put( countries[i], parseStructure( ibanStructure ) );
                        }
                    }
                }
            }
        }
        catch ( final ParseException e )
        {
            throw new AssertionError( e );
        }
        catch ( final IOException e )
        {
            throw new AssertionError( e );
        }
        finally
        {
            try
            {
                if ( in != null )
                {
                    in.close();
                }
            }
            catch ( final IOException e )
            {
                throw new AssertionError( e );
            }
        }
    }

    /** Maximum number of characters of an {@code IBAN}. */
    public static final int MAX_CHARACTERS = 42;

    /**
     * The two-letter ISO 3166-1 country code of the IBAN.
     * @serial
     */
    private String countryCode;

    /**
     * Flag indicating the country of the IBAN is part of the jurisdictional scope of the SEPA Schemes.
     * @serial
     */
    private boolean sepaCountry;

    /**
     * The bank identifier part of the BBAN of the IBAN.
     * @serial
     */
    private String bankIdentifier;

    /**
     * The branch identifier part of the BBAN of the IBAN.
     * @serial
     */
    private String branchIdentifier;

    /**
     * The electronic format of the IBAN.
     * @serial
     */
    private String electronicFormat;

    /**
     * The letter format of the IBAN.
     * @serial
     */
    private String letterFormat;

    /**
     * The parts of the IBAN.
     * @serial
     */
    private Comparable[] parts;

    /** Cached string representation of the instance. */
    private transient String string;

    /**
     * Creates a new {@code IBAN} instance.
     *
     * @param countryCode The two-letter ISO 3166-1 country code of the IBAN.
     * @param sepaCountry Flag indicating the country is part of the jurisdictional scope of the SEPA Schemes.
     * @param bankIdentifier The bank identifier part of the BBAN of the IBAN.
     * @param branchIdentifier The branch identifier part of the BBAN of the IBAN or {@code null}.
     * @param electronicFormat The electronic format representation of the IBAN.
     * @param letterFormat The letter format representation of the IBAN.
     * @param parts The parts of the IBAN.
     *
     * @see #parse(String, ParsePosition)
     */
    private IBAN( final String countryCode, final boolean sepaCountry, final String bankIdentifier,
                  final String branchIdentifier, final String electronicFormat, final String letterFormat,
                  final Comparable[] parts )
    {
        super();
        this.countryCode = countryCode;
        this.sepaCountry = sepaCountry;
        this.bankIdentifier = bankIdentifier;
        this.branchIdentifier = branchIdentifier;
        this.electronicFormat = electronicFormat;
        this.letterFormat = letterFormat;
        this.parts = parts;
        getCache().put( electronicFormat, this );
        getCache().put( letterFormat, this );
    }

    /**
     * Gets an array holding two-letter ISO 3166-1 country codes of all countries that have implemented the IBAN
     * standard.
     *
     * @return An array holding two-letter ISO 3166-1 country codes of all countries that have implemented the IBAN
     * standard.
     */
    public static String[] getCountryCodes()
    {
        return COUNTRIES.toArray( new String[ COUNTRIES.size() ] );
    }

    /**
     * Tests a given two-letter ISO 3166-1 country code to identify a country that is part of the jurisdictional scope
     * of the Single Euro Payment Area (SEPA) Schemes.
     * The <a href="../../../doc-files/EPC409-09_EPC_List_of_SEPA_Scheme_Countries_v2_4_-_April_2016.pdf">
     * EPC List of SEPA Scheme Countries</a> document lists the countries and territories which are part of the
     * jurisdictional scope of the Single Euro Payment Area (SEPA) Schemes. An updated version of the document may be
     * found at <a href="http://www.europeanpaymentscouncil.eu">The European Payments Council (EPC)</a>.
     *
     * @param countryCode The two-letter ISO 3166-1 country code to test.
     *
     * @return {@code true}, if the country identified by {@code countryCode} is part of the jurisdictional scope of the
     * Single Euro Payment Area (SEPA) Schemes; {@code false}, if not.
     *
     * @throws NullPointerException if {@code countryCode} is {@code null}.
     *
     * @see #getCountryCodes()
     * @since 2007.46
     */
    public static boolean isSepaCountry( final String countryCode )
    {
        if ( countryCode == null )
        {
            throw new NullPointerException( "countryCode" );
        }

        final Boolean sepaCountry = SEPA_COUNTRY_FLAGS.get( countryCode );
        return sepaCountry != null ? sepaCountry : false;
    }

    /**
     * Gets the two-letter ISO 3166-1 country code identifying the country the IBAN belongs to.
     *
     * @return The two-letter ISO 3166-1 country code identifying the country the IBAN belongs to.
     *
     * @see #getCountryCodes()
     */
    public String getCountryCode()
    {
        return this.countryCode;
    }

    /**
     * Gets a flag indicating the country of the IBAN to be part of the jurisdictional scope of the Single Euro Payment
     * Area (SEPA) Schemes.
     *
     * @return {@code true}, if the country of the IBAN is part of the jurisdictional scope of the Single Euro Payment
     * Area (SEPA) Schemes; {@code false}, else.
     *
     * @see #getCountryCode()
     * @see #isSepaCountry(java.lang.String)
     */
    public boolean isSepaCountry()
    {
        return this.sepaCountry;
    }

    /**
     * Gets the bank identifier part of the BBAN of the IBAN.
     *
     * @return The bank identifier part of the BBAN of the IBAN or {@code null}.
     */
    public String getBankIdentifier()
    {
        return this.bankIdentifier;
    }

    /**
     * Gets the branch identifier part of the BBAN of the IBAN.
     *
     * @return The branch identifier part of the BBAN of the IBAN or {@code null}.
     */
    public String getBranchIdentifier()
    {
        return this.branchIdentifier;
    }

    /**
     * Parses text from a BBAN string to produce an {@code IBAN} instance.
     *
     * @param countryCode The two-letter ISO 3166-1 country code of the IBAN to create.
     * @param bban A string to parse BBAN characters from.
     *
     * @return The parsed value.
     *
     * @throws NullPointerException if {@code countryCode} or {@code bban} is {@code null}.
     * @throws IllegalArgumentException if the country identified by {@code countryCode} has not implemented the IBAN
     * standard, that is, {@code countryCode} is not contained in the array returned by method {@code getCountryCodes}.
     * @throws IbanSyntaxException if the parse fails or the length of {@code bban} is invalid.
     *
     * @see #getCountryCodes()
     * @see #valueOf(java.lang.String, java.lang.String)
     */
    public static IBAN parse( final String countryCode, final String bban ) throws IbanSyntaxException
    {
        if ( countryCode == null )
        {
            throw new NullPointerException( "countryCode" );
        }
        if ( bban == null )
        {
            throw new NullPointerException( "bban" );
        }

        final String ibanCountryCode = toIbanCountryCode( countryCode );
        final Structure structure = BBAN_STRUCTURES.get( ibanCountryCode );

        if ( structure == null )
        {
            throw new IllegalArgumentException( countryCode );
        }

        final List<Integer> bankIdIndices = BBAN_BANK_ID_INDICES.get( ibanCountryCode );
        final List<Integer> branchIdIndices = BBAN_BRANCH_ID_INDICES.get( ibanCountryCode );
        final boolean sepa_country = SEPA_COUNTRY_FLAGS.get( countryCode );

        // Parse the parts.
        final StringBuilder electronic_format_builder = new StringBuilder( MAX_CHARACTERS );
        final StringBuilder bank_id_builder = new StringBuilder( MAX_CHARACTERS );
        final StringBuilder branch_id_builder = new StringBuilder( MAX_CHARACTERS );
        final List<Comparable<?>> comparables = new ArrayList<Comparable<?>>( structure.getParts().size() + 2 );
        final ParseContext context =
            new ParseContext( bban, new ParsePosition( 0 ), bban.length() > 4 && bban.charAt( 4 ) == ' '
                                                                ? IbanFormat.PRINT : IbanFormat.ELECTRONIC );

        for ( int p = 0, s0 = structure.getParts().size(); p < s0 && context.parsePosition.getErrorIndex() < 0; p++ )
        {
            final Structure.Part part = structure.getParts().get( p );
            final String chars = parsePart( context, part );

            if ( context.parsePosition.getErrorIndex() < 0 )
            {
                if ( part.getFixedLength() != null && chars.length() != part.getFixedLength().intValue() )
                { // Fixed length mismatch.
                    throw new IbanSyntaxException( bban, context.parsePosition.getIndex() );
                }

                electronic_format_builder.append( chars );

                switch ( part.getType() )
                {
                    case 'n':
                        comparables.add( new BigInteger( chars ) );
                        break;
                    default:
                        comparables.add( chars );
                        break;
                }
            }
            else
            { // Invalid part.
                throw new IbanSyntaxException( bban, context.parsePosition.getErrorIndex() );
            }
        }

        // Extract bank id and branch id.
        if ( bankIdIndices != null )
        {
            bank_id_builder.append( electronic_format_builder.subSequence( bankIdIndices.get( 0 ),
                                                                           bankIdIndices.get( 1 ) ) );

        }

        if ( branchIdIndices != null )
        {
            branch_id_builder.append( electronic_format_builder.subSequence( branchIdIndices.get( 0 ),
                                                                             branchIdIndices.get( 1 ) ) );

        }

        // Calculate checksum.
        final StringBuilder integer_builder = new StringBuilder( MAX_CHARACTERS * 2 );
        appendIso7064Digits( integer_builder, electronic_format_builder.toString() );
        appendIso7064Digits( integer_builder, ibanCountryCode );
        appendIso7064Digits( integer_builder, "00" );

        final BigInteger integer = new BigInteger( integer_builder.toString() );
        final BigInteger checksum = INTEGER_98.subtract( integer.remainder( INTEGER_97 ) );
        final String checksumPart = checksum.compareTo( BigInteger.TEN ) < 0 ? "0" + checksum : checksum.toString();

        comparables.add( 0, checksumPart );
        comparables.add( 0, ibanCountryCode );

        electronic_format_builder.insert( 0, checksumPart );
        electronic_format_builder.insert( 0, ibanCountryCode );

        return new IBAN( countryCode, sepa_country, bank_id_builder.length() > 0 ? bank_id_builder.toString() : null,
                         branch_id_builder.length() > 0 ? branch_id_builder.toString() : null,
                         electronic_format_builder.toString(), toLetterFormat( electronic_format_builder.toString() ),
                         comparables.toArray( new Comparable<?>[ comparables.size() ] ) );

    }

    /**
     * Parses text from a string to produce an {@code IBAN} instance.
     * <p>The method attempts to parse text starting at the index given by {@code pos}. If parsing succeeds, then the
     * index of {@code pos} is updated to the index after the last character used (parsing does not necessarily use all
     * characters up to the end of the string), and the parsed value is returned. The updated {@code pos} can be used to
     * indicate the starting point for the next call to this method.</p>
     *
     * @param text A string to parse IBAN characters from.
     * @param pos A {@code ParsePosition} object with index and error index information as described above.
     *
     * @return The parsed value or {@code null}, if the parse fails.
     *
     * @throws NullPointerException if {@code text} or {@code pos} is {@code null}.
     * @throws IbanCheckDigitsException if check digits validation of the parsed value fails.
     */
    public static IBAN parse( final String text, final ParsePosition pos ) throws IbanCheckDigitsException
    {
        if ( text == null )
        {
            throw new NullPointerException( "text" );
        }
        if ( pos == null )
        {
            throw new NullPointerException( "pos" );
        }

        IBAN ret = null;
        final int begin_index = pos.getIndex();

        // Extract the country code to query properties.
        if ( text.length() > begin_index + 1 )
        {
            final String country_code = text.substring( begin_index, begin_index + 2 );

            if ( !isLiteral( country_code.charAt( 0 ) ) )
            {
                pos.setIndex( begin_index );
                pos.setErrorIndex( begin_index );
            }
            else if ( !isLiteral( country_code.charAt( 1 ) ) )
            {
                pos.setIndex( begin_index );
                pos.setErrorIndex( begin_index + 1 );
            }
            else
            {
                final Structure structure = IBAN_STRUCTURES.get( country_code );

                if ( structure != null )
                { // Parse the parts.
                    final boolean sepa_country = SEPA_COUNTRY_FLAGS.get( country_code );
                    final StringBuilder electronic_format_builder = new StringBuilder( MAX_CHARACTERS );
                    final StringBuilder bank_id_builder = new StringBuilder( MAX_CHARACTERS );
                    final StringBuilder branch_id_builder = new StringBuilder( MAX_CHARACTERS );
                    final List<Comparable<?>> comparables = new ArrayList<Comparable<?>>( structure.getParts().size() );
                    final ParseContext context =
                        new ParseContext( text, pos,
                                          text.length() > begin_index + 4 && text.charAt( begin_index + 4 ) == ' '
                                              ? IbanFormat.PRINT : IbanFormat.ELECTRONIC );

                    for ( int p = 0, s0 = structure.getParts().size();
                          p < s0 && context.parsePosition.getErrorIndex() < 0;
                          p++ )
                    {
                        final Structure.Part part = structure.getParts().get( p );
                        final String chars = parsePart( context, part );

                        if ( context.parsePosition.getErrorIndex() < 0 )
                        {
                            if ( part.getFixedLength() != null && chars.length() != part.getFixedLength().intValue() )
                            { // Fixed length mismatch.
                                context.parsePosition.setErrorIndex( context.parsePosition.getIndex() );
                                context.parsePosition.setIndex( begin_index );
                                break;
                            }

                            electronic_format_builder.append( chars );

                            switch ( part.getType() )
                            {
                                case 'n':
                                    comparables.add( new BigInteger( chars ) );
                                    break;
                                default:
                                    comparables.add( chars );
                                    break;
                            }
                        }
                    }

                    if ( context.parsePosition.getErrorIndex() < 0 )
                    { // Validate checksum.
                        final StringBuilder integer_builder = new StringBuilder( MAX_CHARACTERS * 2 );
                        appendIso7064Digits( integer_builder, electronic_format_builder.substring( 4 ) );
                        appendIso7064Digits( integer_builder, electronic_format_builder.substring( 0, 4 ) );

                        final BigInteger integer = new BigInteger( integer_builder.toString() );

                        if ( integer.remainder( INTEGER_97 ).equals( BigInteger.ONE ) )
                        {
                            final List<Integer> bankIdIndices = BBAN_BANK_ID_INDICES.get( country_code );
                            final List<Integer> branchIdIndices = BBAN_BRANCH_ID_INDICES.get( country_code );

                            if ( bankIdIndices != null )
                            {
                                bank_id_builder.append( electronic_format_builder.subSequence(
                                    bankIdIndices.get( 0 ) + 4, bankIdIndices.get( 1 ) + 4 ) );

                            }

                            if ( branchIdIndices != null )
                            {
                                branch_id_builder.append( electronic_format_builder.subSequence(
                                    branchIdIndices.get( 0 ) + 4, branchIdIndices.get( 1 ) + 4 ) );

                            }

                            ret = new IBAN( country_code, sepa_country,
                                            bank_id_builder.length() > 0 ? bank_id_builder.toString() : null,
                                            branch_id_builder.length() > 0 ? branch_id_builder.toString() : null,
                                            electronic_format_builder.toString(),
                                            toLetterFormat( electronic_format_builder.toString() ),
                                            comparables.toArray( new Comparable<?>[ comparables.size() ] ) );

                        }
                        else
                        { // Invalid checksum.
                            context.parsePosition.setIndex( begin_index );
                            context.parsePosition.setErrorIndex( begin_index + 2 );

                            throw new IbanCheckDigitsException( electronic_format_builder.toString(),
                                                                (Number) comparables.get( 1 ) );

                        }
                    }
                }
                else
                { // Unsupported country code.
                    pos.setIndex( begin_index );
                    pos.setErrorIndex( begin_index + 1 );
                }
            }
        }
        else
        { // No country code provided.
            pos.setIndex( begin_index );
            pos.setErrorIndex( begin_index );

            if ( begin_index < text.length() && isLiteral( text.charAt( begin_index ) ) )
            {
                pos.setErrorIndex( begin_index + 1 );
            }
        }

        return ret;
    }

    /**
     * Parses text from the beginning of the given string to produce an {@code IBAN} instance.
     * <p>Unlike the {@link #parse(String, ParsePosition)} method this method throws an {@code IbanSyntaxException} if
     * {@code text} cannot be parsed or is of invalid length.</p>
     *
     * @param text A string to parse IBAN characters from.
     *
     * @return The parsed value.
     *
     * @throws NullPointerException if {@code text} is {@code null}.
     * @throws IbanSyntaxException if the parse fails or the length of {@code text} is invalid.
     * @throws IbanCheckDigitsException if check digits validation of the parsed value fails.
     *
     * @see #valueOf(java.lang.String)
     */
    public static IBAN parse( final String text ) throws IbanSyntaxException, IbanCheckDigitsException
    {
        if ( text == null )
        {
            throw new NullPointerException( "text" );
        }

        IBAN iban = getCache().get( text );

        if ( iban == null )
        {
            final ParsePosition pos = new ParsePosition( 0 );
            iban = IBAN.parse( text, pos );

            if ( iban == null || pos.getErrorIndex() != -1 || pos.getIndex() < text.length() )
            {
                throw new IbanSyntaxException( text, pos.getErrorIndex() != -1
                                                         ? pos.getErrorIndex()
                                                         : pos.getIndex() );

            }
        }

        return iban;
    }

    /**
     * Parses text from the beginning of the given string to produce an {@code IBAN} instance.
     * <p>Unlike the {@link #parse(String)} method this method throws an {@code IllegalArgumentException} if
     * {@code text} cannot be parsed, is of invalid length or check digits validation fails.</p>
     *
     * @param text A string to parse IBAN characters from.
     *
     * @return The parsed value.
     *
     * @throws NullPointerException if {@code text} is {@code null}.
     * @throws IllegalArgumentException if the parse fails, the length of {@code text} is invalid or if check digits
     * validation of the parsed value fails.
     *
     * @see #parse(java.lang.String)
     */
    public static IBAN valueOf( final String text )
    {
        if ( text == null )
        {
            throw new NullPointerException( "text" );
        }

        try
        {
            return IBAN.parse( text );
        }
        catch ( final IbanSyntaxException e )
        {
            throw new IllegalArgumentException( text, e );
        }
        catch ( final IbanCheckDigitsException e )
        {
            throw new IllegalArgumentException( text, e );
        }
    }

    /**
     * Parses text from a BBAN string to produce an {@code IBAN} instance.
     * <p>Unlike the {@link #parse(String, String)} method this method throws an {@code IllegalArgumentException} if the
     * parse fails or the length of {@code bban} is invalid.</p>
     *
     * @param countryCode The two-letter ISO 3166-1 country code of the IBAN to create.
     * @param bban A string to parse BBAN characters from.
     *
     * @return The parsed value.
     *
     * @throws NullPointerException if {@code countryCode} or {@code bban} is {@code null}.
     * @throws IllegalArgumentException if the country identified by {@code countryCode} has not implemented the IBAN
     * standard, that is, {@code countryCode} is not contained in the array returned by method {@code getCountryCodes},
     * if the parse fails, or if the length of {@code bban} is invalid.
     *
     * @see #parse(java.lang.String, java.lang.String)
     * @since 2007.46
     */
    public static IBAN valueOf( final String countryCode, final String bban )
    {
        if ( countryCode == null )
        {
            throw new NullPointerException( "countryCode" );
        }
        if ( bban == null )
        {
            throw new NullPointerException( "bban" );
        }

        try
        {
            return IBAN.parse( countryCode, bban );
        }
        catch ( final IbanSyntaxException e )
        {
            throw new IllegalArgumentException( bban, e );
        }
    }

    /**
     * Checks a given character to belong to the IBAN alphabet.
     *
     * @param c The character to check.
     *
     * @return {@code true} if {@code c} is a character of the IBAN alphabet; {@code false}, else.
     */
    public static boolean isIbanAlphabet( final char c )
    {
        return ( c >= 'A' && c <= 'Z' ) || ( c >= 'a' && c <= 'z' ) || ( c >= '0' && c <= '9' );
    }

    /**
     * Parses text from a string to peek at a national {@code IBAN} format.
     * <p>This method peeks at the given string to test whether it denotes a possibly valid national IBAN format.</p>
     *
     * @param text A string to peek at.
     *
     * @return {@code true}, if {@code text} denotes a (possibly partially) valid national IBAN format; {@code false},
     * if {@code text} does not denote a valid national IBAN format.
     *
     * @throws NullPointerException if {@code text} is {@code null}.
     * @throws IbanCheckDigitsException if check digits validation fails.
     */
    public static boolean peekIban( final String text ) throws IbanCheckDigitsException
    {
        if ( text == null )
        {
            throw new NullPointerException( "text" );
        }

        boolean valid = true;
        boolean complete = true;

        // Extract the country code to query properties.
        if ( text.length() > 1 )
        {
            final String country_code = text.substring( 0, 2 );
            final Structure structure = IBAN_STRUCTURES.get( country_code );

            if ( structure != null )
            { // Parse the parts.
                final StringBuilder electronic_format_builder = new StringBuilder( MAX_CHARACTERS );
                final ParseContext context =
                    new ParseContext( text, new ParsePosition( 0 ), text.length() > 4 && text.charAt( 4 ) == ' '
                                                                        ? IbanFormat.PRINT : IbanFormat.ELECTRONIC );

                for ( int p = 0, s0 = structure.getParts().size();
                      p < s0 && context.parsePosition.getErrorIndex() < 0 && complete;
                      p++ )
                {
                    final Structure.Part part = structure.getParts().get( p );
                    final String chars = parsePart( context, part );

                    if ( context.parsePosition.getErrorIndex() < 0 )
                    {
                        if ( part.getFixedLength() != null && chars.length() != part.getFixedLength().intValue() )
                        { // Fixed length mismatch.
                            complete = false;
                            break;
                        }

                        electronic_format_builder.append( chars );
                    }
                }

                if ( context.parsePosition.getErrorIndex() < 0 )
                {
                    if ( complete )
                    { // Validate checksum.
                        final StringBuilder integer_builder = new StringBuilder( MAX_CHARACTERS * 2 );
                        appendIso7064Digits( integer_builder, electronic_format_builder.substring( 4 ) );
                        appendIso7064Digits( integer_builder, electronic_format_builder.substring( 0, 4 ) );

                        final BigInteger integer = new BigInteger( integer_builder.toString() );

                        valid = integer.remainder( INTEGER_97 ).equals( BigInteger.ONE );

                        if ( !valid )
                        {
                            throw new IbanCheckDigitsException(
                                electronic_format_builder.toString(),
                                new BigInteger( electronic_format_builder.substring( 2, 4 ) ) );

                        }

                        if ( context.parsePosition.getIndex() != text.length() )
                        { // Unexpected length.
                            valid = false;
                        }
                    }
                }
                else
                { // Invalid part.
                    valid = false;
                }
            }
            else
            { // Unsupported country code.
                valid = false;
            }
        }
        else if ( text.length() > 0 && !( text.charAt( 0 ) >= 'A' && text.charAt( 0 ) <= 'Z' ) )
        { // Illegal first character.
            valid = false;
        }

        return valid;
    }

    /**
     * Formats an IBAN and appends the resulting text to a given {@code Appendable}.
     *
     * @param format The format to use.
     * @param appendable The {@code Appendable} to append the formatted IBAN to.
     *
     * @return The value passed in as {@code appendable}.
     *
     * @throws NullPointerException if {@code format} or {@code appendable} is {@code null}.
     * @throws IOException if appending fails.
     */
    public Appendable append( final IbanFormat format, final Appendable appendable ) throws IOException
    {
        if ( format == null )
        {
            throw new NullPointerException( "format" );
        }
        if ( appendable == null )
        {
            throw new NullPointerException( "appendable" );
        }

        switch ( format )
        {
            case ELECTRONIC:
                return appendable.append( this.electronicFormat );
            case PRINT:
                return appendable.append( this.letterFormat );
            default:
                throw new AssertionError( format );
        }
    }

    /**
     * Formats an IBAN to produce a string.
     *
     * @param format The format to use.
     *
     * @return The formatted string.
     *
     * @throws NullPointerException if {@code format} is {@code null}.
     */
    public String toString( final IbanFormat format )
    {
        if ( format == null )
        {
            throw new NullPointerException( "format" );
        }

        switch ( format )
        {
            case ELECTRONIC:
                return this.electronicFormat;
            case PRINT:
                return this.letterFormat;
            default:
                throw new AssertionError( format );
        }
    }

    /**
     * Formats the object using the provided {@code Formatter}.
     * <p>This method uses the {@code ELECTRONIC} format by default. The {@code PRINT} format can be used by specifying
     * the alternate form flag ({@code #}) in a format specifier.</p>
     *
     * @param formatter The {@code Formatter}.
     * @param flags The flags to modify the output format.
     * @param width The minimum number of characters to be written to the output.
     * @param precision The maximum number of characters to be written to the output.
     *
     * @throws IllegalFormatFlagsException if the {@code UPPERCASE} flag is set.
     */
    public void formatTo( final Formatter formatter, final int flags, final int width, final int precision )
    {
        if ( formatter == null )
        {
            throw new NullPointerException( "formatter" );
        }
        if ( ( flags & UPPERCASE ) == UPPERCASE )
        {
            final StringBuilder flagsBuilder = new StringBuilder( 3 );

            if ( ( flags & ALTERNATE ) == ALTERNATE )
            {
                flagsBuilder.append( "#" );
            }
            if ( ( flags & LEFT_JUSTIFY ) == LEFT_JUSTIFY )
            {
                flagsBuilder.append( "-" );
            }

            flagsBuilder.append( "^" );

            throw new IllegalFormatFlagsException( flagsBuilder.toString() );
        }

        final IbanFormat format = ( flags & ALTERNATE ) == ALTERNATE ? IbanFormat.PRINT : IbanFormat.ELECTRONIC;

        String str = this.toString( format );
        if ( precision != -1 && precision < str.length() )
        {
            str = str.substring( 0, precision );
        }

        final StringBuilder stringBuilder = new StringBuilder( str );

        if ( width != -1 )
        {
            final int len = width - stringBuilder.length();

            if ( len > 0 )
            {
                final char[] pad = new char[ len ];
                Arrays.fill( pad, ' ' );

                if ( ( flags & LEFT_JUSTIFY ) == LEFT_JUSTIFY )
                {
                    stringBuilder.append( pad );
                }
                else
                {
                    stringBuilder.insert( 0, pad );
                }
            }
        }

        formatter.format( stringBuilder.toString() );
    }

    /**
     * Returns the length of this character sequence.
     * <p>The length is the number of 16-bit {@code char}s in the sequence.</p>
     *
     * @return The number of {@code char}s in this sequence.
     */
    public int length()
    {
        return this.electronicFormat.length();
    }

    /**
     * Returns the {@code char} value at the specified index.
     * <p>An index ranges from zero to {@code length() - 1}. The first {@code char} value of the sequence is at index
     * zero, the next at index one, and so on, as for array indexing.</p>
     * <p>If the {@code char} value specified by the index is a surrogate, the surrogate value is returned.</p>
     *
     * @param index The index of the {@code char} value to return.
     *
     * @return The {@code char} value at {@code index}.
     *
     * @throws IndexOutOfBoundsException if {@code index} is negative or not less than the length of the character
     * sequence.
     *
     * @see #length()
     */
    public char charAt( final int index )
    {
        return this.electronicFormat.charAt( index );
    }

    /**
     * Returns a new {@code CharSequence} that is a subsequence of this sequence.
     * <p>The subsequence starts with the {@code char} value at the specified index and ends with the {@code char} value
     * at index {@code end - 1}. The length (in {@code char}s) of the returned sequence is {@code end - start}, so if
     * {@code start == end} then an empty sequence is returned.</p>
     *
     * @param start The start index, inclusive.
     * @param end The end index, exclusive.
     *
     * @return The subsequence starting at {@code start} up to {@code end -1}.
     *
     * @throws IndexOutOfBoundsException if {@code start} or {@code end} are negative, if {@code end} is greater than
     * the length of the character sequence, or if {@code start} is greater than {@code end}.
     */
    public CharSequence subSequence( final int start, final int end )
    {
        return this.electronicFormat.subSequence( start, end );
    }

    /**
     * Gets the hash code value of the object.
     *
     * @return The hash code value of the object.
     */
    @Override
    public int hashCode()
    {
        return this.electronicFormat.hashCode();
    }

    /**
     * Indicates whether some other object is equal to this one.
     *
     * @param o The reference object with which to compare.
     *
     * @return {@code true} if this object is the same as {@code o}; {@code false} otherwise.
     */
    @Override
    public boolean equals( final Object o )
    {
        boolean equal = this == o;

        if ( !equal && ( o instanceof IBAN ) )
        {
            equal = this.electronicFormat.equals( ( (IBAN) o ).electronicFormat );
        }

        return equal;
    }

    /**
     * Gets a string representation of the object.
     *
     * @return A string representation of the object.
     */
    @Override
    public String toString()
    {
        return super.toString() + this.internalString();
    }

    /**
     * Compares this object with the specified object for order.
     * <p>IBAN comparison respects the national IBAN formats when comparing IBANs of the same country. Any country codes
     * and check digits are ignored.</p>
     *
     * @param o The Object to be compared.
     *
     * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or greater than
     * the specified object.
     *
     * @throws NullPointerException if {@code o} is {@code null}.
     */
    public int compareTo( final IBAN o )
    {
        if ( o == null )
        {
            throw new NullPointerException( "o" );
        }

        int r = this.getCountryCode().compareTo( o.getCountryCode() );
        for ( int i = 2, s0 = this.parts.length; r == 0 && i < s0; r = this.parts[i].compareTo( o.parts[i] ), i++ );
        return r;
    }

    /**
     * Gets the current cache instance.
     *
     * @return Current cache instance.
     */
    private static Map<String, IBAN> getCache()
    {
        Map<String, IBAN> cache = cacheReference.get();

        if ( cache == null )
        {
            cache = new ConcurrentHashMap<String, IBAN>( 1024 );
            cacheReference = new SoftReference<Map<String, IBAN>>( cache );
        }

        return cache;
    }

    /**
     * Creates a string representing the properties of the instance.
     *
     * @return A string representing the properties of the instance.
     */
    private String internalString()
    {
        if ( this.string == null )
        {
            final StringBuilder b = new StringBuilder( 500 ).append( "{" );
            b.append( "countryCode=" ).append( this.countryCode ).
                append( ", sepaCountry=" ).append( this.sepaCountry ).
                append( ", bankIdentifier=" ).append( this.bankIdentifier ).
                append( ", branchIdentifier=" ).append( this.branchIdentifier ).
                append( ", electronicFormat=" ).append( this.electronicFormat ).
                append( ", letterFormat=" ).append( this.letterFormat ).
                append( ", parts={" );

            final StringBuilder partBuilder = new StringBuilder();
            for ( int i = 0, s0 = this.parts.length; i < s0; i++ )
            {
                partBuilder.append( ",[" ).append( i ).append( "]=" ).append( this.parts[i] );
            }

            this.string = b.append( partBuilder.substring( 1 ) ).append( "}}" ).toString();
        }

        return this.string;
    }

    /**
     * Parses a given structure string to produce a list of {@code Part}s.
     *
     * @param structure The structure string to parse.
     *
     * @return The parsed {@code Structure}.
     *
     * @throws ParseException if parsing {@code structure} fails.
     */
    private static Structure parseStructure( final String structure ) throws ParseException
    {
        boolean in_literal = false;
        boolean in_part = false;
        boolean fixed_length_part = false;
        final List<Structure.Part> parts = new ArrayList<Structure.Part>( IBAN.MAX_CHARACTERS );
        final StringBuilder literalBuilder = new StringBuilder( IBAN.MAX_CHARACTERS );
        final StringBuilder numberBuilder = new StringBuilder( IBAN.MAX_CHARACTERS );

        for ( int i = 0, s0 = structure.length(); i < s0; i++ )
        {
            final char c = structure.charAt( i );

            if ( in_part )
            {
                // Expect number or "(n|a|c|e)|!(n|a|c|e)"
                if ( isDigit( c ) )
                {
                    if ( fixed_length_part )
                    { // Expect type after length indicator.
                        throw new ParseException( structure, i );
                    }
                    numberBuilder.append( c );
                }
                else if ( isTypeIdentifier( c ) )
                {
                    if ( fixed_length_part )
                    {
                        parts.add( new Structure.Part( c, null, Integer.parseInt( numberBuilder.toString() ), null ) );
                    }
                    else
                    {
                        parts.add( new Structure.Part( c, null, null, Integer.parseInt( numberBuilder.toString() ) ) );
                    }
                    numberBuilder.setLength( 0 );
                    in_part = false;
                    fixed_length_part = false;
                }
                else if ( isLengthIndicator( c ) )
                {
                    if ( fixed_length_part )
                    { // Expect type after length indicator.
                        throw new ParseException( structure, i );
                    }
                    fixed_length_part = true;
                }
                else
                {
                    throw new ParseException( structure, i );
                }
            }
            else if ( in_literal )
            {
                // Expect literal or number starting a part.
                if ( isLiteral( c ) )
                {
                    literalBuilder.append( c );
                }
                else if ( isDigit( c ) )
                {
                    parts.add( new Structure.Part( 'l', literalBuilder.toString(), literalBuilder.length(), null ) );
                    numberBuilder.append( c );
                    literalBuilder.setLength( 0 );
                    in_part = true;
                    in_literal = false;
                }
                else
                {
                    throw new ParseException( structure, i );
                }
            }
            else
            {
                if ( fixed_length_part )
                { // Expect type after length indicator.
                    throw new ParseException( structure, i );
                }

                // Expect number starting a part or upper-case letter starting a literal.
                if ( isDigit( c ) )
                {
                    numberBuilder.append( c );
                    in_part = true;
                }
                else if ( isLiteral( c ) )
                {
                    literalBuilder.append( c );
                    in_literal = true;
                }
                else
                {
                    throw new ParseException( structure, i );
                }
            }
        }

        if ( fixed_length_part )
        { // Expect type after length indicator.
            throw new ParseException( structure, structure.length() );
        }

        if ( in_part )
        { // Unclosed part.
            throw new ParseException( structure, structure.length() );
        }

        if ( in_literal )
        { // Literal at end of structure.
            parts.add( new Structure.Part( 'l', literalBuilder.toString(), literalBuilder.length(), null ) );
        }

        return new Structure( parts );
    }

    /**
     * Parses text from a string according to a given {@code Part} to produce a {@code String} instance.
     *
     * @param context The context of the parse.
     * @param part The {@code Part} to parse.
     *
     * @return The parsed value, or {@code null} if the parse fails.
     */
    private static String parsePart( final ParseContext context, final Structure.Part part )
    {
        final StringBuilder partBuilder = new StringBuilder( MAX_CHARACTERS );
        final int start_index = context.parsePosition.getIndex();

        next:
        for ( int index = context.parsePosition.getIndex(), text_len = context.text.length(), literal_index = 0,
            part_len = part.getFixedLength() != null
                           ? part.getFixedLength().intValue() : part.getMaximumLength().intValue();
              index < text_len && index - start_index < part_len; index++, context.parsePosition.setIndex( index ) )
        {
            final char current = context.text.charAt( index );

            if ( current == ' ' && part.getType() != 'e' )
            {
                if ( context.format == IbanFormat.PRINT && context.length % 4 == 0
                         && ( context.previous == null || context.previous.charValue() != ' ' ) )
                { // Skip letter format separator.
                    part_len++;
                    context.previous = Character.valueOf( current );
                    continue next;
                }
                else
                { // Unexpected letter format separator.
                    context.parsePosition.setIndex( start_index );
                    context.parsePosition.setErrorIndex( index );
                    break next;
                }
            }

            switch ( part.getType() )
            {
                case 'a':
                    // Upper case letters (alphabetic characters A-Z only)
                    if ( current >= 'A' && current <= 'Z' )
                    {
                        partBuilder.append( current );
                        context.length++;
                    }
                    else
                    {
                        context.parsePosition.setIndex( start_index );
                        context.parsePosition.setErrorIndex( index );
                    }
                    break;
                case 'c':
                    // Upper and lower case alphanumeric characters (A-Z, a-z and 0-9)
                    if ( ( current >= 'A' && current <= 'Z' )
                             || ( current >= 'a' && current <= 'z' )
                             || ( current >= '0' && current <= '9' ) )
                    {
                        partBuilder.append( current );
                        context.length++;
                    }
                    else
                    {
                        context.parsePosition.setIndex( start_index );
                        context.parsePosition.setErrorIndex( index );
                    }
                    break;
                case 'e':
                    // blank space
                    if ( current == ' ' )
                    {
                        partBuilder.append( current );
                        context.length++;
                    }
                    else
                    {
                        context.parsePosition.setIndex( start_index );
                        context.parsePosition.setErrorIndex( index );
                    }
                    break;
                case 'n':
                    // Digits (numeric characters 0 to 9 only)
                    if ( current >= '0' && current <= '9' )
                    {
                        partBuilder.append( current );
                        context.length++;
                    }
                    else
                    {
                        context.parsePosition.setIndex( start_index );
                        context.parsePosition.setErrorIndex( index );
                    }
                    break;
                case 'l':
                    // Literal
                    if ( current == part.getLiteral().charAt( literal_index++ ) )
                    {
                        context.length++;
                        partBuilder.append( current );
                    }
                    else
                    {
                        context.parsePosition.setIndex( start_index );
                        context.parsePosition.setErrorIndex( index );
                    }
                    break;
                default:
                    context.parsePosition.setIndex( start_index );
                    context.parsePosition.setErrorIndex( index );
                    break next;
            }

            context.previous = Character.valueOf( current );
        }

        return context.parsePosition.getErrorIndex() < 0 ? partBuilder.toString() : null;
    }

    /**
     * Tests a given character to conform to a literal.
     *
     * @param c The character to test.
     *
     * @return {@code true}, if {@code c} conforms to a literal; {@code false}, else.
     */
    private static boolean isLiteral( final char c )
    {
        return ( c >= 'A' && c <= 'Z' );
    }

    /**
     * Tests a given character to conform to a digit.
     *
     * @param c The character to test.
     *
     * @return {@code true}, if {@code c} conforms to a digit; {@code false}, else.
     */
    private static boolean isDigit( final char c )
    {
        return ( c >= '0' && c <= '9' );
    }

    /**
     * Tests a given character to conform to a type identifier.
     *
     * @param c The character to test.
     *
     * @return {@code true}, if {@code c} conforms to a type identifier; {@code false}, else.
     */
    private static boolean isTypeIdentifier( final char c )
    {
        return c == 'a' || c == 'c' || c == 'e' || c == 'n';
    }

    /**
     * Tests a given character to conform to a length indicator.
     *
     * @param c The character to test.
     *
     * @return {@code true}, if {@code c} conforms to a length indicator; {@code false}, else.
     */
    private static boolean isLengthIndicator( final char c )
    {
        return c == '!';
    }

    /**
     * Parses a given bank/branch id indices {@code String} like {@code 1-5} to produce a list of bank/branch id indices
     * compatible to {@link CharSequence#subSequence(int, int)}.
     *
     * @param str The string to parse.
     *
     * @return The indices of {@code str}.
     */
    private static List<Integer> parseIndices( final String str )
    {
        final String[] parts = str.split( "-" );
        assert parts.length == 2 : "Unexpected bank/branch id indices '" + str + "'.";
        final List<Integer> indices = new ArrayList<Integer>( parts.length );
        indices.add( 0, Integer.valueOf( parts[0] ) - 1 );
        indices.add( 1, Integer.valueOf( parts[1] ) );
        return Collections.unmodifiableList( indices );
    }

    /**
     * Translates characters to digits according to the MOD 97-10 (ISO 7064) algorithm.
     *
     * @param buffer The buffer to append digits to.
     * @param chars The characters to translate.
     */
    private static void appendIso7064Digits( final StringBuilder buffer, final String chars )
    {
        for ( int i = 0, l0 = chars.length(); i < l0; i++ )
        {
            final char c = chars.charAt( i );

            if ( c >= 'A' && c <= 'Z' )
            {
                buffer.append( Integer.toString( ( c - 'A' ) + 10 ) );
            }
            else if ( c >= 'a' && c <= 'z' )
            {
                buffer.append( Integer.toString( ( c - 'a' ) + 10 ) );
            }
            else if ( c >= '0' && c <= '9' )
            {
                buffer.append( c );
            }
            else
            {
                throw new AssertionError( c );
            }
        }
    }

    /**
     * Formats an electronic format IBAN to a letter format IBAN.
     *
     * @param electronicFormat An electronic format IBAN.
     *
     * @return The given IBAN formatted to the letter format representation.
     */
    private static String toLetterFormat( final String electronicFormat )
    {
        final StringBuilder letter_format_builder = new StringBuilder( electronicFormat.length() );

        for ( int i = 0, l0 = electronicFormat.length(); i < l0; i++ )
        {
            if ( i > 0 && i % 4 == 0 )
            {
                letter_format_builder.append( ' ' );
            }

            letter_format_builder.append( electronicFormat.charAt( i ) );
        }

        return letter_format_builder.toString();
    }

    private static String toIbanCountryCode( final String countryCode )
    {
        final String ibanCountryCode = IBAN_COUNTRY_CODES.get( countryCode );
        return ibanCountryCode != null ? ibanCountryCode : countryCode;
    }

    // SECTION-END
    // SECTION-START[Dependencies]
    // SECTION-END
    // SECTION-START[Properties]
    // SECTION-END
    // SECTION-START[Messages]
    // SECTION-END
}