8000 Add GS1-128 format · viggo-devries/python-stdnum@180788a · GitHub
[go: up one dir, main page]

Skip to content

Commit 180788a

Browse files
committed
Add GS1-128 format
This adds validation, parsing and encoding functions for GS1-128. It is based on the lists of formats as published by the GS1 organisation. Based on the implementation provided by Sergi Almacellas Abellana <sergi@koolpi.com>. Closes arthurdejong#144
1 parent c2284f3 commit 180788a

File tree

4 files changed

+669
-0
lines changed

4 files changed

+669
-0
lines changed

stdnum/gs1_128.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
# gs1_128.py - functions for handling GS1-128 codes
2+
#
3+
# Copyright (C) 2019 Sergi Almacellas Abellana
4+
# Copyright (C) 2020 Arthur de Jong
5+
#
6+
# This library is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 2.1 of the License, or (at your option) any later version.
10+
#
11+
# This library is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public
17+
# License along with this library; if not, write to the Free Software
18+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19+
# 02110-1301 USA
20+
21+
"""GS1-128 (Standard to encode product information in Code 128 barcodes).
22+
23+
The GS1-128 (also called EAN-128, UCC/EAN-128 or UCC-128) is an international
24+
standard for embedding data such as best before dates, weights, etc. with
25+
Application Identifiers (AI).
26+
27+
The GS1-128 standard is used as a product identification code on bar codes.
28+
It embeds data with Application Identifiers (AI) that defines the kind of
29+
data, the type and length. The standard is also known as UCC/EAN-128, UCC-128
30+
and EAN-128.
31+
32+
GS1-128 is a subset of Code 128 symbology.
33+
34+
More information:
35+
36+
* https://en.wikipedia.org/wiki/GS1-128
37+
* https://www.gs1.org/standards/barcodes/application-identifiers
38+
* https://www.gs1.org/docs/barcodes/GS1_General_Specifications.pdf
39+
40+
>>> compact('(01)38425876095074(17)181119(37)1 ')
41+
'013842587609507417181119371'
42+
>>> encode({'01': '38425876095074'})
43+
'0138425876095074'
44+
>>> info('0138425876095074')
45+
{'01': '38425876095074'}
46+
>>> validate('(17)181119(01)38425876095074(37)1')
47+
'013842587609507417181119371'
48+
"""
49+
50+
import datetime
51+
import decimal
52+
import re
53+
54+
from stdnum import numdb
55+
from stdnum.exceptions import *
56+
from stdnum.util import clean
57+
58+
59+
# our open copy of the application identifier database
60+
_gs1_aidb = numdb.get('gs1_ai')
61+
62+
63+
# Extra validation modules based on the application identifier
64+
_ai_validators = {
65+
'01': 'stdnum.ean',
66+
'02': 'stdnum.ean',
67+
'8007': 'stdnum.iban',
68+
}
69+
70+
71+
def compact(number):
72+
"""Convert the GS1-128 to the minimal representation.
73+
74+
This strips the number of any valid separators and removes surrounding
75+
whitespace. For a more consistent compact representation use
76+
:func:`validate()`.
77+
"""
78+
return clean(number, '()').strip()
79+
80+
81+
def _encode_value(fmt, _type, value):
82+
"""Encode the specified value given the format and type."""
83+
if _type == 'decimal':
84+
if isinstance(value, (list, tuple)) and fmt.startswith('N3+'):
85+
number = _encode_value(fmt[3:], _type, value[1])
86+
return number[0] + value[0].rjust(3, '0') + number[1:]
87+
value = str(value)
88+
if fmt.startswith('N..'):
89+
length = int(fmt[3:])
90+
value = value[:length + 1]
91+
number, digits = (value.split('.') + [''])[:2]
92+
digits = digits[:9]
93+
return str(len(digits)) + number + digits
94+
else:
95+
length = int(fmt[1:])
96+
value = value[:length + 1]
97+
number, digits = (value.split('.') + [''])[:2]
98+
digits = digits[:9]
99+
return str(len(digits)) + (number + digits).rjust(length, '0')
100+
elif _type == 'date':
101+
if isinstance(value, (list, tuple)) and fmt == 'N6..12':
102+
return '%s%s' % (
103+
_encode_value('N6', _type, value[0]),
104+
_encode_value('N6', _type, value[1]))
105+
elif isinstance(value, datetime.date):
106+
if fmt == 'N10':
107+
return value.strftime('%y%m%d%H%M')
108+
elif fmt == 'N8+N..4':
109+
value = datetime.datetime.strftime(value, '%y%m%d%H%M%S')
110+
if value.endswith('00'):
111+
value = value[:-2]
112+
if value.endswith('00'):
113+
value = value[:-2]
114+
return value
115+
return value.strftime('%y%m%d')
116+
return str(value)
117+
118+
119+
def _max_length(fmt, _type):
120+
"""Determine the maximum length based on the format ad type."""
121+
length = sum(int(re.match(r'^[NXY][0-9]*?[.]*([0-9]+)$', x).group(1)) for x in fmt.split('+'))
122+
if _type == 'decimal':
123+
length += 1
124+
return length
125+
126+
127+
def _pad_value(fmt, _type, value):
128+
"""Pad the value to the maximum length for the format."""
129+
if _type in ('decimal', 'int'):
130+
return value.rjust(_max_length(fmt, _type), '0')
131+
return value.ljust(_max_length(fmt, _type))
132+
133+
134+
def _decode_value(fmt, _type, value):
135+
"""Decode the specified value given the fmt and type."""
136+
if _type == 'decimal':
137+
if fmt.startswith('N3+'):
138+
return (value[1:4], _decode_value(fmt[3:], _type, value[0] + value[4:]))
139+
digits = int(value[0])
140+
value = value[1:]
141+
if digits:
142+
value = value[:-digits] + '.' + value[-digits:]
143+
return decimal.Decimal(value)
144+
elif _type == 'date':
145+
if fmt == 'N8+N..4':
146+
return datetime.datetime.strptime(value, '%y%m%d%H%M%S'[:len(value)])
147+
elif len(value) == 10:
148+
return datetime.datetime.strptime(value, '%y%m%d%H%M')
149+
elif len(value) == 12:
150+
return (_decode_value(fmt, _type, value[:6]), _decode_value(fmt, _type, value[6:]))
151+
return datetime.datetime.strptime(value, '%y%m%d').date()
152+
elif _type == 'int':
153+
return int(value)
154+
return value.strip()
155+
156+
157+
def info(number, separator=''):
158+
"""Return a dictionary containing the information from the GS1-128 code.
159+
160+
The returned dictionary maps application identifiers to values with the
161+
appropriate type (`str`, `int`, `Decimal`, `datetime.date` or
162+
`datetime.datetime`).
163+
164+
If a `separator` is provided it will be used as FNC1 to determine the end
165+
of variable-sized values.
166+
"""
167+
number = compact(number)
168+
data = {}
169+
identifier = ''
170+
# skip separator
171+
if separator and number.startswith(separator):
172+
number = number[len(separator):]
173+
while number:
174+
# extract the application identifier
175+
ai, info = _gs1_aidb.info(number)[0]
176+
if not info or not number.startswith(ai):
177+
raise InvalidComponent()
178+
number = number[len(ai):]
179+
# figure out the value part
180+
value = number[:_max_length(info['format'], info['type'])]
181+
if separator and info.get('fnc1', False):
182+
idx = number.find(separator)
183+
if idx > 0:
184+
value = number[:idx]
185+
number = number[len(value):]
186+
# validate the value if we have a custom module for it
187+
if ai in _ai_validators:
188+
mod = __import__(_ai_validators[ai], globals(), locals(), ['validate'])
189+
mod.validate(value)
190+
# convert the number
191+
data[ai] = _decode_value(info['format'], info['type'], value)
192+
# skip separator
193+
if separator and number.startswith(separator):
194+
number = number[len(separator):]
195+
return data
196+
197+
198+
def encode(data, separator='', parentheses=False):
199+
"""Generate a GS1-128 for the application identifiers supplied.
200+
201+
The provided dictionary is expected to map application identifiers to
202+
values. The supported value types and formats depend on the application
203+
identifier.
204+
205+
If a `separator` is provided it will be used as FNC1 representation,
206+
otherwise variable-sized values will be expanded to their maximum size
207+
with appropriate padding.
208+
209+
If `parentheses` is set the application identifiers will be surrounded
210+
by parentheses for readability.
211+
"""
212+
ai_fmt = '(%s)' if parentheses else '%s'
213+
# we keep items sorted and keep fixed-sized values separate tot output
214+
# them first
215+
fixed_values = []
216+
variable_values = []
217+
for inputai, value in sorted(data.items()):
218+
ai, info = _gs1_aidb.info(inputai)[0]
219+
if not info:
220+
raise InvalidComponent()
221+
# validate the value if we have a custom module for it
222+
if ai in _ai_validators:
223+
mod = __import__(_ai_validators[ai], globals(), locals(), ['validate'])
224+
mod.validate(value)
225+
value = _encode_value(info['format'], info['type'], value)
226+
# store variable-sized values separate from fixed-size values
227+
if info.get('fnc1', False):
228+
variable_values.append((ai_fmt % ai, info['format'], info['type'], value))
229+
else:
230+
fixed_values.append(ai_fmt % ai + value)
231+
# we need the separator for all but the last variable-sized value
232+
# (or pad values if we don't have a separator)
233+
return ''.join(
234+
fixed_values + [
235+
ai + (value if separator else _pad_value(fmt, _type, value)) + separator
236+
for ai, fmt, _type, value in variable_values[:-1]
237+
] + [
238+
ai + value
239+
for ai, fmt, _type, value in variable_values[-1:]
240+
])
241+
242+
243+
def validate(number, separator=''):
244+
"""Check if the number provided is a valid GS1-128.
245+
246+
This checks formatting of the number and values and returns a stable
247+
representation.
248+
249+
If a separator is provided it will be used as FNC1 for both parsing the
250+
provided number and for encoding the returned number.
251+
"""
252+
try:
253+
return encode(info(number, separator), separator)
254+
except ValidationError:
255+
raise
256+
except Exception:
257+
# We wrap all other exceptions to ensure that we only return
258+
# exceptions that are a subclass of ValidationError
259+
# (the info() and encode() functions expect some semblance of valid
260+
# input)
261+
raise InvalidFormat()
262+
263+
264+
def is_valid(number, separator=''):
265+
"""Check if the number provided is a valid GS1-128."""
266+
try:
267+
return bool(validate(number))
268+
except ValidationError:
269+
return False

0 commit comments

Comments
 (0)
0