Calculates the VAT rate that should be collected based on address
    information provided

    :param country_code:
        The two-character country code

    :param postal_code:
        The postal code for the user

    :param city:
        The city name for the user

    :raises:
        ValueError - If country code is not two characers, or postal_code or city are not strings. postal_code may be None or blank string for countries without postal codes.

    :return:
        A tuple of (Decimal percentage rate, country code, exception name [or None])

def calculate_rate(country_code, postal_code, city):
    """
    Calculates the VAT rate that should be collected based on address
    information provided

    :param country_code:
        The two-character country code

    :param postal_code:
        The postal code for the user

    :param city:
        The city name for the user

    :raises:
        ValueError - If country code is not two characers, or postal_code or city are not strings. postal_code may be None or blank string for countries without postal codes.

    :return:
        A tuple of (Decimal percentage rate, country code, exception name [or None])
    """

    if not country_code or not isinstance(country_code, str_cls):
        raise ValueError('Invalidly formatted country code')

    country_code = country_code.strip()
    if len(country_code) != 2:
        raise ValueError('Invalidly formatted country code')

    country_code = country_code.upper()

    if country_code not in COUNTRIES_WITHOUT_POSTAL_CODES:
        if not postal_code or not isinstance(postal_code, str_cls):
            raise ValueError('Postal code is not a string')

    if not city or not isinstance(city, str_cls):
        raise ValueError('City is not a string')

    if isinstance(postal_code, str_cls):
        postal_code = re.sub('\\s+', '', postal_code)
        postal_code = postal_code.upper()

        # Remove the common european practice of adding the country code
        # to the beginning of a postal code, followed by a dash
        if len(postal_code) > 3 and postal_code[0:3] == country_code + '-':
            postal_code = postal_code[3:]

        postal_code = postal_code.replace('-', '')

    city = city.lower().strip()

    if country_code not in rates.BY_COUNTRY and country_code not in POSTAL_CODE_EXCEPTIONS:
        return (Decimal('0.0'), country_code, None)

    country_default = rates.BY_COUNTRY.get(country_code, {'rate': Decimal('0.0')})['rate']

    if country_code not in POSTAL_CODE_EXCEPTIONS:
        return (country_default, country_code, None)

    exceptions = POSTAL_CODE_EXCEPTIONS[country_code]
    for matcher in exceptions:
        # Postal code-only match
        if isinstance(matcher, str_cls):
            postal_regex = matcher
            city_regex = None
        else:
            postal_regex, city_regex = matcher

        if not re.match(postal_regex, postal_code):
            continue

        if city_regex and not re.search(city_regex, city):
            continue

        mapped_country = exceptions[matcher]['country_code']

        # There is at least one entry where we map to a different country,
        # but are not mapping to an exception
        if 'name' not in exceptions[matcher]:
            country_code = mapped_country
            country_default = rates.BY_COUNTRY[country_code]['rate']
            break

        mapped_name = exceptions[matcher]['name']

        rate = rates.BY_COUNTRY[mapped_country]['exceptions'][mapped_name]
        return (rate, mapped_country, mapped_name)

    return (country_default, country_code, None)