CreditCardValidator.java

/*
 * Copyright (C) 2016 Alberto Irurueta Carro (alberto@irurueta.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.irurueta.commons.validators;

import java.util.Arrays;

/**
 * Class to validate credit card PAN numbers.
 * PAN numbers are formed by IIN (Issuer Identification Number) followed by a
 * series of digits + the check digit (using Luhn algorithm), if available for
 * a given credit card network. Notice that the first digit of the IIN corresponds
 * to a MII (Major Industry Identifier).
 * This class also provides functionality to detect network of a given credit
 * card PAN number.
 * Notice that network detection and validation is not 100% accurate, hence it
 * is suggested to never prevent users from completing payments if validation
 * does not pass or network cannot be detected. In those cases simply a warning
 * can be shown to the user indicating that payment data might be wrong.
 * Notice as well that if network is detected and validation passes, then it is
 * almost certain that payment data is correct.
 */
public class CreditCardValidator {
    /**
     * Minimum allowed length for a credit card PAN.
     */
    public static final int MIN_LENGTH = 8;

    /**
     * Maximum allowed length for a credit card PAN.
     */
    public static final int MAX_LENGTH = 19;

    /**
     * Constant indicating whether American Express uses Luhn validation to
     * ensure that PAN is valid.
     */
    protected static final boolean AMEX_HAS_VALIDATION = true;

    /**
     * Constant indicating whether American Express network is still being used.
     */
    protected static final boolean AMEX_IS_ACTIVE = true;

    /**
     * Constant indicating whether Bankcard uses Luhn validation to ensure that
     * PAN is valid.
     */
    protected static final boolean BANKCARD_HAS_VALIDATION = true;

    /**
     * Constant indicating whether Bankcard network is still being used.
     */
    protected static final boolean BANKCARD_IS_ACTIVE = false;

    /**
     * Constant indicating whether China UnionPay uses Luhn validation to ensure
     * that PAN is valid.
     */
    protected static final boolean CHINA_UNIONPAY_HAS_VALIDATION = false;

    /**
     * Constant indicating whether China UnionPay network is still being used.
     */
    protected static final boolean CHINA_UNIONPAY_IS_ACTIVE = true;

    /**
     * Constant indicating whether Diners Club Carte Blanche uses Luhn
     * validation to ensure that PAN is valid.
     */
    protected static final boolean DINERS_CLUB_CARTE_BLANCHE_HAS_VALIDATION =
            true;

    /**
     * Constant indicating whether Diners Club Carte Blanche network is still
     * being used.
     */
    protected static final boolean DINERS_CLUB_CARTE_BLANCHE_IS_ACTIVE = true;

    /**
     * Constant indicating whether Diners Club enRoute uses Luhn validation to
     * ensure that PAN is valid.
     */
    protected static final boolean DINERS_CLUB_ENROUTE_HAS_VALIDATION = false;

    /**
     * Constant indicating whether Diners Club enRoute network is still being
     * used.
     */
    protected static final boolean DINERS_CLUB_ENROUTE_IS_ACTIVE = false;

    /**
     * Constant indicating whether Diners Club International uses Luhn
     * validation to ensure that PAN is valid.
     */
    protected static final boolean DINERS_CLUB_INTERATIONAL_HAS_VALIDATION =
            true;

    /**
     * Constant indicating whether Diners Club International network is still
     * being used.
     */
    protected static final boolean DINERS_CLUB_INTERATIONAL_IS_ACTIVE = true;

    /**
     * Constant indicating whether Diners Club USA and Canada uses Luhn
     * validation to ensure that PAN is valid.
     */
    protected static final boolean DINERS_CLUB_USA_CA_HAS_VALIDATION = true;

    /**
     * Constant indicating whether Diners Club USA and Canada network is still
     * being used.
     */
    protected static final boolean DINERS_CLUB_USA_CA_IS_ACTIVE = true;

    /**
     * Constant indicating whether Discover uses Luhn validation to ensure that
     * PAN is valid.
     */
    protected static final boolean DISCOVER_HAS_VALIDATION = true;

    /**
     * Constant indicating whether Discover network is still being used.
     */
    protected static final boolean DISCOVER_IS_ACTIVE = true;

    /**
     * Constant indicating whether InstaPayment uses Luhn validation to ensure
     * that PAN is valid.
     */
    protected static final boolean INSTAPAYMENT_HAS_VALIDATION = true;

    /**
     * Constant indicating whether InstaPayment network is still being used.
     */
    protected static final boolean INSTAPAYMENT_IS_ACTIVE = true;

    /**
     * Constant indicating whether JCB uses Luhn validation to ensure that PAN
     * is valid.
     */
    protected static final boolean JCB_HAS_VALIDATION = true;

    /**
     * Constant indicating whether JCB network is still being used.
     */
    protected static final boolean JCB_IS_ACTIVE = true;

    /**
     * Constant indicating whether Laser uses Luhn validation to ensure that PAN
     * is valid.
     */
    protected static final boolean LASER_HAS_VALIDATION = true;

    /**
     * Constant indicating whether Laser network is still being used.
     */
    protected static final boolean LASER_IS_ACTIVE = true;

    /**
     * Constant indicating whether Maestro uses Luhn validation to ensure
     * that PAN is valid.
     */
    protected static final boolean MAESTRO_HAS_VALIDATION = true;

    /**
     * Constant indicating whether Maestro network is still being used.
     */
    protected static final boolean MAESTRO_IS_ACTIVE = true;

    /**
     * Constant indicating whether MasterCard uses Luhn validation to ensure
     * that PAN is valid.
     */
    protected static final boolean MASTERCARD_HAS_VALIDATION = true;

    /**
     * Constant indicating whether MasterCard network is still being used.
     */
    protected static final boolean MASTERCARD_IS_ACTIVE = true;

    /**
     * Constant indicating whether Solo uses Luhn validation to ensure
     * that PAN is valid.
     */
    protected static final boolean SOLO_HAS_VALIDATION = true;

    /**
     * Constant indicating whether Solo network is still being used.
     */
    protected static final boolean SOLO_IS_ACTIVE = false;

    /**
     * Constant indicating whether Switch uses Luhn validation to ensure
     * that PAN is valid.
     */
    protected static final boolean SWITCH_HAS_VALIDATION = true;

    /**
     * Constant indicating whether Switch network is still being used.
     */
    protected static final boolean SWITCH_IS_ACTIVE = false;

    /**
     * Constant indicating whether VISA uses Luhn validation to ensure
     * that PAN is valid.
     */
    protected static final boolean VISA_HAS_VALIDATION = true;

    /**
     * Constant indicating whether VISA network is still being used.
     */
    protected static final boolean VISA_IS_ACTIVE = true;

