11# gs1_128.py - functions for handling GS1-128 codes
22#
33# Copyright (C) 2019 Sergi Almacellas Abellana
4- # Copyright (C) 2020-2024 Arthur de Jong
4+ # Copyright (C) 2020-2025 Arthur de Jong
55#
66# This library is free software; you can redistribute it and/or
77# modify it under the terms of the GNU Lesser General Public
@@ -86,103 +86,135 @@ def compact(number: str) -> str:
8686 return clean (number , '()' ).strip ()
8787
8888
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 ]:
90151 """Encode the specified value given the format and type."""
91152 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 )
109154 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 (
141163 int (re .match (r'^[NXY][0-9]*?[.]*([0-9]+)[\[\]]?$' , x ).group (1 )) # type: ignore[misc, union-attr]
142164 for x in fmt .split ('+' )
143165 )
144- if _type == 'decimal' :
145- length += 1
146- return length
147166
148167
149168def _pad_value (fmt : str , _type : str , value : str ) -> str :
150169 """Pad the value to the maximum length for the format."""
151170 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 )])
154206
155207
156- def _decode_value (fmt : str , _type : str , value : str ) -> Any :
208+ def _decode_value (ai : str , fmt : str , _type : str , value : str ) -> Any :
157209 """Decode the specified value given the fmt and type."""
158210 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 )
166212 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 )
183214 elif _type == 'int' :
184215 return int (value )
185- return value .strip ()
216+ else : # str
217+ return value .strip ()
186218
187219
188220def info (number : str , separator : str = '' ) -> dict [str , Any ]:
@@ -208,7 +240,7 @@ def info(number: str, separator: str = '') -> dict[str, Any]:
208240 raise InvalidComponent ()
209241 number = number [len (ai ):]
210242 # figure out the value part
211- value = number [:_max_length (info ['format' ], info [ 'type' ] )]
243+ value = number [:_max_length (info ['format' ])]
212244 if separator and info .get ('fnc1' ):
213245 idx = number .find (separator )
214246 if idx > 0 :
@@ -219,7 +251,7 @@ def info(number: str, separator: str = '') -> dict[str, Any]:
219251 mod = __import__ (_ai_validators [ai ], globals (), locals (), ['validate' ])
220252 mod .validate (value )
221253 # convert the number
222- data [ai ] = _decode_value (info ['format' ], info ['type' ], value )
254+ data [ai ] = _decode_value (ai , info ['format' ], info ['type' ], value )
223255 # skip separator
224256 if separator and number .startswith (separator ):
225257 number = number [len (separator ):]
@@ -253,12 +285,12 @@ def encode(data: Mapping[str, object], separator: str = '', parentheses: bool =
253285 if ai in _ai_validators :
254286 mod = __import__ (_ai_validators [ai ], globals (), locals (), ['validate' ])
255287 mod .validate (value )
256- value = _encode_value (info ['format' ], info ['type' ], value )
288+ ai , value = _encode_value (ai , info ['format' ], info ['type' ], value )
257289 # 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' ):
261291 fixed_values .append (ai_fmt % ai + value )
292+ else :
293+ variable_values .append ((ai_fmt % ai , info ['format' ], info ['type' ], value ))
262294 # we need the separator for all but the last variable-sized value
263295 # (or pad values if we don't have a separator)
264296 return '' .join (
0 commit comments