diff options
author | Arthur de Jong <arthur@arthurdejong.org> | 2025-05-18 16:22:35 +0200 |
---|---|---|
committer | Arthur de Jong <arthur@arthurdejong.org> | 2025-05-18 17:10:01 +0200 |
commit | 210b9cecc34d403293d2bd9a814e66dd74421abc (patch) | |
tree | a7c47561e818bec66400f2eac0d347ca2e245acd | |
parent | d66998e6c6af5852b2761060c12c1a48a1c167ec (diff) |
This fixes the handling of GS1-128 Application Identifiers with decimal
types. Previously, with some 4 digit decimal application identifiers the
number of decimals were incorrectly considered part of the value instead
of the application identifier.
For example (310)5033333 is not correct but it should be evaluated as
(3105)033333 (application identifier 3105 and value 0.33333).
Closes https://github.com/arthurdejong/python-stdnum/issues/471
-rw-r--r-- | stdnum/gs1_128.py | 204 | ||||
-rw-r--r-- | stdnum/gs1_ai.dat | 120 | ||||
-rw-r--r-- | tests/test_gs1_128.doctest | 52 | ||||
-rwxr-xr-x | update/gs1_ai.py | 10 |
4 files changed, 213 insertions, 173 deletions
diff --git a/stdnum/gs1_128.py b/stdnum/gs1_128.py index 38e6e47..edee598 100644 --- a/stdnum/gs1_128.py +++ b/stdnum/gs1_128.py @@ -1,7 +1,7 @@ # gs1_128.py - functions for handling GS1-128 codes # # Copyright (C) 2019 Sergi Almacellas Abellana -# Copyright (C) 2020-2024 Arthur de Jong +# Copyright (C) 2020-2025 Arthur de Jong # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -86,103 +86,135 @@ def compact(number: str) -> str: return clean(number, '()').strip() -def _encode_value(fmt: str, _type: str, value: object) -> str: +def _encode_decimal(ai: str, fmt: str, value: object) -> tuple[str, str]: + """Encode the specified decimal value given the format.""" + # For decimal types the last digit of the AI is used to encode the + # number of decimal places (we replace the last digit) + if isinstance(value, (list, tuple)) and fmt.startswith('N3+'): + # Two numbers, where the number of decimal places is expected to apply + # to the second value + ai, number = _encode_decimal(ai, fmt[3:], value[1]) + return ai, str(value[0]).rjust(3, '0') + number + value = str(value) + if fmt.startswith('N..'): + # Variable length number up to a certain length + length = int(fmt[3:]) + value = value[:length + 1] + number, decimals = (value.split('.') + [''])[:2] + decimals = decimals[:9] + return ai[:-1] + str(len(decimals)), number + decimals + else: + # Fixed length numeric + length = int(fmt[1:]) + value = value[:length + 1] + number, decimals = (value.split('.') + [''])[:2] + decimals = decimals[:9] + return ai[:-1] + str(len(decimals)), (number + decimals).rjust(length, '0') + + +def _encode_date(fmt: str, value: object) -> str: + """Encode the specified date value given the format.""" + if isinstance(value, (list, tuple)) and fmt in ('N6..12', 'N6[+N6]'): + # Two date values + return '%s%s' % ( + _encode_date('N6', value[0]), + _encode_date('N6', value[1]), + ) + elif isinstance(value, datetime.date): + # Format date in different formats + if fmt in ('N6', 'N6..12', 'N6[+N6]'): + return value.strftime('%y%m%d') + elif fmt == 'N10': + return value.strftime('%y%m%d%H%M') + elif fmt in ('N6+N..4', 'N6[+N..4]', 'N6[+N4]'): + value = value.strftime('%y%m%d%H%M') + if value.endswith('00'): + value = value[:-2] + if value.endswith('00'): + value = value[:-2] + return value + elif fmt in ('N8+N..4', 'N8[+N..4]'): + value = value.strftime('%y%m%d%H%M%S') + if value.endswith('00'): + value = value[:-2] + if value.endswith('00'): + value = value[:-2] + return value + else: # pragma: no cover (all formats should be covered) + raise ValueError('unsupported format: %s' % fmt) + else: + # Value is assumed to be in the correct format already + return str(value) + + +def _encode_value(ai: str, fmt: str, _type: str, value: object) -> tuple[str, str]: """Encode the specified value given the format and type.""" if _type == 'decimal': - if isinstance(value, (list, tuple)) and fmt.startswith('N3+'): - number = _encode_value(fmt[3:], _type, value[1]) - assert isinstance(value[0], str) - return number[0] + value[0].rjust(3, '0') + number[1:] - value = str(value) - if fmt.startswith('N..'): - length = int(fmt[3:]) - value = value[:length + 1] - number, digits = (value.split('.') + [''])[:2] - digits = digits[:9] - return str(len(digits)) + number + digits - else: - length = int(fmt[1:]) - value = value[:length + 1] - number, digits = (value.split('.') + [''])[:2] - digits = digits[:9] - return str(len(digits)) + (number + digits).rjust(length, '0') + return _encode_decimal(ai, fmt, value) elif _type == 'date': - if isinstance(value, (list, tuple)) and fmt in ('N6..12', 'N6[+N6]'): - return '%s%s' % ( - _encode_value('N6', _type, value[0]), - _encode_value('N6', _type, value[1])) - elif isinstance(value, datetime.date): - if fmt in ('N6', 'N6..12', 'N6[+N6]'): - return value.strftime('%y%m%d') - elif fmt == 'N10': - return value.strftime('%y%m%d%H%M') - elif fmt in ('N6+N..4', 'N6[+N..4]', 'N6[+N4]'): - value = value.strftime('%y%m%d%H%M') - if value.endswith('00'): - value = value[:-2] - if value.endswith('00'): - value = value[:-2] - return value - elif fmt in ('N8+N..4', 'N8[+N..4]'): - value = value.strftime('%y%m%d%H%M%S') - if value.endswith('00'): - value = value[:-2] - if value.endswith('00'): - value = value[:-2] - return value - else: # pragma: no cover (all formats should be covered) - raise ValueError('unsupported format: %s' % fmt) - return str(value) - - -def _max_length(fmt: str, _type: str) -> int: - """Determine the maximum length based on the format ad type.""" - length = sum( + return ai, _encode_date(fmt, value) + else: # str or int types + return ai, str(value) + + +def _max_length(fmt: str) -> int: + """Determine the maximum length based on the format.""" + return sum( int(re.match(r'^[NXY][0-9]*?[.]*([0-9]+)[\[\]]?$', x).group(1)) # type: ignore[misc, union-attr] for x in fmt.split('+') ) - if _type == 'decimal': - length += 1 - return length def _pad_value(fmt: str, _type: str, value: str) -> str: """Pad the value to the maximum length for the format.""" if _type in ('decimal', 'int'): - return value.rjust(_max_length(fmt, _type), '0') - return value.ljust(_max_length(fmt, _type)) + return value.rjust(_max_length(fmt), '0') + else: + return value.ljust(_max_length(fmt)) + + +def _decode_decimal(ai: str, fmt: str, value: str) -> decimal.Decimal | tuple[str, decimal.Decimal]: + """Decode the specified decimal value given the fmt.""" + if fmt.startswith('N3+'): + # If the number consists of two parts, it is assumed that the decimal + # from the AI applies to the second part + return (value[:3], _decode_decimal(ai, fmt[3:], value[3:])) # type: ignore[return-value] + decimals = int(ai[-1]) + if decimals: + value = value[:-decimals] + '.' + value[-decimals:] + return decimal.Decimal(value) + + +def _decode_date(fmt: str, value: str) -> datetime.date | datetime.datetime | tuple[datetime.date, datetime.date]: + """Decode the specified date value given the fmt.""" + if len(value) == 6: + if value[4:] == '00': + # When day == '00', it must be interpreted as last day of month + date = datetime.datetime.strptime(value[:4], '%y%m') + if date.month == 12: + date = date.replace(day=31) + else: + date = date.replace(month=date.month + 1, day=1) - datetime.timedelta(days=1) + return date.date() + else: + return datetime.datetime.strptime(value, '%y%m%d').date() + elif len(value) == 12 and fmt in ('N12', 'N6..12', 'N6[+N6]'): + return (_decode_date('N6', value[:6]), _decode_date('N6', value[6:])) # type: ignore[return-value] + else: + # Other lengths are interpreted as variable-length datetime values + return datetime.datetime.strptime(value, '%y%m%d%H%M%S'[:len(value)]) -def _decode_value(fmt: str, _type: str, value: str) -> Any: +def _decode_value(ai: str, fmt: str, _type: str, value: str) -> Any: """Decode the specified value given the fmt and type.""" if _type == 'decimal': - if fmt.startswith('N3+'): - return (value[1:4], _decode_value(fmt[3:], _type, value[0] + value[4:])) - digits = int(value[0]) - value = value[1:] - if digits: - value = value[:-digits] + '.' + value[-digits:] - return decimal.Decimal(value) + return _decode_decimal(ai, fmt, value) elif _type == 'date': - if len(value) == 6: - if value[4:] == '00': - # When day == '00', it must be interpreted as last day of month - date = datetime.datetime.strptime(value[:4], '%y%m') - if date.month == 12: - date = date.replace(day=31) - else: - date = date.replace(month=date.month + 1, day=1) - datetime.timedelta(days=1) - return date.date() - else: - return datetime.datetime.strptime(value, '%y%m%d').date() - elif len(value) == 12 and fmt in ('N12', 'N6..12', 'N6[+N6]'): - return (_decode_value('N6', _type, value[:6]), _decode_value('N6', _type, value[6:])) - else: - # other lengths are interpreted as variable-length datetime values - return datetime.datetime.strptime(value, '%y%m%d%H%M%S'[:len(value)]) + return _decode_date(fmt, value) elif _type == 'int': return int(value) - return value.strip() + else: # str + return value.strip() def info(number: str, separator: str = '') -> dict[str, Any]: @@ -208,7 +240,7 @@ def info(number: str, separator: str = '') -> dict[str, Any]: raise InvalidComponent() number = number[len(ai):] # figure out the value part - value = number[:_max_length(info['format'], info['type'])] + value = number[:_max_length(info['format'])] if separator and info.get('fnc1'): idx = number.find(separator) if idx > 0: @@ -219,7 +251,7 @@ def info(number: str, separator: str = '') -> dict[str, Any]: mod = __import__(_ai_validators[ai], globals(), locals(), ['validate']) mod.validate(value) # convert the number - data[ai] = _decode_value(info['format'], info['type'], value) + data[ai] = _decode_value(ai, info['format'], info['type'], value) # skip separator if separator and number.startswith(separator): number = number[len(separator):] @@ -253,12 +285,12 @@ def encode(data: Mapping[str, object], separator: str = '', parentheses: bool = if ai in _ai_validators: mod = __import__(_ai_validators[ai], globals(), locals(), ['validate']) mod.validate(value) - value = _encode_value(info['format'], info['type'], value) + ai, value = _encode_value(ai, info['format'], info['type'], value) # store variable-sized values separate from fixed-size values - if info.get('fnc1'): - variable_values.append((ai_fmt % ai, info['format'], info['type'], value)) - else: + if not info.get('fnc1'): fixed_values.append(ai_fmt % ai + value) + else: + variable_values.append((ai_fmt % ai, info['format'], info['type'], value)) # we need the separator for all but the last variable-sized value # (or pad values if we don't have a separator) return ''.join( diff --git a/stdnum/gs1_ai.dat b/stdnum/gs1_ai.dat index f05c502..4a98ba0 100644 --- a/stdnum/gs1_ai.dat +++ b/stdnum/gs1_ai.dat @@ -1,5 +1,5 @@ # generated from https://ref.gs1.org/ai/ -# on 2025-05-17 11:59:28.567729+00:00 +# on 2025-05-18 14:21:46.081509+00:00 00 format="N18" type="str" name="SSCC" description="Serial Shipping Container Code (SSCC)" 01 format="N14" type="str" name="GTIN" description="Global Trade Item Number (GTIN)" 02 format="N14" type="str" name="CONTENT" description="Global Trade Item Number (GTIN) of contained trade items" @@ -25,66 +25,66 @@ 254 format="X..20" type="str" fnc1="1" name="GLN EXTENSION COMPONENT" description="Global Location Number (GLN) extension component" 255 format="N13[+N..12]" type="str" fnc1="1" name="GCN" description="Global Coupon Number (GCN)" 30 format="N..8" type="int" fnc1="1" name="VAR. COUNT" description="Variable count of items (variable measure trade item)" -310 format="N6" type="decimal" name="NET WEIGHT (kg)" description="Net weight, kilograms (variable measure trade item)" -311 format="N6" type="decimal" name="LENGTH (m)" description="Length or first dimension, metres (variable measure trade item)" -312 format="N6" type="decimal" name="WIDTH (m)" description="Width, diameter, or second dimension, metres (variable measure trade item)" -313 format="N6" type="decimal" name="HEIGHT (m)" description="Depth, thickness, height, or third dimension, metres (variable measure trade item)" -314 format="N6" type="decimal" name="AREA (m²)" description="Area, square metres (variable measure trade item)" -315 format="N6" type="decimal" name="NET VOLUME (l)" description="Net volume, litres (variable measure trade item)" -316 format="N6" type="decimal" name="NET VOLUME (m³)" description="Net volume, cubic metres (variable measure trade item)" -320 format="N6" type="decimal" name="NET WEIGHT (lb)" description="Net weight, pounds (variable measure trade item)" -321 format="N6" type="decimal" name="LENGTH (in)" description="Length or first dimension, inches (variable measure trade item)" -322 format="N6" type="decimal" name="LENGTH (ft)" description="Length or first dimension, feet (variable measure trade item)" -323 format="N6" type="decimal" name="LENGTH (yd)" description="Length or first dimension, yards (variable measure trade item)" -324 format="N6" type="decimal" name="WIDTH (in)" description="Width, diameter, or second dimension, inches (variable measure trade item)" -325 format="N6" type="decimal" name="WIDTH (ft)" description="Width, diameter, or second dimension, feet (variable measure trade item)" -326 format="N6" type="decimal" name="WIDTH (yd)" description="Width, diameter, or second dimension, yards (variable measure trade item)" -327 format="N6" type="decimal" name="HEIGHT (in)" description="Depth, thickness, height, or third dimension, inches (variable measure trade item)" -328 format="N6" type="decimal" name="HEIGHT (ft)" description="Depth, thickness, height, or third dimension, feet (variable measure trade item)" -329 format="N6" type="decimal" name="HEIGHT (yd)" description="Depth, thickness, height, or third dimension, yards (variable measure trade item)" -330 format="N6" type="decimal" name="GROSS WEIGHT (kg)" description="Logistic weight, kilograms" -331 format="N6" type="decimal" name="LENGTH (m), log" description="Length or first dimension, metres" -332 format="N6" type="decimal" name="WIDTH (m), log" description="Width, diameter, or second dimension, metres" -333 format="N6" type="decimal" name="HEIGHT (m), log" description="Depth, thickness, height, or third dimension, metres" -334 format="N6" type="decimal" name="AREA (m²), log" description="Area, square metres" -335 format="N6" type="decimal" name="VOLUME (l), log" description="Logistic volume, litres" -336 format="N6" type="decimal" name="VOLUME (m³), log" description="Logistic volume, cubic metres" -337 format="N6" type="decimal" name="KG PER m²" description="Kilograms per square metre" -340 format="N6" type="decimal" name="GROSS WEIGHT (lb)" description="Logistic weight, pounds" -341 format="N6" type="decimal" name="LENGTH (in), log" description="Length or first dimension, inches" -342 format="N6" type="decimal" name="LENGTH (ft), log" description="Length or first dimension, feet" -343 format="N6" type="decimal" name="LENGTH (yd), log" description="Length or first dimension, yards" -344 format="N6" type="decimal" name="WIDTH (in), log" description="Width, diameter, or second dimension, inches" -345 format="N6" type="decimal" name="WIDTH (ft), log" description="Width, diameter, or second dimension, feet" -346 format="N6" type="decimal" name="WIDTH (yd), log" description="Width, diameter, or second dimension, yard" -347 format="N6" type="decimal" name="HEIGHT (in), log" description="Depth, thickness, height, or third dimension, inches" -348 format="N6" type="decimal" name="HEIGHT (ft), log" description="Depth, thickness, height, or third dimension, feet" -349 format="N6" type="decimal" name="HEIGHT (yd), log" description="Depth, thickness, height, or third dimension, yards" -350 format="N6" type="decimal" name="AREA (in²)" description="Area, square inches (variable measure trade item)" -351 format="N6" type="decimal" name="AREA (ft²)" description="Area, square feet (variable measure trade item)" -352 format="N6" type="decimal" name="AREA (yd²)" description="Area, square yards (variable measure trade item)" -353 format="N6" type="decimal" name="AREA (in²), log" description="Area, square inches" -354 format="N6" type="decimal" name="AREA (ft²), log" description="Area, square feet" -355 format="N6" type="decimal" name="AREA (yd²), log" description="Area, square yards" -356 format="N6" type="decimal" name="NET WEIGHT (troy oz)" description="Net weight, troy ounces (variable measure trade item)" -357 format="N6" type="decimal" name="NET VOLUME (oz)" description="Net weight (or volume), ounces (variable measure trade item)" -360 format="N6" type="decimal" name="NET VOLUME (qt)" description="Net volume, quarts (variable measure trade item)" -361 format="N6" type="decimal" name="NET VOLUME (gal.)" description="Net volume, gallons U.S. (variable measure trade item)" -362 format="N6" type="decimal" name="VOLUME (qt), log" description="Logistic volume, quarts" -363 format="N6" type="decimal" name="VOLUME (gal.), log" description="Logistic volume, gallons U.S." -364 format="N6" type="decimal" name="VOLUME (in³)" description="Net volume, cubic inches (variable measure trade item)" -365 format="N6" type="decimal" name="VOLUME (ft³)" description="Net volume, cubic feet (variable measure trade item)" -366 format="N6" type="decimal" name="VOLUME (yd³)" description="Net volume, cubic yards (variable measure trade item)" -367 format="N6" type="decimal" name="VOLUME (in³), log" description="Logistic volume, cubic inches" -368 format="N6" type="decimal" name="VOLUME (ft³), log" description="Logistic volume, cubic feet" -369 format="N6" type="decimal" name="VOLUME (yd³), log" description="Logistic volume, cubic yards" +3100-3105 format="N6" type="decimal" name="NET WEIGHT (kg)" description="Net weight, kilograms (variable measure trade item)" +3110-3115 format="N6" type="decimal" name="LENGTH (m)" description="Length or first dimension, metres (variable measure trade item)" +3120-3125 format="N6" type="decimal" name="WIDTH (m)" description="Width, diameter, or second dimension, metres (variable measure trade item)" +3130-3135 format="N6" type="decimal" name="HEIGHT (m)" description="Depth, thickness, height, or third dimension, metres (variable measure trade item)" +3140-3145 format="N6" type="decimal" name="AREA (m²)" description="Area, square metres (variable measure trade item)" +3150-3155 format="N6" type="decimal" name="NET VOLUME (l)" description="Net volume, litres (variable measure trade item)" +3160-3165 format="N6" type="decimal" name="NET VOLUME (m³)" description="Net volume, cubic metres (variable measure trade item)" +3200-3205 format="N6" type="decimal" name="NET WEIGHT (lb)" description="Net weight, pounds (variable measure trade item)" +3210-3215 format="N6" type="decimal" name="LENGTH (in)" description="Length or first dimension, inches (variable measure trade item)" +3220-3225 format="N6" type="decimal" name="LENGTH (ft)" description="Length or first dimension, feet (variable measure trade item)" +3230-3235 format="N6" type="decimal" name="LENGTH (yd)" description="Length or first dimension, yards (variable measure trade item)" +3240-3245 format="N6" type="decimal" name="WIDTH (in)" description="Width, diameter, or second dimension, inches (variable measure trade item)" +3250-3255 format="N6" type="decimal" name="WIDTH (ft)" description="Width, diameter, or second dimension, feet (variable measure trade item)" +3260-3265 format="N6" type="decimal" name="WIDTH (yd)" description="Width, diameter, or second dimension, yards (variable measure trade item)" +3270-3275 format="N6" type="decimal" name="HEIGHT (in)" description="Depth, thickness, height, or third dimension, inches (variable measure trade item)" +3280-3285 format="N6" type="decimal" name="HEIGHT (ft)" description="Depth, thickness, height, or third dimension, feet (variable measure trade item)" +3290-3295 format="N6" type="decimal" name="HEIGHT (yd)" description="Depth, thickness, height, or third dimension, yards (variable measure trade item)" +3300-3305 format="N6" type="decimal" name="GROSS WEIGHT (kg)" description="Logistic weight, kilograms" +3310-3315 format="N6" type="decimal" name="LENGTH (m), log" description="Length or first dimension, metres" +3320-3325 format="N6" type="decimal" name="WIDTH (m), log" description="Width, diameter, or second dimension, metres" +3330-3335 format="N6" type="decimal" name="HEIGHT (m), log" description="Depth, thickness, height, or third dimension, metres" +3340-3345 format="N6" type="decimal" name="AREA (m²), log" description="Area, square metres" +3350-3355 format="N6" type="decimal" name="VOLUME (l), log" description="Logistic volume, litres" +3360-3365 format="N6" type="decimal" name="VOLUME (m³), log" description="Logistic volume, cubic metres" +3370-3375 format="N6" type="decimal" name="KG PER m²" description="Kilograms per square metre" +3400-3405 format="N6" type="decimal" name="GROSS WEIGHT (lb)" description="Logistic weight, pounds" +3410-3415 format="N6" type="decimal" name="LENGTH (in), log" description="Length or first dimension, inches" +3420-3425 format="N6" type="decimal" name="LENGTH (ft), log" description="Length or first dimension, feet" +3430-3435 format="N6" type="decimal" name="LENGTH (yd), log" description="Length or first dimension, yards" +3440-3445 format="N6" type="decimal" name="WIDTH (in), log" description="Width, diameter, or second dimension, inches" +3450-3455 format="N6" type="decimal" name="WIDTH (ft), log" description="Width, diameter, or second dimension, feet" +3460-3465 format="N6" type="decimal" name="WIDTH (yd), log" description="Width, diameter, or second dimension, yard" +3470-3475 format="N6" type="decimal" name="HEIGHT (in), log" description="Depth, thickness, height, or third dimension, inches" +3480-3485 format="N6" type="decimal" name="HEIGHT (ft), log" description="Depth, thickness, height, or third dimension, feet" +3490-3495 format="N6" type="decimal" name="HEIGHT (yd), log" description="Depth, thickness, height, or third dimension, yards" +3500-3505 format="N6" type="decimal" name="AREA (in²)" description="Area, square inches (variable measure trade item)" +3510-3515 format="N6" type="decimal" name="AREA (ft²)" description="Area, square feet (variable measure trade item)" +3520-3525 format="N6" type="decimal" name="AREA (yd²)" description="Area, square yards (variable measure trade item)" +3530-3535 format="N6" type="decimal" name="AREA (in²), log" description="Area, square inches" +3540-3545 format="N6" type="decimal" name="AREA (ft²), log" description="Area, square feet" +3550-3555 format="N6" type="decimal" name="AREA (yd²), log" description="Area, square yards" +3560-3565 format="N6" type="decimal" name="NET WEIGHT (troy oz)" description="Net weight, troy ounces (variable measure trade item)" +3570-3575 format="N6" type="decimal" name="NET VOLUME (oz)" description="Net weight (or volume), ounces (variable measure trade item)" +3600-3605 format="N6" type="decimal" name="NET VOLUME (qt)" description="Net volume, quarts (variable measure trade item)" +3610-3615 format="N6" type="decimal" name="NET VOLUME (gal.)" description="Net volume, gallons U.S. (variable measure trade item)" +3620-3625 format="N6" type="decimal" name="VOLUME (qt), log" description="Logistic volume, quarts" +3630-3635 format="N6" type="decimal" name="VOLUME (gal.), log" description="Logistic volume, gallons U.S." +3640-3645 format="N6" type="decimal" name="VOLUME (in³)" description="Net volume, cubic inches (variable measure trade item)" +3650-3655 format="N6" type="decimal" name="VOLUME (ft³)" description="Net volume, cubic feet (variable measure trade item)" +3660-3665 format="N6" type="decimal" name="VOLUME (yd³)" description="Net volume, cubic yards (variable measure trade item)" +3670-3675 format="N6" type="decimal" name="VOLUME (in³), log" description="Logistic volume, cubic inches" +3680-3685 format="N6" type="decimal" name="VOLUME (ft³), log" description="Logistic volume, cubic feet" +3690-3695 format="N6" type="decimal" name="VOLUME (yd³), log" description="Logistic volume, cubic yards" 37 format="N..8" type="int" fnc1="1" name="COUNT" description="Count of trade items or trade item pieces contained in a logistic unit" -390 format="N..15" type="decimal" fnc1="1" name="AMOUNT" description="Applicable amount payable or Coupon value, local currency" -391 format="N3+N..15" type="decimal" fnc1="1" name="AMOUNT" description="Applicable amount payable with ISO currency code" -392 format="N..15" type="decimal" fnc1="1" name="PRICE" description="Applicable amount payable, single monetary area (variable measure trade item)" -393 format="N3+N..15" type="decimal" fnc1="1" name="PRICE" description="Applicable amount payable with ISO currency code (variable measure trade item)" -394 format="N4" type="decimal" fnc1="1" name="PRCNT OFF" description="Percentage discount of a coupon" -395 format="N6" type="decimal" fnc1="1" name="PRICE/UoM" description="Amount Payable per unit of measure single monetary area (variable measure trade item)" +3900-3909 format="N..15" type="decimal" fnc1="1" name="AMOUNT" description="Applicable amount payable or Coupon value, local currency" +3910-3919 format="N3+N..15" type="decimal" fnc1="1" name="AMOUNT" description="Applicable amount payable with ISO currency code" +3920-3929 format="N..15" type="decimal" fnc1="1" name="PRICE" description="Applicable amount payable, single monetary area (variable measure trade item)" +3930-3939 format="N3+N..15" type="decimal" fnc1="1" name="PRICE" description="Applicable amount payable with ISO currency code (variable measure trade item)" +3940-3943 format="N4" type="decimal" fnc1="1" name="PRCNT OFF" description="Percentage discount of a coupon" +3950-3955 format="N6" type="decimal" fnc1="1" name="PRICE/UoM" description="Amount Payable per unit of measure single monetary area (variable measure trade item)" 400 format="X..30" type="str" fnc1="1" name="ORDER NUMBER" description="Customers purchase order number" 401 format="X..30" type="str" fnc1="1" name="GINC" description="Global Identification Number for Consignment (GINC)" 402 format="N17" type="str" fnc1="1" name="GSIN" description="Global Shipment Identification Number (GSIN)" diff --git a/tests/test_gs1_128.doctest b/tests/test_gs1_128.doctest index e074a84..3d3cf5e 100644 --- a/tests/test_gs1_128.doctest +++ b/tests/test_gs1_128.doctest @@ -38,34 +38,42 @@ will be converted to the correct representation. >>> gs1_128.encode({'01': '38425876095074', '17': datetime.date(2018, 11, 19), '37': 1}, parentheses=True) '(01)38425876095074(17)181119(37)1' ->>> gs1_128.encode({'02': '98412345678908', '310': 17.23, '37': 32}) +>>> gs1_128.encode({'02': '98412345678908', '3100': 17.23, '37': 32}) '029841234567890831020017233732' +>>> gs1_128.encode({'02': '98412345678908', '3100': 17.23, '37': 32}, parentheses=True) +'(02)98412345678908(3102)001723(37)32' +>>> gs1_128.encode({'253': '1234567890005000123'}) +'2531234567890005000123' >>> gs1_128.encode({'09': '1234'}) # unknown AI Traceback (most recent call last): ... InvalidComponent: ... ->>> gs1_128.encode({'253': '1234567890005000123'}) -'2531234567890005000123' +>>> gs1_128.encode({'310': 17.23}) # no longer detected as part of the 3100-3105 range +Traceback (most recent call last): + ... +InvalidComponent: ... If we have a separator we use it to separate variable-length values, otherwise we pad all variable-length values to the maximum length (except the last one). ->>> gs1_128.encode({'01': '58425876097843', '10': '123456', '37': 18, '390': 42, '392': 12}, parentheses=True) -'(01)58425876097843(10)123456 (37)00000018(390)0000000000000042(392)012' ->>> gs1_128.encode({'01': '58425876097843', '10': '123456', '37': 18, '390': 42, '392': 12}, parentheses=True, separator='[FNC1]') -'(01)58425876097843(10)123456[FNC1](37)18[FNC1](390)042[FNC1](392)012' +>>> gs1_128.encode({'01': '58425876097843', '10': '123456', '37': 18, '3900': 42, '3920': 12}, parentheses=True) +'(01)58425876097843(10)123456 (37)00000018(3900)000000000000042(3920)12' +>>> gs1_128.encode({'01': '58425876097843', '10': '123456', '37': 18, '3900': 42, '3920': 12}, parentheses=True, separator='[FNC1]') +'(01)58425876097843(10)123456[FNC1](37)18[FNC1](3900)42[FNC1](3920)12' Numeric values can be provided in several forms and precision is encoded properly. >>> gs1_128.encode({ -... '310': 17.23, # float -... '311': 456, # int -... '312': 1.0 / 3.0, # float with lots of digits -... '313': '123.456', # str -... '391': ('123', Decimal('123.456')), # currency number combo +... '3100': 17.23, # float +... '3110': 456, # int +... '3120': 1.0 / 3.0, # float with lots of digits +... '3130': '123.456', # str +... '3910': ('123', Decimal('123.456')), # currency number combo ... }, parentheses=True) -'(310)2001723(311)0000456(312)5033333(313)3123456(391)3123123456' +'(3102)001723(3110)000456(3125)033333(3133)123456(3913)123123456' +>>> gs1_128.encode({'01': '98456789014533', '3100': Decimal('0.035')}, parentheses=True) +'(01)98456789014533(3103)000035' We generate dates in various formats, depending on the AI. @@ -104,8 +112,8 @@ pprint.pprint(gs1_128.info('(01)38425876095074(17)181119(37)1 ')) {'01': '38425876095074', '17': datetime.date(2018, 11, 19), '37': 1} >>> pprint.pprint(gs1_128.info('013842587609507417181119371')) {'01': '38425876095074', '17': datetime.date(2018, 11, 19), '37': 1} ->>> pprint.pprint(gs1_128.info('(02)98412345678908(310)3017230(37)32')) -{'02': '98412345678908', '310': Decimal('17.230'), '37': 32} +>>> pprint.pprint(gs1_128.info('(02)98412345678908(3103)017230(37)32')) +{'02': '98412345678908', '3103': Decimal('17.230'), '37': 32} >>> pprint.pprint(gs1_128.info('(01)58425876097843(10)123456 (17)181119(37)18')) {'01': '58425876097843', '10': '123456', '17': datetime.date(2018, 11, 19), '37': 18} >>> pprint.pprint(gs1_128.info('|(01)58425876097843|(10)123456|(17)181119(37)18', separator='|')) @@ -123,12 +131,14 @@ InvalidComponent: ... We can decode decimal values from various formats. ->>> pprint.pprint(gs1_128.info('(310)5033333')) -{'310': Decimal('0.33333')} ->>> pprint.pprint(gs1_128.info('(310)0033333')) -{'310': Decimal('33333')} ->>> pprint.pprint(gs1_128.info('(391)3123123456')) -{'391': ('123', Decimal('123.456'))} +>>> pprint.pprint(gs1_128.info('(3105)033333')) +{'3105': Decimal('0.33333')} +>>> pprint.pprint(gs1_128.info('(3103)000035')) +{'3103': Decimal('0.035')} +>>> pprint.pprint(gs1_128.info('(3100)033333')) +{'3100': Decimal('33333')} +>>> pprint.pprint(gs1_128.info('(3913)123123456')) +{'3913': ('123', Decimal('123.456'))} We an decode date files from various formats. diff --git a/update/gs1_ai.py b/update/gs1_ai.py index 5257988..9b1ffaa 100755 --- a/update/gs1_ai.py +++ b/update/gs1_ai.py @@ -60,11 +60,11 @@ def fetch_ais(): entry['description'].strip()) -def group_ai_ranges(): +def group_ai_ranges(ranges): """Combine downloaded application identifiers into ranges.""" first = None prev = (None, ) * 5 - for value in sorted(fetch_ais()): + for value in sorted(ranges): if value[1:] != prev[1:]: if first: yield (first, *prev) @@ -76,7 +76,7 @@ def group_ai_ranges(): if __name__ == '__main__': print('# generated from %s' % download_url) print('# on %s' % datetime.datetime.now(datetime.UTC)) - for ai1, ai2, format, require_fnc1, name, description in group_ai_ranges(): + for ai1, ai2, format, require_fnc1, name, description in group_ai_ranges(fetch_ais()): _type = 'str' if re.match(r'^(N[68]\[?\+)?N[0-9]*[.]*[0-9]+\]?$', format) and 'date' in description.lower(): _type = 'date' @@ -85,10 +85,8 @@ if __name__ == '__main__': ai = ai1 if ai1 != ai2: if len(ai1) == 4: - ai = ai1[:3] _type = 'decimal' - else: - ai = '%s-%s' % (ai1, ai2) + ai = '%s-%s' % (ai1, ai2) print('%s format="%s" type="%s"%s name="%s" description="%s"' % ( ai, format, _type, ' fnc1="1"' if require_fnc1 else '', |