    /**
     * Constant indicating whether VISA Electron uses Luhn validation to
     * ensure that PAN is valid.
     */
    protected static final boolean VISA_ELECTRON_HAS_VALIDATION = true;

    /**
     * Constant indicating whether VISA Electron network is still being used.
     */
    protected static final boolean VISA_ELECTRON_IS_ACTIVE = true;

    /**
     * Constant containing IIN ranges reserved for American Express.
     */
    private static final String[][] AMEX_IIN = new String[][]{
            new String[]{"34", "34"},
            new String[]{"37", "37"}
    };

    /**
     * Constant containing valid lengths for American Express.
     */
    private static final byte[][] AMEX_LENGTH = new byte[][]{
            new byte[]{15, 15}
    };

    /**
     * Constant defining digit groups for American Express.
     */
    private static final byte[][] AMEX_GROUPING = new byte[][]{
            new byte[]{4, 4},
            new byte[]{6, 6},
            new byte[]{5, 5}
    };

    /**
     * Constant containing IIN ranges reserved for Bankcard.
     */
    private static final String[][] BANKCARD_IIN = new String[][]{
            new String[]{"5610", "5610"},
            new String[]{"560221", "560225"}
    };

    /**
     * Constant containing valid lengths for Bankcard.
     */
    private static final byte[][] BANKCARD_LENGTH = new byte[][]{
            new byte[]{16, 16}
    };

    /**
     * Constant containing IIN ranges reserved for China UnionPay.
     */
    private static final String[][] CHINA_UNIONPAY_IIN = new String[][]{
            new String[]{"62", "62"}
    };

    /**
     * Constant containing valid lengths for China UnionPay.
     */
    private static final byte[][] CHINA_UNIONPAY_LENGTH = new byte[][]{
            new byte[]{16, 19}
    };

    /**
     * Constant containing IIN ranges reserved for Diners Club Carte Blanche.
     */
    private static final String[][] DINERS_CLUB_CARTE_BLANCHE_IIN = new String[][]{
            new String[]{"300", "305"}
    };

    /**
     * Constant containing valid lengths for Diners Club Carte Blanche.
     */
    private static final byte[][] DINERS_CLUB_CARTE_BLANCHE_LENGTH = new byte[][]{
            new byte[]{14, 14}
    };

    /**
     * Constant containing IIN ranges reserved for Diners Club enRoute.
     */
    private static final String[][] DINERS_CLUB_ENROUTE_IIN = new String[][]{
            new String[]{"2014", "2014"},
            new String[]{"2149", "2149"}
    };

    /**
     * Constant containing valid lengths for Diners Club enRoute.
     */
    private static final byte[][] DINERS_CLUB_ENROUTE_LENGTH = new byte[][]{
            new byte[]{15, 15}
    };

    /**
     * Constant defining digit groups for Dinners club enroute.
     */
    private static final byte[][] DINERS_CLUB_ENROUTE_GROUPING = new byte[][]{
            new byte[]{4, 4},
            new byte[]{7, 7},
            new byte[]{4, 4}
    };

    /**
     * Constant containing IIN ranges reserved for Diners Club International.
     */
    private static final String[][] DINERS_CLUB_INTERNATIONAL_IIN = new String[][]{
            new String[]{"36", "36"}
    };

    /**
     * Constant containing valid lengths for Diners Club International.
     */
    private static final byte[][] DINERS_CLUB_INTERNATIONAL_LENGTH = new byte[][]{
            new byte[]{14, 14}
    };

    /**
     * Constant defining digit groups for Dinners club internacional. This
     * grouping is also used for DINERS_CLUB_CARTE_BLANCHE.
     */
    private static final byte[][] DINERS_CLUB_INTERATIONAL_GROUPING = new byte[][]{
            new byte[]{4, 4},
            new byte[]{6, 6},
            new byte[]{4, 4}};

    /**
     * Constant containing IIN ranges reserved for Diners Club USA and Canada.
     */
    private static final String[][] DINERS_CLUB_USA_CA_IIN = new String[][]{
            new String[]{"54", "55"}
    };

    /**
     * Constant containing valid lengths for Diners Club USA and Canada.
     */
    private static final byte[][] DINERS_CLUB_USA_CA_LENGTH = new byte[][]{
            new byte[]{16, 16}
    };

    /**
     * Constant containing IIN ranges reserved for Discover.
     */
    private static final String[][] DISCOVER_IIN = new String[][]{
            new String[]{"6011", "6011"},
            new String[]{"622126", "62295"},
            new String[]{"644", "649"},
            new String[]{"65", "65"}
    };

    /**
     * Constant containing valid lengths for Discover.
     */
    private static final byte[][] DISCOVER_LENGTH = new byte[][]{
            new byte[]{16, 16}
    };

    /**
     * Constant defining digit groups for Discover. This grouping is also used
     * for all Diners Club cards.
     */
    private static final byte[][] DISCOVER_GROUPING = new byte[][]{
            new byte[]{4, 4},
            new byte[]{4, 4},
            new byte[]{4, 4},
            new byte[]{4, 4}
    };

    /**
     * Constant containing IIN ranges reserved for InstaPayment.
     */
    private static final String[][] INSTAPAYMENT_IIN = new String[][]{
            new String[]{"637", "639"}
    };

    /**
     * Constant containing valid lengths for InstaPayment.
     */
    private static final byte[][] INSTAPAYMENT_LENGTH = new byte[][]{
            new byte[]{16, 16}
    };

    /**
     * Constant containing IIN ranges reserved for JCB.
     */
    private static final String[][] JCB_IIN = new String[][]{
            new String[]{"3528", "3589"}
    };

    /**
     * Constant containing valid lengths for JCB.
     */
    private static final byte[][] JCB_LENGTH = new byte[][]{
            new byte[]{16, 16}
    };

    /**
     * Constant containing IIN ranges reserved for Laser.
     */
    private static final String[][] LASER_IIN = new String[][]{
            new String[]{"6304", "6304"},
            new String[]{"6706", "6706"},
            new String[]{"6771", "6771"},
            new String[]{"6709", "6709"}
    };

    /**
     * Constant containing valid lengths for Laser.
     */
    private static final byte[][] LASER_LENGTH = new byte[][]{
            new byte[]{16, 19}
    };

    /**
     * Constant containing IIN ranges reserved for Maestro.
     */
    private static final String[][] MAESTRO_IIN = new String[][]{
            new String[]{"5018", "5018"},
            new String[]{"5020", "5020"},
            new String[]{"5038", "5038"},
            new String[]{"5893", "5893"},
            new String[]{"6304", "6304"},
            new String[]{"6759", "6759"},
            new String[]{"6761", "6763"},
            new String[]{"0604", "0604"}
    };

    /**
     * Constant containing valid lengths for Maestro.
     */
    private static final byte[][] MAESTRO_LENGTH = new byte[][]{
            new byte[]{12, 19}
    };

