1
1
# gs1_128.py - functions for handling GS1-128 codes
2
2
#
3
3
# Copyright (C) 2019 Sergi Almacellas Abellana
4
- # Copyright (C) 2020-2024 Arthur de Jong
4
+ # Copyright (C) 2020-2025 Arthur de Jong
5
5
#
6
6
# This library is free software; you can redistribute it and/or
7
7
# modify it under the terms of the GNU Lesser General Public
@@ -86,103 +86,135 @@ def compact(number: str) -> str:
86
86
return clean (number , '()' ).strip ()
87
87
88
88
89
- def _encode_value (fmt : str , _type : str , value : object ) -> str :
89
+ def _encode_decimal (ai : str , fmt : str , value : object ) -> tuple [str , str ]:
90
+ """Encode the specified decimal value given the format."""
91
+ # For decimal types the last digit of the AI is used to encode the
92
+ # number of decimal places (we replace the last digit)
93
+ if isinstance (value , (list , tuple )) and fmt .startswith ('N3+' ):
94
+ # Two numbers, where the number of decimal places is expected to apply
95
+ # to the second value
96
+ ai , number = _encode_decimal (ai , fmt [3 :], value [1 ])
97
+ return ai , str (value [0 ]).rjust (3 , '0' ) + number
98
+ value = str (value )
99
+ if fmt .startswith ('N..' ):
100
+ # Variable length number up to a certain length
101
+ length = int (fmt [3 :])
102
+ value = value [:length + 1 ]
103
+ number , decimals = (value .split ('.' ) + ['' ])[:2 ]
104
+ decimals = decimals [:9 ]
105
+ return ai [:- 1 ] + str (len (decimals )), number + decimals
106
+ else :
107
+ # Fixed length numeric
108
+ length = int (fmt [1 :])
109
+ value = value [:length + 1 ]
110
+ number , decimals = (value .split ('.' ) + ['' ])[:2 ]
111
+ decimals = decimals [:9 ]
112
+ return ai [:- 1 ] + str (len (decimals )), (number + decimals ).rjust (length , '0' )
113
+
114
+
115
+ def _encode_date (fmt : str , value : object ) -> str :
116
+ """Encode the specified date value given the format."""
117
+ if isinstance (value , (list , tuple )) and fmt in ('N6..12' , 'N6[+N6]' ):
118
+ # Two date values
119
+ return '%s%s' % (
120
+ _encode_date ('N6' , value [0 ]),
121
+ _encode_date ('N6' , value [1 ]),
122
+ )
123
+ elif isinstance (value , datetime .date ):
124
+ # Format date in different formats
125
+ if fmt in ('N6' , 'N6..12' , 'N6[+N6]' ):
126
+ return value .strftime ('%y%m%d' )
127
+ elif fmt == 'N10' :
128
+ return value .strftime ('%y%m%d%H%M' )
129
+ elif fmt in ('N6+N..4' , 'N6[+N..4]' , 'N6[+N4]' ):
130
+ value = value .strftime ('%y%m%d%H%M' )
131
+ if value .endswith ('00' ):
132
+ value = value [:- 2 ]
133
+ if value .endswith ('00' ):
134
+ value = value [:- 2 ]
135
+ return value
136
+ elif fmt in ('N8+N..4' , 'N8[+N..4]' ):
137
+ value = value .strftime ('%y%m%d%H%M%S' )
138
+ if value .endswith ('00' ):
139
+ value = value [:- 2 ]
140
+ if value .endswith ('00' ):
141
+ value = value [:- 2 ]
142
+ return value
143
+ else : # pragma: no cover (all formats should be covered)
144
+ raise ValueError ('unsupported format: %s' % fmt )
145
+ else :
146
+ # Value is assumed to be in the correct format already
147
+ return str (value )
148
+
149
+
150
+ def _encode_value (ai : str , fmt : str , _type : str , value : object ) -> tuple [str , str ]:
90
151
"""Encode the specified value given the format and type."""
91
152
if _type == 'decimal' :
92
- if isinstance (value , (list , tuple )) and fmt .startswith ('N3+' ):
93
- number = _encode_value (fmt [3 :], _type , value [1 ])
94
- assert isinstance (value [0 ], str )
95
- return number [0 ] + value [0 ].rjust (3 , '0' ) + number [1 :]
96
- value = str (value )
97
- if fmt .startswith ('N..' ):
98
- length = int (fmt [3 :])
99
- value = value [:length + 1 ]
100
- number , digits = (value .split ('.' ) + ['' ])[:2 ]
101
- digits = digits [:9 ]
102
- return str (len (digits )) + number + digits
103
- else :
104
- length = int (fmt [1 :])
105
- value = value [:length + 1 ]
106
- number , digits = (value .split ('.' ) + ['' ])[:2 ]
107
- digits = digits [:9 ]
108
- return str (len (digits )) + (number + digits ).rjust (length , '0' )
153
+ return _encode_decimal (ai , fmt , value )
109
154
elif _type == 'date' :
110
- if isinstance (value , (list , tuple )) and fmt in ('N6..12' , 'N6[+N6]' ):
111
- return '%s%s' % (
112
- _encode_value ('N6' , _type , value [0 ]),
113
- _encode_value ('N6' , _type , value [1 ]))
114
- elif isinstance (value , datetime .date ):
115
- if fmt in ('N6' , 'N6..12' , 'N6[+N6]' ):
116
- return value .strftime ('%y%m%d' )
117
- elif fmt == 'N10' :
118
- return value .strftime ('%y%m%d%H%M' )
119
- elif fmt in ('N6+N..4' , 'N6[+N..4]' , 'N6[+N4]' ):
120
- value = value .strftime ('%y%m%d%H%M' )
121
- if value .endswith ('00' ):
122
- value = value [:- 2 ]
123
- if value .endswith ('00' ):
124
- value = value [:- 2 ]
125
- return value
126
- elif fmt in ('N8+N..4' , 'N8[+N..4]' ):
127
- value = value .strftime ('%y%m%d%H%M%S' )
128
- if value .endswith ('00' ):
129
- value = value [:- 2 ]
130
- if value .endswith ('00' ):
131
- value = value [:- 2 ]
132
- return value
133
- else : # pragma: no cover (all formats should be covered)
134
- raise ValueError ('unsupported format: %s' % fmt )
135
- return str (value )
136
-
137
-
138
- def _max_length (fmt : str , _type : str ) -> int :
139
- """Determine the maximum length based on the format ad type."""
140
- length = sum (
155
+ return ai , _encode_date (fmt , value )
156
+ else : # str or int types
157
+ return ai , str (value )
158
+
159
+
160
+ def _max_length (fmt : str ) -> int :
161
+ """Determine the maximum length based on the format."""
162
+ return sum (
141
163
int (re .match (r'^[NXY][0-9]*?[.]*([0-9]+)[\[\]]?$' , x ).group (1 )) # type: ignore[misc, union-attr]
142
164
for x in fmt .split ('+' )
143
165
)
144
- if _type == 'decimal' :
145
- length += 1
146
- return length
147
166
148
167
149
168
def _pad_value (fmt : str , _type : str , value : str ) -> str :
150
169
"""Pad the value to the maximum length for the format."""
151
170
if _type in ('decimal' , 'int' ):
152
- return value .rjust (_max_length (fmt , _type ), '0' )
153
- return value .ljust (_max_length (fmt , _type ))
171
+ return value .rjust (_max_length (fmt ), '0' )
172
+ else :
173
+ return value .ljust (_max_length (fmt ))
174
+
175
+
176
+ def _decode_decimal (ai : str , fmt : str , value : str ) -> decimal .Decimal | tuple [str , decimal .Decimal ]:
177
+ """Decode the specified decimal value given the fmt."""
178
+ if fmt .startswith ('N3+' ):
179
+ # If the number consists of two parts, it is assumed that the decimal
180
+ # from the AI applies to the second part
181
+ return (value [:3 ], _decode_decimal (ai , fmt [3 :], value [3 :])) # type: ignore[return-value]
182
+ decimals = int (ai [- 1 ])
183
+ if decimals :
184
+ value = value [:- decimals ] + '.' + value [- decimals :]
185
+ return decimal .Decimal (value )
186
+
187
+
188
+ def _decode_date (fmt : str , value : str ) -> datetime .date | datetime .datetime | tuple [datetime .date , datetime .date ]:
189
+ """Decode the specified date value given the fmt."""
190
+ if len (value ) == 6 :
191
+ if value [4 :] == '00' :
192
+ # When day == '00', it must be interpreted as last day of month
193
+ date = datetime .datetime .strptime (value [:4 ], '%y%m' )
194
+ if date .month == 12 :
195
+ date = date .replace (day = 31 )
196
+ else :
197
+ date = date .replace (month = date .month + 1 , day = 1 ) - datetime .timedelta (days = 1 )
198
+ return date .date ()
199
+ else :
200
+ return datetime .datetime .strptime (value , '%y%m%d' ).date ()
201
+ elif len (value ) == 12 and fmt in ('N12' , 'N6..12' , 'N6[+N6]' ):
202
+ return (_decode_date ('N6' , value [:6 ]), _decode_date ('N6' , value [6 :])) # type: ignore[return-value]
203
+ else :
204
+ # Other lengths are interpreted as variable-length datetime values
205
+ return datetime .datetime .strptime (value , '%y%m%d%H%M%S' [:len (value )])
154
206
155
207
156
- def _decode_value (fmt : str , _type : str , value : str ) -> Any :
208
+ def _decode_value (ai : str , fmt : str , _type : str , value : str ) -> Any :
157
209
"""Decode the specified value given the fmt and type."""
158
210
if _type == 'decimal' :
159
- if fmt .startswith ('N3+' ):
160
- return (value [1 :4 ], _decode_value (fmt [3 :], _type , value [0 ] + value [4 :]))
161
- digits = int (value [0 ])
162
- value = value [1 :]
163
- if digits :
164
- value = value [:- digits ] + '.' + value [- digits :]
165
- return decimal .Decimal (value )
211
+ return _decode_decimal (ai , fmt , value )
166
212
elif _type == 'date' :
167
- if len (value ) == 6 :
168
- if value [4 :] == '00' :
169
- # When day == '00', it must be interpreted as last day of month
170
- date = datetime .datetime .strptime (value [:4 ], '%y%m' )
171
- if date .month == 12 :
172
- date = date .replace (day = 31 )
173
- else :
174
- date = date .replace (month = date .month + 1 , day = 1 ) - datetime .timedelta (days = 1 )
175
- return date .date ()
176
- else :
177
- return datetime .datetime .strptime (value , '%y%m%d' ).date ()
178
- elif len (value ) == 12 and fmt in ('N12' , 'N6..12' , 'N6[+N6]' ):
179
- return (_decode_value ('N6' , _type , value [:6 ]), _decode_value ('N6' , _type , value [6 :]))
180
- else :
181
- # other lengths are interpreted as variable-length datetime values
182
- return datetime .datetime .strptime (value , '%y%m%d%H%M%S' [:len (value )])
213
+ return _decode_date (fmt , value )
183
214
elif _type == 'int' :
184
215
return int (value )
185
- return value .strip ()
216
+ else : # str
217
+ return value .strip ()
186
218
187
219
188
220
def info (number : str , separator : str = '' ) -> dict [str , Any ]:
@@ -208,7 +240,7 @@ def info(number: str, separator: str = '') -> dict[str, Any]:
208
240
raise InvalidComponent ()
209
241
number = number [len (ai ):]
210
242
# figure out the value part
211
- value = number [:_max_length (info ['format' ], info [ 'type' ] )]
243
+ value = number [:_max_length (info ['format' ])]
212
244
if separator and info .get ('fnc1' ):
213
245
idx = number .find (separator )
214
246
if idx > 0 :
@@ -219,7 +251,7 @@ def info(number: str, separator: str = '') -> dict[str, Any]:
219
251
mod = __import__ (_ai_validators [ai ], globals (), locals (), ['validate' ])
220
252
mod .validate (value )
221
253
# convert the number
222
- data [ai ] = _decode_value (info ['format' ], info ['type' ], value )
254
+ data [ai ] = _decode_value (ai , info ['format' ], info ['type' ], value )
223
255
# skip separator
224
256
if separator and number .startswith (separator ):
225
257
number = number [len (separator ):]
@@ -253,12 +285,12 @@ def encode(data: Mapping[str, object], separator: str = '', parentheses: bool =
253
285
if ai in _ai_validators :
254
286
mod = __import__ (_ai_validators [ai ], globals (), locals (), ['validate' ])
255
287
mod .validate (value )
256
- value = _encode_value (info ['format' ], info ['type' ], value )
288
+ ai , value = _encode_value (ai , info ['format' ], info ['type' ], value )
257
289
# store variable-sized values separate from fixed-size values
258
- if info .get ('fnc1' ):
259
- variable_values .append ((ai_fmt % ai , info ['format' ], info ['type' ], value ))
260
- else :
290
+ if not info .get ('fnc1' ):
261
291
fixed_values .append (ai_fmt % ai + value )
292
+ else :
293
+ variable_values .append ((ai_fmt % ai , info ['format' ], info ['type' ], value ))
262
294
# we need the separator for all but the last variable-sized value
263
295
# (or pad values if we don't have a separator)
264
296
return '' .join (
0 commit comments