Skip to content

Commit

Permalink
Implemented seperate EAN validators
Browse files Browse the repository at this point in the history
To be used in Swiss SSN implementation
  • Loading branch information
camillobruni committed Jun 4, 2015
1 parent afa8ffe commit 876da33
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 5 deletions.
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

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)

0 comments on commit 876da33

Please sign in to comment.