    /**
     * Constant containing IIN ranges reserved for MasterCard.
     */
    private static final String[][] MASTERCARD_IIN = new String[][]{
            new String[]{"222100", "272099"},
            new String[]{"51", "55"}
    };

    /**
     * Constant containing valid lengths for MasterCard.
     */
    private static final byte[][] MASTERCARD_LENGTH = new byte[][]{
            new byte[]{16, 16}
    };

    /**
     * Constant defining digit groups for Mastercard.
     */
    private static final byte[][] MASTERCARD_GROUPING = new byte[][]{
            new byte[]{4, 4},
            new byte[]{4, 4},
            new byte[]{4, 4},
            new byte[]{4, 4}
    };

    /**
     * Constant containing IIN ranges reserved for Solo.
     */
    private static final String[][] SOLO_IIN = new String[][]{
            new String[]{"6334", "6334"},
            new String[]{"6767", "6767"}
    };

    /**
     * Constant containing valid lengths for Solo.
     */
    private static final byte[][] SOLO_LENGTH = new byte[][]{
            new byte[]{16, 16},
            new byte[]{18, 19}
    };

    /**
     * Constant containing IIN ranges reserved for Switch.
     */
    private static final String[][] SWITCH_IIN = new String[][]{
            new String[]{"4903", "4903"},
            new String[]{"4905", "4905"},
            new String[]{"4911", "4911"},
            new String[]{"4936", "4936"},
            new String[]{"564182", "564182"},
            new String[]{"633110", "633110"},
            new String[]{"6333", "6333"},
            new String[]{"6759", "6759"}
    };

    /**
     * Constant containing valid lengths for Switch.
     */
    private static final byte[][] SWITCH_LENGTH = new byte[][]{
            new byte[]{16, 16},
            new byte[]{18, 19}
    };

    /**
     * Constant containing IIN ranges reserved for VISA.
     */
    private static final String[][] VISA_IIN = new String[][]{
            new String[]{"4", "4"}
    };

    /**
     * Constant containing valid lengths for VISA.
     */
    private static final byte[][] VISA_LENGTH = new byte[][]{
            new byte[]{13, 13},
            new byte[]{16, 16}
    };

    /**
     * Constant defining digit groups for VISA.
     */
    private static final byte[][] VISA_GROUPING = new byte[][]{
            new byte[]{4, 4},
            new byte[]{4, 4},
            new byte[]{4, 4},
            new byte[]{1, 4}
    };

    /**
     * Constant containing IIN ranges reserved for VISA Electron.
     */
    private static final String[][] VISA_ELECTRON_IIN = new String[][]{
            new String[]{"4026", "4026"},
            new String[]{"417500", "417500"},
            new String[]{"4405", "4405"},
            new String[]{"4508", "4508"},
            new String[]{"4844", "4844"},
            new String[]{"4913", "4913"},
            new String[]{"4917", "4917"}
    };

    /**
     * Constant containing valid lengths for VISA Electron.
     */
    private static final byte[][] VISA_ELECTRON_LENGTH = new byte[][]{
            new byte[]{16, 16}
    };

    /**
     * Constant defining digit groups for VISA Electron.
     */
    private static final byte[][] VISA_ELECTRON_GROUPING = new byte[][]{
            new byte[]{4, 4},
            new byte[]{4, 4},
            new byte[]{4, 4},
            new byte[]{4, 4}
    };

    /**
     * Constant defining digit groups for any other network or unknown networks.
     */
    private static final byte[][] DEFAULT_GROUPING = new byte[][]{
            new byte[]{4, 4},
            new byte[]{4, 4},
            new byte[]{4, 4},
            new byte[]{0, 7}
    };

    /**
     * Constructor.
     */
    protected CreditCardValidator() {
    }

    /**
     * Indicates if provided credit card PAN value corresponds to any valid MII
     * (Major Industry Identifier). All valid credit card PANs must start by
     * 3, 4, 5 or 6, which correspond to all the assigned MII.
     *
     * @param pan a credit card PAN value.
     * @return true if PAN corresponds to a valid MII, false otherwise.
     */
    public static boolean isValidMII(final String pan) {
        return isValidMII(toDigits(pan));
    }

    /**
     * Indicates if provided array of credit card PAN digits corresponds to any
     * valid MII (Major Industry Identifier). All valid credit card PANs must
     * start by 3, 4, 5 or 6, which correspond to all the assigned MII.
     *
     * @param panDigits an array of credit card PAN digits.
     * @return true if PAN corresponds to a valid MII, false otherwise.
     */
    protected static boolean isValidMII(final byte[] panDigits) {
        if (panDigits == null || panDigits.length == 0) {
            return false;
        }

        final byte first = panDigits[0];
        return first == 3 || first == 4 || first == 5 || first == 6;
    }

    /**
     * Indicates if a given credit card network uses Luhn algorithm to validate
     * PAN values. For unknown networks validation is assumed to be disabled.
     *
     * @param network a credit card network.
     * @return true if provided network uses PAN validation, false otherwise.
     */
    public static boolean isValidationEnabledForNetwork(final CreditCardNetwork network) {
        if (network == null) {
            return false;
        }

        switch (network) {
            case AMERICAN_EXPRESS:
                return AMEX_HAS_VALIDATION;
            case BANKCARD:
                return BANKCARD_HAS_VALIDATION;
            case CHINA_UNIONPAY:
                return CHINA_UNIONPAY_HAS_VALIDATION;
            case DINERS_CLUB_CARTE_BLANCHE:
                return DINERS_CLUB_CARTE_BLANCHE_HAS_VALIDATION;
            case DINERS_CLUB_ENROUTE:
                return DINERS_CLUB_ENROUTE_HAS_VALIDATION;
            case DINERS_CLUB_INTERNATIONAL:
                return DINERS_CLUB_INTERATIONAL_HAS_VALIDATION;
            case DINERS_CLUB_USA_CANADA:
                return DINERS_CLUB_USA_CA_HAS_VALIDATION;
            case DISCOVER:
                return DISCOVER_HAS_VALIDATION;
            case INSTAPAYMENT:
                return INSTAPAYMENT_HAS_VALIDATION;
            case JCB:
                return JCB_HAS_VALIDATION;
            case LASER:
                return LASER_HAS_VALIDATION;
            case MAESTRO:
                return MAESTRO_HAS_VALIDATION;
            case MASTERCARD:
                return MASTERCARD_HAS_VALIDATION;
            case SOLO:
                return SOLO_HAS_VALIDATION;
            case SWITCH:
                return SWITCH_HAS_VALIDATION;
            case VISA:
                return VISA_HAS_VALIDATION;
            case VISA_ELECTRON:
                return VISA_ELECTRON_HAS_VALIDATION;
            case UNKNOWN:
            default:
                return false;
        }
    }

