Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add separate EAN validator, #156

Merged
merged 1 commit into from
Jun 4, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion localflavor/generic/checksums.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
"""
from django.utils import six

__all__ = ['luhn']
__all__ = ['luhn', 'ean']

LUHN_ODD_LOOKUP = (0, 2, 4, 6, 8, 1, 3, 5, 7, 9) # sum_of_digits(index * 2)
EAN_LOOKUP = (3, 1)


def luhn(candidate):
Expand All @@ -22,3 +23,24 @@ def luhn(candidate):
return ((evens + odds) % 10 == 0)
except ValueError: # Raised if an int conversion fails
return False


def ean(candidate):
"""
Checks a candidate number for validity according to the EAN checksum calculation.
Note that this validator does not enforce any length checks (usually 13 or 8).

http://en.wikipedia.org/wiki/International_Article_Number_(EAN)
"""
if not isinstance(candidate, six.string_types):
candidate = str(candidate)
if len(candidate) <= 1:
return False
given_number, given_checksum = candidate[:-1], candidate[-1]
try:
calculated_checksum = sum(
int(digit) * EAN_LOOKUP[i % 2] for i, digit in enumerate(reversed(given_number)))
calculated_checksum = 10 - (calculated_checksum % 10)
return str(calculated_checksum) == given_checksum
except ValueError: # Raised if an int conversion fails
return False
27 changes: 27 additions & 0 deletions localflavor/generic/validators.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import re
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: blank line before, not after import re


import string

from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.utils.translation import ugettext_lazy as _

from .countries.iso_3166 import ISO_3166_1_ALPHA2_COUNTRY_CODES
from . import checksums

# Dictionary of ISO country code to IBAN length.
#
Expand All @@ -20,6 +24,7 @@
# http://www.ecbs.org/iban/france-bank-account-number.html
# https://www.nordea.com/V%C3%A5ra+tj%C3%A4nster/Internationella+produkter+och+tj%C3%A4nster/Cash+Management/IBAN+countries/908472.html


IBAN_COUNTRY_CODE_LENGTH = {'AL': 28, # Albania
'AD': 24, # Andorra
'AE': 23, # United Arab Emirates
Expand Down Expand Up @@ -209,3 +214,25 @@ def __call__(self, value):
country_code = value[4:6]
if country_code not in ISO_3166_1_ALPHA2_COUNTRY_CODES:
raise ValidationError(_('%s is not a valid country code.') % country_code)


class EANValidator(object):
"""
A generic validator for EAN like codes with the last digit being the checksum.

http://en.wikipedia.org/wiki/International_Article_Number_(EAN)
"""
message = _('Not a valid EAN code.')

def __init__(self, strip_nondigits=False, message=None):
if message is not None:
self.message = message
self.strip_nondigits = strip_nondigits

def __call__(self, value):
if value is None:
return value
if self.strip_nondigits:
value = re.compile(r'[^\d]+').sub('', value)
if not checksums.ean(value):
raise ValidationError(self.message)
24 changes: 21 additions & 3 deletions tests/test_checksums.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@

class TestUtilsChecksums(unittest.TestCase):

def assertChecksumOuput(self, checksum, result_pairs):
for value, output in result_pairs:
self.assertEqual(checksum(value), output, "Expected %s(%s) == %s but got %s" % (
checksum.__name__, repr(value), output, not output))

def test_luhn(self):
"""
Check that function(value) equals output.
"""
items = (
result_pairs = (
(4111111111111111, True),
('4111111111111111', True),
(4222222222222, True),
Expand All @@ -30,5 +35,18 @@ def test_luhn(self):
(None, False),
(object(), False),
)
for value, output in items:
self.assertEqual(checksums.luhn(value), output, value)
self.assertChecksumOuput(checksums.luhn, result_pairs)

def test_ean(self):
result_pairs = (
('73513537', True),
(73513537, True),
('73513538', False),
(73513538, False),
('4006381333931', True),
(4006381333931, True),
('abc', False),
(None, False),
(object(), False),
)
self.assertChecksumOuput(checksums.ean, result_pairs)
62 changes: 61 additions & 1 deletion tests/test_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from localflavor.generic.countries.sepa import IBAN_SEPA_COUNTRIES
from localflavor.generic.models import BICField, IBANField
from localflavor.generic.validators import BICValidator, IBANValidator
from localflavor.generic.validators import BICValidator, IBANValidator, EANValidator
from localflavor.generic.forms import DateField, DateTimeField, SplitDateTimeField, BICFormField, IBANFormField


Expand Down Expand Up @@ -337,3 +337,63 @@ def test_bic_model_field(self):
def test_default_form(self):
bic_model_field = BICField()
self.assertEqual(type(bic_model_field.formfield()), type(BICFormField()))


class EANTests(TestCase):

def test_ean_validator(self):
valid = [
'4006381333931',
'73513537',

'012345678905',
'0012345678905',

None,
]
error_message = 'Not a valid EAN code.'
invalid = [
'400.6381.3339.31',
'4006381333930',
'',
'0',
'DÉUTDEFF',
]

validator = EANValidator()
for value in valid:
validator(value)

for value in invalid:
self.assertRaisesMessage(ValidationError, error_message, validator, value)

def test_ean_validator_strip_nondigits(self):
valid = [
'4006381333931',
'400.6381.3339.31',
'73513537',
'73-51-3537',
'73 51 3537',
'73A51B3537',

'012345678905',
'0012345678905',

None,
]
error_message = 'Not a valid EAN code.'
invalid = [
'4006381333930',
'400-63-813-339-30',
'400 63 813 339 30',
'',
'0',
'DÉUTDEFF',
]

validator = EANValidator(strip_nondigits=True)
for value in valid:
validator(value)

for value in invalid:
self.assertRaisesMessage(ValidationError, error_message, validator, value)