From 164537bc30e537c5917c0e1048677ab406e1e486 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Tue, 16 Jun 2026 12:47:18 +0200 Subject: [PATCH] Support GS1-128 date AIs with a four-digit year AI 7250 (DOB, N8 YYYYMMDD) and 7251 (DOB TIME, N12 YYYYMMDDhhmm) use a four-digit year, but _encode_date() and _decode_date() only handled two-digit-year formats. Encoding raised "unsupported format: N8" and decoding raised a bare ValueError: N8 was parsed with '%y%m%d%H' and N12 was mistaken for two YYMMDD dates. Handle N8 and N12 explicitly in both directions and stop treating N12 as a pair of dates (only N6[+N6] / N6..12 encode two dates). --- stdnum/gs1_128.py | 18 ++++++++++++++++-- tests/test_gs1_128.doctest | 8 ++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/stdnum/gs1_128.py b/stdnum/gs1_128.py index 001dd584..03cff8a4 100644 --- a/stdnum/gs1_128.py +++ b/stdnum/gs1_128.py @@ -122,8 +122,15 @@ def _encode_date(fmt: str, value: object) -> str: # Format date in different formats if fmt in ('N6', 'N6..12', 'N6[+N6]'): return value.strftime('%y%m%d') + elif fmt == 'N8': + # Date with a four-digit year (YYYYMMDD), e.g. AI 7250 (DOB). + return value.strftime('%Y%m%d') elif fmt == 'N10': return value.strftime('%y%m%d%H%M') + elif fmt == 'N12': + # Date and time with a four-digit year (YYYYMMDDhhmm), e.g. AI 7251 + # (DOB TIME). + 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'): @@ -185,7 +192,14 @@ def _decode_decimal(ai: str, fmt: str, value: str) -> decimal.Decimal | tuple[st 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 fmt == 'N8': + # Date with a four-digit year (YYYYMMDD), e.g. AI 7250 (DOB). + return datetime.datetime.strptime(value, '%Y%m%d').date() + elif fmt == 'N12': + # Date and time with a four-digit year (YYYYMMDDhhmm), e.g. AI 7251 + # (DOB TIME). This is a single datetime, not two YYMMDD dates. + return datetime.datetime.strptime(value, '%Y%m%d%H%M') + elif 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') @@ -196,7 +210,7 @@ def _decode_date(fmt: str, value: str) -> datetime.date | datetime.datetime | tu 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]'): + elif len(value) == 12 and fmt in ('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 diff --git a/tests/test_gs1_128.doctest b/tests/test_gs1_128.doctest index 7036d271..98172f4a 100644 --- a/tests/test_gs1_128.doctest +++ b/tests/test_gs1_128.doctest @@ -94,6 +94,10 @@ We generate dates in various formats, depending on the AI. '(7011)181119' >>> gs1_128.encode({'7011': datetime.datetime(2018, 11, 19, 12, 45)}, parentheses=True) '(7011)1811191245' +>>> gs1_128.encode({'7250': datetime.date(1980, 7, 15)}, parentheses=True) # four-digit year +'(7250)19800715' +>>> gs1_128.encode({'7251': datetime.datetime(1980, 7, 15, 14, 30)}, parentheses=True) +'(7251)198007151430' If we try to encode an invalid EAN we will get an error. @@ -154,6 +158,10 @@ We an decode date files from various formats. {'7011': datetime.date(2018, 11, 19)} >>> pprint.pprint(gs1_128.info('(7011)1811191245')) {'7011': datetime.datetime(2018, 11, 19, 12, 45)} +>>> pprint.pprint(gs1_128.info('(7250)19800715')) +{'7250': datetime.date(1980, 7, 15)} +>>> pprint.pprint(gs1_128.info('(7251)198007151430')) +{'7251': datetime.datetime(1980, 7, 15, 14, 30)} While the compact() function can clean up the number somewhat the validate()