    /**
     * Indicates if provided credit card network is still being used
     * commercially.
     * Unknown networks are assumed to be active.
     *
     * @param network a credit card network.
     * @return true if provided network is still being used, false otherwise.
     */
    public static boolean isNetworkActive(final CreditCardNetwork network) {
        if (network == null) {
            return true;
        }

        switch (network) {
            case AMERICAN_EXPRESS:
                return AMEX_IS_ACTIVE;
            case BANKCARD:
                return BANKCARD_IS_ACTIVE;
            case CHINA_UNIONPAY:
                return CHINA_UNIONPAY_IS_ACTIVE;
            case DINERS_CLUB_CARTE_BLANCHE:
                return DINERS_CLUB_CARTE_BLANCHE_IS_ACTIVE;
            case DINERS_CLUB_ENROUTE:
                return DINERS_CLUB_ENROUTE_IS_ACTIVE;
            case DINERS_CLUB_INTERNATIONAL:
                return DINERS_CLUB_INTERATIONAL_IS_ACTIVE;
            case DINERS_CLUB_USA_CANADA:
                return DINERS_CLUB_USA_CA_IS_ACTIVE;
            case DISCOVER:
                return DISCOVER_IS_ACTIVE;
            case INSTAPAYMENT:
                return INSTAPAYMENT_IS_ACTIVE;
            case JCB:
                return JCB_IS_ACTIVE;
            case LASER:
                return LASER_IS_ACTIVE;
            case MAESTRO:
                return MAESTRO_IS_ACTIVE;
            case MASTERCARD:
                return MASTERCARD_IS_ACTIVE;
            case SOLO:
                return SOLO_IS_ACTIVE;
            case SWITCH:
                return SWITCH_IS_ACTIVE;
            case VISA:
                return VISA_IS_ACTIVE;
            case VISA_ELECTRON:
                return VISA_ELECTRON_IS_ACTIVE;
            case UNKNOWN:
            default:
                return true;
        }
    }

    /**
     * Detects credit card network by using provided credit card PAN value.
     * Detection is done by checking all known registered credit card IINs
     * (Issuer Identification Number) against provided PAN until a match is
     * found.
     *
     * @param pan a credit card PAN number.
     * @return detected credit card network.
     */
    public static CreditCardNetwork detectNetworkFromPAN(final String pan) {
        return detectNetworkFromPAN(toDigits(pan));
    }

    /**
     * Detects credit card network by using provided credit card PAN digits.
     * Detection is done by checking all known registered credit card IINs
     * (Issuer Identification Number) against provided PAN digits until a match
     * is found.
     *
     * @param panDigits an array containing a credit card PAN digits.
     * @return detected credit card network.
     */
    protected static CreditCardNetwork detectNetworkFromPAN(final byte[] panDigits) {
        if (isAmericanExpressIIN(panDigits)) {
            return CreditCardNetwork.AMERICAN_EXPRESS;
        }
        if (isBankcardIIN(panDigits)) {
            return CreditCardNetwork.BANKCARD;
        }
        if (isDinersClubCarteBlancheIIN(panDigits)) {
            return CreditCardNetwork.DINERS_CLUB_CARTE_BLANCHE;
        }
        if (isDinersClubEnrouteIIN(panDigits)) {
            return CreditCardNetwork.DINERS_CLUB_ENROUTE;
        }
        if (isDinersClubInternationalIIN(panDigits)) {
            return CreditCardNetwork.DINERS_CLUB_INTERNATIONAL;
        }
        if (isDinersClubUSACanadaIIN(panDigits)) {
            return CreditCardNetwork.DINERS_CLUB_USA_CANADA;
        }
        // Discover lies within China UnionPay range, so we check it first
        if (isDiscoverIIN(panDigits)) {
            return CreditCardNetwork.DISCOVER;
        }
        if (isChinaUnionPayIIN(panDigits)) {
            return CreditCardNetwork.CHINA_UNIONPAY;
        }
        if (isInstaPaymentIIN(panDigits)) {
            return CreditCardNetwork.INSTAPAYMENT;
        }
        if (isJCBIIN(panDigits)) {
            return CreditCardNetwork.JCB;
        }
        if (isLaserIIN(panDigits)) {
            return CreditCardNetwork.LASER;
        }
        if (isMaestroIIN(panDigits)) {
            return CreditCardNetwork.MAESTRO;
        }
        if (isMastercardIIN(panDigits)) {
            return CreditCardNetwork.MASTERCARD;
        }
        if (isSoloIIN(panDigits)) {
            return CreditCardNetwork.SOLO;
        }
        if (isSwitchIIN(panDigits)) {
            return CreditCardNetwork.SWITCH;
        }
        // VISA Electron is contained within VISA IIN, so we check it first
        if (isVISAElectronIIN(panDigits)) {
            return CreditCardNetwork.VISA_ELECTRON;
        }
        if (isVISAIIN(panDigits)) {
            return CreditCardNetwork.VISA;
        }
        return CreditCardNetwork.UNKNOWN;
    }

    /**
     * Detects network for provided PAN and indicates if provided credit card
     * PAN has an appropriate length for provided credit card network.
     *
     * @param pan a credit card network.
     * @return true if PAN has a valid length, false otherwise.
     */
    public static boolean isValidLength(final String pan) {
        return isValidLength(toDigits(pan));
    }

    /**
     * Detects network for provided PAN digits and indicates if provided array
     * of credit card PAN digits has an appropriate length for provided credit
     * card network.
     * PAN for unknown network is assumed to always have a valid length.
     *
     * @param panDigits an array of credit card PAN digits.
     * @return true if PAN has a valid length, false otherwise.
     */
    protected static boolean isValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, detectNetworkFromPAN(panDigits));
    }

    /**
     * Indicates if provided credit card PAN has an appropriate length for
     * provided credit card network.
     * PAN for unknown network is assumed to always have a valid length.
     *
     * @param pan     a credit card PAN.
     * @param network a credit card network.
     * @return true if PAN has a valid length, false otherwise.
     */
    public static boolean isValidLength(final String pan, final CreditCardNetwork network) {
        return isValidLength(toDigits(pan), network);
    }

    /**
     * Indicates if provided array of credit card PAN digits has an appropriate
     * length for provided credit card network.
     * PAN for unknown networks is assumed to always have a valid length.
     *
     * @param panDigits an array of credit card PAN digits.
     * @param network   a credit card network.
     * @return true if PAN has a valid length, false otherwise.
     */
    protected static boolean isValidLength(final byte[] panDigits,
                                           final CreditCardNetwork network) {
        if (network == null) {
            return true;
        }

        switch (network) {
            case AMERICAN_EXPRESS:
                return isAmericanExpressValidLength(panDigits);
            case BANKCARD:
                return isBankcardValidLength(panDigits);
            case CHINA_UNIONPAY:
                return isChinaUnionPayValidLength(panDigits);
            case DINERS_CLUB_CARTE_BLANCHE:
                return isDinersClubCarteBlancheValidLength(panDigits);
            case DINERS_CLUB_ENROUTE:
                return isDinersClubEnrouteValidLength(panDigits);
            case DINERS_CLUB_INTERNATIONAL:
                return isDinersClubInternationalValidLength(panDigits);
            case DINERS_CLUB_USA_CANADA:
                return isDinersClubUSACanadaValidLength(panDigits);
            case DISCOVER:
                return isDiscoverValidLength(panDigits);
            case INSTAPAYMENT:
                return isInstaPaymentValidLength(panDigits);
            case JCB:
                return isJCBValidLength(panDigits);
            case LASER:
                return isLaserValidLength(panDigits);
            case MAESTRO:
                return isMaestroValidLength(panDigits);
            case MASTERCARD:
                return isMastercardValidLength(panDigits);
            case SOLO:
                return isSoloValidLength(panDigits);
            case SWITCH:
                return isSwitchValidLength(panDigits);
            case VISA:
                return isVISAValidLength(panDigits);
            case VISA_ELECTRON:
                return isVISAElectronValidLength(panDigits);
            case UNKNOWN:
            default:
                return true;
        }
    }

    /**
     * Indicates if provided credit card PAN is valid by detecting its assigned
     * credit card network, checking its length and validating its checksum if
     * network supports validation.
     *
     * @param pan a credit card pan.
     * @return true if credit card PAN appears to be valid, false otherwise.
     */
    public static boolean isValid(final String pan) {
        return isValid(toDigits(pan));
    }

    /**
     * Indicates if provided credit card PAN is valid by detecting its assigned
     * credit card network, checking its length and validating its checksum if
     * network supports validation.
     *
     * @param panDigits an array containing credit card pan digits.
     * @return true if credit card PAN appears to be valid, false otherwise.
     */
    protected static boolean isValid(final byte[] panDigits) {
        final CreditCardNetwork network = detectNetworkFromPAN(panDigits);
        boolean valid = isValidLength(panDigits, network);
        if (valid && isValidationEnabledForNetwork(network)) {
            valid = isValidChecksumForPAN(panDigits);
        }
        return valid;
    }

    /**
     * Indicates if provided credit card PAN corresponds to American Express
     * IIN (Issuer Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to American Express, false
     * otherwise.
     */
    public static boolean isAmericanExpressIIN(final String pan) {
        return isValidIIN(pan, AMEX_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits correspond to American
     * Express IIN (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to American Express, false
     * otherwise.
     */
    protected static boolean isAmericanExpressIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, AMEX_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for American
     * Express cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid American Express length, false otherwise.
     */
    public static boolean isAmericanExpressValidLength(final String pan) {
        return isValidLength(pan, AMEX_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * American Express cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid American Express length, false otherwise.
     */
    protected static boolean isAmericanExpressValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, AMEX_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to BankCard IIN (Issuer
     * Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to BankCard, false otherwise.
     */
    public static boolean isBankcardIIN(final String pan) {
        return isValidIIN(pan, BANKCARD_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits correspond to BankCard IIN
     * (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to BankCard, false otherwise.
     */
    protected static boolean isBankcardIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, BANKCARD_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for BankCard
     * cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid BankCard length, false otherwise.
     */
    public static boolean isBankcardValidLength(final String pan) {
        return isValidLength(pan, BANKCARD_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * BankCard cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid BankCard length, false otherwise.
     */
    protected static boolean isBankcardValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, BANKCARD_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to China UnionPay IIN
     * (Issuer Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to China UnionPay, false
     * otherwise.
     */
    public static boolean isChinaUnionPayIIN(final String pan) {
        return isValidIIN(pan, CHINA_UNIONPAY_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits correspond to China UnionPay
     * IIN (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to China UnionPay, false
     * otherwise.
     */
    protected static boolean isChinaUnionPayIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, CHINA_UNIONPAY_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for China UnionPay
     * cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid China UnionPay length, false otherwise.
     */
    public static boolean isChinaUnionPayValidLength(final String pan) {
        return isValidLength(pan, CHINA_UNIONPAY_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * China UnionPay cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid China UnionPay length, false otherwise.
     */
    protected static boolean isChinaUnionPayValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, CHINA_UNIONPAY_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to Diners Club Carte
     * Blanche IIN (Issuer Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to Diners Club Carte Blanche,
     * false otherwise.
     */
    public static boolean isDinersClubCarteBlancheIIN(final String pan) {
        return isValidIIN(pan, DINERS_CLUB_CARTE_BLANCHE_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits corresponds to Diners Club
     * Carte Blanche IIN (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to Diners Club Carte Blance,
     * false otherwise.
     */
    protected static boolean isDinersClubCarteBlancheIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, DINERS_CLUB_CARTE_BLANCHE_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for Diners Club
     * Carte Blanche cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid Diners Club Carte Blanche length, false
     * otherwise.
     */
    public static boolean isDinersClubCarteBlancheValidLength(final String pan) {
        return isValidLength(pan, DINERS_CLUB_CARTE_BLANCHE_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * Diners Club Carte Blanche cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid Diners Club Carte Blanche length, false
     * otherwise.
     */
    protected static boolean isDinersClubCarteBlancheValidLength(
            final byte[] panDigits) {
        return isValidLength(panDigits, DINERS_CLUB_CARTE_BLANCHE_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to Diners Club Enroute
     * IIN (Issuer Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to Diners Club Enroute, false
     * otherwise.
     */
    public static boolean isDinersClubEnrouteIIN(final String pan) {
        return isValidIIN(pan, DINERS_CLUB_ENROUTE_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits corresponds to Diners Club
     * Enroute IIN (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to Diners Club Enroute, false
     * otherwise.
     */
    protected static boolean isDinersClubEnrouteIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, DINERS_CLUB_ENROUTE_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for Diners Club
     * Enroute cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid Diners Club Enroute length, false
     * otherwise.
     */
    public static boolean isDinersClubEnrouteValidLength(final String pan) {
        return isValidLength(pan, DINERS_CLUB_ENROUTE_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * Diners Club Enroute cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid Diners Club Enroute length, false
     * otherwise.
     */
    protected static boolean isDinersClubEnrouteValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, DINERS_CLUB_ENROUTE_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to Diners Club
     * International IIN (Issuer Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to Diners Club International,
     * false otherwise.
     */
    public static boolean isDinersClubInternationalIIN(final String pan) {
        return isValidIIN(pan, DINERS_CLUB_INTERNATIONAL_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits corresponds to Diners Club
     * International IIN (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to Diners Club International,
     * false otherwise.
     */
    protected static boolean isDinersClubInternationalIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, DINERS_CLUB_INTERNATIONAL_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for Diners Club
     * International cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid Diners Club International length, false
     * otherwise.
     */
    public static boolean isDinersClubInternationalValidLength(final String pan) {
        return isValidLength(pan, DINERS_CLUB_INTERNATIONAL_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * Diners Club International cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid Diners Club International length, false
     * otherwise.
     */
    protected static boolean isDinersClubInternationalValidLength(
            final byte[] panDigits) {
        return isValidLength(panDigits, DINERS_CLUB_INTERNATIONAL_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to Diners Club USA and
     * Canada IIN (Issuer Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to Diners Club USA and Canada,
     * false otherwise.
     */
    public static boolean isDinersClubUSACanadaIIN(final String pan) {
        return isValidIIN(pan, DINERS_CLUB_USA_CA_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits corresponds to Diners Club
     * USA and Canada IIN (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to Diners Club USA and Canada,
     * false otherwise.
     */
    protected static boolean isDinersClubUSACanadaIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, DINERS_CLUB_USA_CA_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for Diners Club
     * USA and Canada cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid Diners Club USA and Canada length, false
     * otherwise.
     */
    public static boolean isDinersClubUSACanadaValidLength(final String pan) {
        return isValidLength(pan, DINERS_CLUB_USA_CA_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * Diners Club USA and Canada cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid Diners Club USA and Canada length, false
     * otherwise.
     */
    protected static boolean isDinersClubUSACanadaValidLength(
            final byte[] panDigits) {
        return isValidLength(panDigits, DINERS_CLUB_USA_CA_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to Discover IIN (Issuer
     * Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to Discover, false otherwise.
     */
    public static boolean isDiscoverIIN(final String pan) {
        return isValidIIN(pan, DISCOVER_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits corresponds to Discover IIN
     * (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to Discover, false otherwise.
     */
    protected static boolean isDiscoverIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, DISCOVER_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for Discover
     * cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid Discover length, false otherwise.
     */
    public static boolean isDiscoverValidLength(final String pan) {
        return isValidLength(pan, DISCOVER_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * Discover cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid Discover length, false otherwise.
     */
    protected static boolean isDiscoverValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, DISCOVER_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to InstaPayment IIN
     * (Issuer Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to InstaPayment, false
     * otherwise.
     */
    public static boolean isInstaPaymentIIN(final String pan) {
        return isValidIIN(pan, INSTAPAYMENT_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits corresponds to InstaPayment
     * IIN (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to InstaPayment, false
     * otherwise.
     */
    protected static boolean isInstaPaymentIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, INSTAPAYMENT_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for InstaPayment
     * cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid InstaPayment length, false otherwise.
     */
    public static boolean isInstaPaymentValidLength(final String pan) {
        return isValidLength(pan, INSTAPAYMENT_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * InstaPayment cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid InstaPayment length, false otherwise.
     */
    protected static boolean isInstaPaymentValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, INSTAPAYMENT_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to JCB IIN (Issuer
     * Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to JCB, false otherwise.
     */
    public static boolean isJCBIIN(final String pan) {
        return isValidIIN(pan, JCB_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits correspond to JCB IIN
     * (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to JCB, false otherwise.
     */
    protected static boolean isJCBIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, JCB_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for JCB
     * cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid JCB length, false otherwise.
     */
    public static boolean isJCBValidLength(final String pan) {
        return isValidLength(pan, JCB_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * JCB cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid JCB length, false otherwise.
     */
    protected static boolean isJCBValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, JCB_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to Laser IIN (Issuer
     * Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to Laser, false otherwise.
     */
    public static boolean isLaserIIN(final String pan) {
        return isValidIIN(pan, LASER_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits correspond to Laser IIN
     * (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to Laser, false otherwise.
     */
    protected static boolean isLaserIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, LASER_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for Laser
     * cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid Laser length, false otherwise.
     */
    public static boolean isLaserValidLength(final String pan) {
        return isValidLength(pan, LASER_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * Laser cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid Laser length, false otherwise.
     */
    protected static boolean isLaserValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, LASER_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to Maestro IIN (Issuer
     * Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to Maestro, false otherwise.
     */
    public static boolean isMaestroIIN(final String pan) {
        return isValidIIN(pan, MAESTRO_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits correspond to Maestro IIN
     * (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to Maestro, false otherwise.
     */
    protected static boolean isMaestroIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, MAESTRO_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for Maestro
     * cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid Maestro length, false otherwise.
     */
    public static boolean isMaestroValidLength(final String pan) {
        return isValidLength(pan, MAESTRO_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * Maestro cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid Maestro length, false otherwise.
     */
    protected static boolean isMaestroValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, MAESTRO_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to Mastercard IIN
     * (Issuer Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to Mastercard, false otherwise.
     */
    public static boolean isMastercardIIN(final String pan) {
        return isValidIIN(pan, MASTERCARD_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits correspond to Mastercard IIN
     * (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to Mastercard, false otherwise.
     */
    protected static boolean isMastercardIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, MASTERCARD_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for Mastercard
     * cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid Mastercard length, false otherwise.
     */
    public static boolean isMastercardValidLength(final String pan) {
        return isValidLength(pan, MASTERCARD_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * Mastercard cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid Mastercard length, false otherwise.
     */
    protected static boolean isMastercardValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, MASTERCARD_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to Solo IIN (Issuer
     * Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to Solo, false otherwise.
     */
    public static boolean isSoloIIN(final String pan) {
        return isValidIIN(pan, SOLO_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits correspond to Solo IIN
     * (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to Solo, false otherwise.
     */
    protected static boolean isSoloIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, SOLO_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for Solo
     * cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid Solo length, false otherwise.
     */
    public static boolean isSoloValidLength(final String pan) {
        return isValidLength(pan, SOLO_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * Solo cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid Solo length, false otherwise.
     */
    protected static boolean isSoloValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, SOLO_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to Switch IIN (Issuer
     * Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to Switch, false otherwise.
     */
    public static boolean isSwitchIIN(final String pan) {
        return isValidIIN(pan, SWITCH_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits correspond to Switch IIN
     * (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to Switch, false otherwise.
     */
    protected static boolean isSwitchIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, SWITCH_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for Switch
     * cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid Switch length, false otherwise.
     */
    public static boolean isSwitchValidLength(final String pan) {
        return isValidLength(pan, SWITCH_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * Switch cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid Switch length, false otherwise.
     */
    protected static boolean isSwitchValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, SWITCH_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to VISA IIN (Issuer
     * Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to VISA, false otherwise.
     */
    public static boolean isVISAIIN(final String pan) {
        return isValidIIN(pan, VISA_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits correspond to VISA IIN
     * (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to VISA, false otherwise.
     */
    protected static boolean isVISAIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, VISA_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for VISA cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid VISA length, false otherwise.
     */
    public static boolean isVISAValidLength(final String pan) {
        return isValidLength(pan, VISA_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * VISA cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid VISA length, false otherwise.
     */
    protected static boolean isVISAValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, VISA_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN corresponds to VISA Electron IIN
     * (Issuer Identification Number).
     *
     * @param pan a credit card PAN.
     * @return true if provided PAN corresponds to VISA Electron, false
     * otherwise.
     */
    public static boolean isVISAElectronIIN(final String pan) {
        return isValidIIN(pan, VISA_ELECTRON_IIN);
    }

    /**
     * Indicates if provided credit card PAN digits correspond to VISA Electron
     * IIN (Issuer Identification Number).
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if provided PAN corresponds to VISA Electron, false
     * otherwise.
     */
    protected static boolean isVISAElectronIIN(final byte[] panDigits) {
        return isValidIIN(panDigits, VISA_ELECTRON_IIN);
    }

    /**
     * Indicates if provided credit card PAN has valid length for VISA Electron
     * cards.
     *
     * @param pan a credit card PAN.
     * @return true if PAN has a valid VISA Electron length, false otherwise.
     */
    public static boolean isVISAElectronValidLength(final String pan) {
        return isValidLength(pan, VISA_ELECTRON_LENGTH);
    }

    /**
     * Indicates if provided credit card PAN digits have valid length for
     * VISA Electron cards.
     *
     * @param panDigits array containing credit card PAN digits.
     * @return true if PAN has a valid VISA Electron length, false otherwise.
     */
    protected static boolean isVISAElectronValidLength(final byte[] panDigits) {
        return isValidLength(panDigits, VISA_ELECTRON_LENGTH);
    }

    /**
     * Validates provided credit card PAN using Luhn algorithm assuming that
     * the last digit of provided PAN corresponds to its checksum value.
     * This method computes the checksum value using Luhn algorithm and compares
     * that with the last digit of provided PAN value, if both are equal PAN
     * checksum is assumed to be valid.
     *
     * @param pan a credit card PAN to validate its checksum.
     * @return true if checksum is valid, false otherwise.
     */
    public static boolean isValidChecksumForPAN(final String pan) {
        return isValidChecksumForPAN(toDigits(pan));
    }

    /**
     * Validates provided credit card PAN using Luhn algorithm assuming that
     * the last digit of provided PAN corresponds to its checksum value.
     * This method computes the checksum value using Luhn algorithm and compares
     * that with the last digit of provided PAN digits, if both are equal PAN
     * checksum is assumed to be valid.
     *
     * @param panDigits array of digits containing a credit card PAN.
     * @return true if checksum is valid, false otherwise.
     */
    protected static boolean isValidChecksumForPAN(final byte[] panDigits) {
        if (panDigits == null) {
            return false;
        }

        // last digit is check value
        final int length = panDigits.length;
        if (length < 2) {
            return false; //we need at least two digits (digits + checksum)
        }

        final byte check = panDigits[length - 1];

        // copy digits without check into new array
        final byte[] digitsWithoutCheck = Arrays.copyOf(panDigits, length - 1);

        // compare checksums
        final byte computedCheck = computeCheck(digitsWithoutCheck);
        return check == computedCheck;
    }

    /**
     * Converts a credit card PAN from its string representation into an array
     * containing PAN digits.
     * This method will strip away any spaces or punctuation marks, leaving only
     * the digits found within provided PAN.
     *
     * @param pan a credit card PAN.
     * @return credit card PAN represented as an array of digits.
     */
    public static byte[] toDigits(final String pan) {
        if (pan == null) {
            return new byte[0];
        }

        final int length = pan.length();
        final byte[] internal = new byte[length];
        byte value;
        char c;
        int numDigits = 0;
        for (int i = 0; i < length; i++) {
            c = pan.charAt(i);
            switch (c) {
                case '0':
                    value = 0;
                    break;
                case '1':
                    value = 1;
                    break;
                case '2':
                    value = 2;
                    break;
                case '3':
                    value = 3;
                    break;
                case '4':
                    value = 4;
                    break;
                case '5':
                    value = 5;
                    break;
                case '6':
                    value = 6;
                    break;
                case '7':
                    value = 7;
                    break;
                case '8':
                    value = 8;
                    break;
                case '9':
                    value = 9;
                    break;
                default:
                    continue;
            }

            // value of character is only added if it is a digit,
            // otherwise this is never executed
            internal[numDigits] = value;
            numDigits++;
        }

        if (numDigits == 0) {
            return new byte[0];
        } else {
            return Arrays.copyOf(internal, numDigits);
        }
    }

    /**
     * Internal method to check whether provided credit card PAN corresponds
     * to a valid IIN by checking the array of valid IIN ranges.
     *
     * @param pan  a credit card PAN.
     * @param iins array of valid IIN ranges.
     * @return true if PAN corresponds to a valid IIN, false otherwise.
     */
    protected static boolean isValidIIN(final String pan, final String[][] iins) {
        if (pan == null || iins == null) {
            return false;
        }
        // remove hyphens, spaces or any punctuation mark from pan string
        return isValidIIN(toDigits(pan), iins);
    }

    /**
     * Internal method to check whether provided array of PAN digits corresponds
     * to a valid IIN by checking the array of valid IIN ranges.
     *
     * @param panDigits array containing credit card PAN digits.
     * @param iins      array of valid IIN ranges.
     * @return true if PAN corresponds to valid IIN, false otherwise.
     */
    protected static boolean isValidIIN(final byte[] panDigits, final String[][] iins) {
        if (iins == null) {
            return false;
        }
        if (panDigits == null || panDigits.length == 0) {
            return false;
        }
        final StringBuilder panBuilder = new StringBuilder();
        for (final byte panDigit : panDigits) {
            panBuilder.append(panDigit);
        }
        final String pan2 = panBuilder.toString();

        String iinStartStr;
        String iinEndStr;
        String iinStr;
        int iinStart;
        int iinEnd;
        int leadingZeros;
        StringBuilder iinBuilder;
        // try for all possible IIN ranges
        for (final String[] iin : iins) {
            iinStartStr = iin[0];
            iinEndStr = iin[1];
            if (iinStartStr == null || iinEndStr == null) {
                return false;
            }

            leadingZeros = numLeadingZeros(iinStartStr);
            if (iinStartStr.length() < iinEndStr.length()) {
                // add 0's to start of range until both start and end have the
                // same length
                iinBuilder = new StringBuilder();
                iinBuilder.append(iinStartStr);
                final int diff = iinEndStr.length() - iinStartStr.length();
                for (int j = 0; j < diff; j++) {
                    iinBuilder.append('0');
                }
                iinStartStr = iinBuilder.toString();
            }
            if (iinStartStr.length() > iinEndStr.length()) {
                // add 9's to end of range until both start and end have the same
                // length
                iinBuilder = new StringBuilder();
                iinBuilder.append(iinEndStr);
                final int diff = iinStartStr.length() - iinEndStr.length();
                for (int j = 0; j < diff; j++) {
                    iinBuilder.append('9');
                }
                iinEndStr = iinBuilder.toString();
            }
            iinStart = Integer.parseInt(iinStartStr);
            iinEnd = Integer.parseInt(iinEndStr);
            for (int j = iinStart; j <= iinEnd; j++) {
                // try for all values within a range
                iinBuilder = new StringBuilder();
                for (int m = 0; m < leadingZeros; m++) {
                    iinBuilder.append('0');
                }
                iinBuilder.append(j);

                // check if pan starts with computed IIN
                iinStr = iinBuilder.toString();
                if (pan2.startsWith(iinStr) || iinStr.startsWith(pan2)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Internal method to determine whether length of provided credit card PAN
     * is valid based on array of valid length ranges.
     *
     * @param pan     a credit card PAN.
     * @param lengths array of valid length ranges.
     * @return true if PAN length is valid, false otherwise.
     */
    protected static boolean isValidLength(final String pan, final byte[][] lengths) {
        if (pan == null || lengths == null) {
            return false;
        }
        return isValidLength(toDigits(pan), lengths);
    }

    /**
     * Internal method to determine whether length of provided array of credit
     * card PAN digits is valid based on array of valid length ranges.
     *
     * @param panDigits array containing credit card PAN digits.
     * @param lengths   array of valid length ranges.
     * @return true if PAN length is valid, false otherwise.
     */
    protected static boolean isValidLength(final byte[] panDigits, final byte[][] lengths) {
        if (panDigits == null || lengths == null) {
            return false;
        }
        final int length = panDigits.length;

        int minLength;
        int maxLength;
        for (final byte[] l : lengths) {
            minLength = l[0];
            maxLength = l[1];
            if (length >= minLength && length <= maxLength) {
                return true;
            }
        }
        return false;
    }

    /**
     * Computes number of leading zeros on any given IIN.
     *
     * @param iin a given IIN in string format.
     * @return number of leading zeros.
     */
    protected static int numLeadingZeros(final String iin) {
        if (iin == null) {
            return 0;
        }

        final int length = iin.length();
        for (int i = 0; i < length; i++) {
            if (iin.charAt(i) != '0') {
                return i;
            }
        }
        return length;
    }

    /**
     * Computes checksum using Luhn algorithm.
     * Notice that this method will modify the values in provided array of
     * digits.
     *
     * @param digitsWithoutCheck array containing a credit card PAN number
     *                           without its checksum value (which is the last digit).
     * @return computed checksum value.
     */
    protected static byte computeCheck(final byte[] digitsWithoutCheck) {
        // multiply by two even digits
        final int length = digitsWithoutCheck.length;
        for (int i = length - 1; i >= 0; i -= 2) {
            digitsWithoutCheck[i] *= 2;
        }

        // sum digits of multiplied digits
        byte value;
        byte unit;
        byte ten;
        for (int i = 0; i < length; i++) {
            value = digitsWithoutCheck[i];
            unit = (byte) (value % 10); //a unit
            ten = (byte) ((value - unit) / 10); //a ten
            digitsWithoutCheck[i] = (byte) (unit + ten);
        }

        // sum all values
        int sum = 0;
        for (final byte digitWithoutCheck : digitsWithoutCheck) {
            sum += digitWithoutCheck;
        }

        // multiply by 9 and pick last digit (modulus 10)
        return (byte) ((sum * 9) % 10);
    }

    /**
     * Returns credit card digit groupings for provided credit card network.
     *
     * @param network a credit card network.
     * @return digit groupings.
     */
    protected static byte[][] groupingsForNetwork(final CreditCardNetwork network) {
        if (network == null) {
            return DEFAULT_GROUPING;
        }

        switch (network) {
            case AMERICAN_EXPRESS:
                return AMEX_GROUPING;
            case VISA:
                return VISA_GROUPING;
            case VISA_ELECTRON:
                return VISA_ELECTRON_GROUPING;
            case MASTERCARD:
                return MASTERCARD_GROUPING;
            case DINERS_CLUB_INTERNATIONAL:
            case DINERS_CLUB_CARTE_BLANCHE:
                return DINERS_CLUB_INTERATIONAL_GROUPING;
            case DINERS_CLUB_ENROUTE:
                return DINERS_CLUB_ENROUTE_GROUPING;
            case DINERS_CLUB_USA_CANADA:
            case DISCOVER:
                return DISCOVER_GROUPING;
            default:
                return DEFAULT_GROUPING;
        }
    }

    /**
     * Returns number of digit groups for provided credit card network.
     *
     * @param network a credit card network.
     * @return number of digit groups for provided credit card network.
     */
    public static int getNumberOfGroupsForNetwork(final CreditCardNetwork network) {
        final byte[][] groups = groupingsForNetwork(network);
        return groups.length;
    }

    /**
     * Returns minimum number of required digits for provided group position
     * (starting at zero until the maximum number of groups minus one).
     *
     * @param groupPos position of group of digits.
     * @param network  credit card network.
     * @return minimum number of required digits.
     * @throws IllegalArgumentException if groupPos is negative or exceeds the
     *                                  maximum number of groups minus one for provided credit card network.
     */
    public static int getMinDigitsForGroupAndNetwork(final int groupPos,
                                                     final CreditCardNetwork network) {
        if (groupPos < 0) {
            throw new IllegalArgumentException();
        }

        final byte[][] groups = groupingsForNetwork(network);
        final int numGroups = groups.length;
        if (groupPos >= numGroups) {
            throw new IllegalArgumentException();
        }

        return groups[groupPos][0];
    }

    /**
     * Returns maximum number of allowed digits for provided group position
     * (starting at zero until the maximum number of groups minus one).
     *
     * @param groupPos position of group of digits.
     * @param network  credit card network.
     * @return maximum number of allowed digits.
     * @throws IllegalArgumentException if groupPos is negative or exceeds the
     *                                  maximum number of groups minus one for provided credit card network.
     */
    public static int getMaxDigitsForGroupAndNetwork(final int groupPos,
                                                     final CreditCardNetwork network) {
        if (groupPos < 0) {
            throw new IllegalArgumentException();
        }

        final byte[][] groups = groupingsForNetwork(network);
        final int numGroups = groups.length;
        if (groupPos >= numGroups) {
            throw new IllegalArgumentException();
        }

        return groups[groupPos][1];
    }
}