diff --git a/src/main/java/se/injoin/gs1utils/ApplicationIdentifier.java b/src/main/java/se/injoin/gs1utils/ApplicationIdentifier.java index 2d13639..2cfe696 100644 --- a/src/main/java/se/injoin/gs1utils/ApplicationIdentifier.java +++ b/src/main/java/se/injoin/gs1utils/ApplicationIdentifier.java @@ -16,7 +16,7 @@ package se.injoin.gs1utils; /** - * Based on GS1 General Specifications, Release 17.0.1. + * Based on GS1 General Specifications, Release 19 *

* http://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdf */ @@ -98,6 +98,11 @@ public enum ApplicationIdentifier { */ SERIAL_NUMBER("21", Format.ALPHANUMERIC_VARIABLE, 20), + /** + * Consumer product variant [r.19] + */ + CONSUMER_PRODUCT_VARIANT("22", Format.ALPHANUMERIC_VARIABLE, 20), + /** * Additional product identification assigned by the manufacturer. */ @@ -162,6 +167,7 @@ public enum ApplicationIdentifier { ITEM_AREA_SQUARE_METRES("314", Format.DECIMAL), ITEM_NET_VOLUME_LITRES("315", Format.DECIMAL), ITEM_NET_VOLUME_CUBIC_METRES("316", Format.DECIMAL), + ITEM_NET_WEIGHT_POUNDS("320", Format.DECIMAL), ITEM_LENGTH_INCHES("321", Format.DECIMAL), ITEM_LENGTH_FEET("322", Format.DECIMAL), @@ -172,16 +178,6 @@ public enum ApplicationIdentifier { ITEM_HEIGHT_INCHES("327", Format.DECIMAL), ITEM_HEIGHT_FEET("328", Format.DECIMAL), ITEM_HEIGHT_YARDS("329", Format.DECIMAL), - ITEM_AREA_SQUARE_INCHES("350", Format.DECIMAL), - ITEM_AREA_SQUARE_FEET("351", Format.DECIMAL), - ITEM_AREA_SQUARE_YARDS("352", Format.DECIMAL), - ITEM_NET_WEIGHT_TROY_OUNCES("356", Format.DECIMAL), - ITEM_NET_VOLUME_OUNCES("357", Format.DECIMAL), - ITEM_NET_VOLUME_QUARTS("360", Format.DECIMAL), - ITEM_NET_VOLUME_GALLONS("361", Format.DECIMAL), - ITEM_NET_VOLUME_CUBIC_INCHES("364", Format.DECIMAL), - ITEM_NET_VOLUME_CUBIC_FEET("365", Format.DECIMAL), - ITEM_NET_VOLUME_CUBIC_YARDS("366", Format.DECIMAL), CONTAINER_GROSS_WEIGHT_KG("330", Format.DECIMAL), CONTAINER_LENGTH_METRES("331", Format.DECIMAL), @@ -190,6 +186,11 @@ public enum ApplicationIdentifier { CONTAINER_AREA_SQUARE_METRES("334", Format.DECIMAL), CONTAINER_VOLUME_LITRES("335", Format.DECIMAL), CONTAINER_VOLUME_CUBIC_METRES("336", Format.DECIMAL), + /** + * Kilograms per square metre, of a particular trade item + */ + KILOGRAMS_PER_SQUARE_METRE("337", Format.DECIMAL), + CONTAINER_GROSS_WEIGHT_POUNDS("340", Format.DECIMAL), CONTAINER_LENGTH_INCHES("341", Format.DECIMAL), CONTAINER_LENGTH_FEET("342", Format.DECIMAL), @@ -200,20 +201,27 @@ public enum ApplicationIdentifier { CONTAINER_HEIGHT_INCHES("347", Format.DECIMAL), CONTAINER_HEIGHT_FEET("348", Format.DECIMAL), CONTAINER_HEIGHT_YARDS("349", Format.DECIMAL), + + ITEM_AREA_SQUARE_INCHES("350", Format.DECIMAL), + ITEM_AREA_SQUARE_FEET("351", Format.DECIMAL), + ITEM_AREA_SQUARE_YARDS("352", Format.DECIMAL), CONTAINER_AREA_SQUARE_INCHES("353", Format.DECIMAL), CONTAINER_AREA_SQUARE_FEET("354", Format.DECIMAL), CONTAINER_AREA_SQUARE_YARDS("355", Format.DECIMAL), + ITEM_NET_WEIGHT_TROY_OUNCES("356", Format.DECIMAL), + ITEM_NET_VOLUME_OUNCES("357", Format.DECIMAL), + + ITEM_NET_VOLUME_QUARTS("360", Format.DECIMAL), + ITEM_NET_VOLUME_GALLONS("361", Format.DECIMAL), CONTAINER_VOLUME_QUARTS("362", Format.DECIMAL), CONTAINER_VOLUME_GALLONS("363", Format.DECIMAL), + ITEM_NET_VOLUME_CUBIC_INCHES("364", Format.DECIMAL), + ITEM_NET_VOLUME_CUBIC_FEET("365", Format.DECIMAL), + ITEM_NET_VOLUME_CUBIC_YARDS("366", Format.DECIMAL), CONTAINER_VOLUME_CUBIC_INCHES("367", Format.DECIMAL), CONTAINER_VOLUME_CUBIC_FEET("368", Format.DECIMAL), CONTAINER_VOLUME_CUBIC_YARDS("369", Format.DECIMAL), - /** - * Kilograms per square metre, of a particular trade item - */ - KILOGRAMS_PER_SQUARE_METRE("337", Format.DECIMAL), - /** * Count of trade items contained in a logistic unit. * @@ -427,7 +435,7 @@ public enum ApplicationIdentifier { /** * Revision status. */ - REVISION_STATUS("7021", Format.ALPHANUMERIC_VARIABLE, 20), + REVISION_STATUS("7022", Format.ALPHANUMERIC_VARIABLE, 20), /** * Global Individual Asset Identifier of an assembly. @@ -478,6 +486,12 @@ public enum ApplicationIdentifier { */ PRODUCTION_DATE_AND_TIME("8008", Format.CUSTOM, 8, 12), + /** + * Optically Readable Sensor Indicator. + */ + OPTICAL_READABLE_SENSOR_INDICATOR("8009", Format.ALPHANUMERIC_VARIABLE, 50), + + /** * Component / Part Identifier. */ @@ -492,6 +506,11 @@ public enum ApplicationIdentifier { * Software version. */ SOFTWARE_VERSION("8012", Format.ALPHANUMERIC_VARIABLE, 20), + + /** + * Global Model Number. [r.19] + */ + GLOBAL_MODEL_NUMBER("8013", Format.ALPHANUMERIC_VARIABLE, 30), /** * Global Service Relation Number of provider. @@ -515,6 +534,11 @@ public enum ApplicationIdentifier { */ PAYMENT_SLIP_REFERENCE_NUMBER("8020", Format.ALPHANUMERIC_VARIABLE, 25), + /** + * Identification of pieces of a trade item contained in a logistic unit [r.19] + */ + ITIP_CONTAINED_IN_A_LOGISTIC_UNIT("8026", Format.NUMERIC_FIXED, 18), + /** * Coupon code identification for use in North America. */ @@ -540,9 +564,17 @@ public enum ApplicationIdentifier { /** * Information mutually agreed between trading partners. */ - MUTUALLY_AGREED_INFORMATION("90", Format.ALPHANUMERIC_VARIABLE, 30); - - // No constants defined for 91-99 Company internal information. Query parse result using strings. + MUTUALLY_AGREED_INFORMATION("90", Format.ALPHANUMERIC_VARIABLE, 30), + + COMPANY_INTERNAL_INFORMATION_1("91", Format.ALPHANUMERIC_VARIABLE, 90), + COMPANY_INTERNAL_INFORMATION_2("92", Format.ALPHANUMERIC_VARIABLE, 90), + COMPANY_INTERNAL_INFORMATION_3("93", Format.ALPHANUMERIC_VARIABLE, 90), + COMPANY_INTERNAL_INFORMATION_4("94", Format.ALPHANUMERIC_VARIABLE, 90), + COMPANY_INTERNAL_INFORMATION_5("95", Format.ALPHANUMERIC_VARIABLE, 90), + COMPANY_INTERNAL_INFORMATION_6("96", Format.ALPHANUMERIC_VARIABLE, 90), + COMPANY_INTERNAL_INFORMATION_7("97", Format.ALPHANUMERIC_VARIABLE, 90), + COMPANY_INTERNAL_INFORMATION_8("98", Format.ALPHANUMERIC_VARIABLE, 90), + COMPANY_INTERNAL_INFORMATION_9("99", Format.ALPHANUMERIC_VARIABLE, 90); private final String key; private final Format format; diff --git a/src/main/java/se/injoin/gs1utils/ApplicationIdentifierTree.java b/src/main/java/se/injoin/gs1utils/ApplicationIdentifierTree.java new file mode 100644 index 0000000..77c6c7f --- /dev/null +++ b/src/main/java/se/injoin/gs1utils/ApplicationIdentifierTree.java @@ -0,0 +1,79 @@ +package se.injoin.gs1utils; + +class ApplicationIdentifierTree { + static ApplicationIdentifierTree instance = buildTree(); + + static ApplicationIdentifier get(String key) { + ApplicationIdentifierTree tree = instance; + for (int i = 0; i < key.length(); i++) { + char ch = key.charAt(i); + if (ch < '0' || ch > '9') + return null; + IdentifierOrTree node = tree.map[(int)(ch - '0')]; + if (null == node) + return null; + if (node.isTree()) { + tree = node.getTree(); + } else { + return node.getIdentifier(); + } + } + return null; + } + + static class IdentifierOrTree { + ApplicationIdentifier identifier = null; + ApplicationIdentifierTree tree = null; + + IdentifierOrTree(ApplicationIdentifier identifier) { + this.identifier = identifier; + } + IdentifierOrTree(ApplicationIdentifierTree tree) { + this.tree = tree; + } + + boolean isTree() { + return null != this.tree; + } + + ApplicationIdentifierTree getTree() { + return this.tree; + } + + ApplicationIdentifier getIdentifier() { + return this.identifier; + } + } + + IdentifierOrTree[] map = new IdentifierOrTree[] { + null, null, null, null, null, + null, null, null, null, null + }; + + ApplicationIdentifierTree() { + } + + void addLeaf(char[] key, int pos, ApplicationIdentifier identifier) { + char ch = key[pos]; + if (ch < '0' || ch > '9') + return; + int mapKey = (int)(ch - '0'); + if (pos == (key.length - 1)) { + this.map[mapKey] = new IdentifierOrTree(identifier); + } else { + if (null == this.map[mapKey]) { + this.map[mapKey] = new IdentifierOrTree(new ApplicationIdentifierTree()); + } + this.map[mapKey].getTree().addLeaf(key, pos+1, identifier); + } + } + + static ApplicationIdentifierTree buildTree() { + ApplicationIdentifierTree tree = new ApplicationIdentifierTree(); + for (ApplicationIdentifier item : ApplicationIdentifier.values()) { + tree.addLeaf(item.getKey().toCharArray(), 0, item); + } + return tree; + } + +} diff --git a/src/main/java/se/injoin/gs1utils/ElementStrings.java b/src/main/java/se/injoin/gs1utils/ElementStrings.java index a0aaf17..e043a9a 100644 --- a/src/main/java/se/injoin/gs1utils/ElementStrings.java +++ b/src/main/java/se/injoin/gs1utils/ElementStrings.java @@ -63,12 +63,12 @@ public BigDecimal getDecimal(String key) { return (BigDecimal) elementsByString.get(key); } - public List getList(ApplicationIdentifier identifier) { - return (List) elementsByEnum.get(identifier); + public List getList(ApplicationIdentifier identifier) { + return (List) elementsByEnum.get(identifier); } - public List getList(String key) { - return (List) elementsByString.get(key); + public List getList(String key) { + return (List) elementsByString.get(key); } public Object getObject(String key) { @@ -98,6 +98,11 @@ public Map getElementsByString() { public Map getElementsByEnum() { return elementsByEnum; } + + } + + public static ParseResult parse(String sequence) { + return parse(sequence, "\u001d"); } /** @@ -105,14 +110,18 @@ public Map getElementsByEnum() { * could be successfully parsed and an error string describing what went wrong and at what position in the sequence * the error occurred. The position reported is zero-based. */ - public static ParseResult parse(String sequence) { + public static ParseResult parse(String sequence, String separator) { if (sequence == null) { throw new NullPointerException("Sequence must not be null"); } + if (separator == null) { + separator = "\u001d"; + } + ParseResult result = new ParseResult(); - SequenceReader reader = new SequenceReader(sequence); + SequenceReader reader = new SequenceReader(sequence, separator); while (!(reader.remainingLength() == 0)) { @@ -122,21 +131,17 @@ public static ParseResult parse(String sequence) { ApplicationIdentifier identifier = null; try { - // Standard AIs defined in ApplicationIdentifier - for (ApplicationIdentifier candidate : ApplicationIdentifier.values()) { - if (reader.startsWith(candidate.getKey())) { - identifier = candidate; - key = reader.read(identifier.getKey().length()); - if (identifier.getFormat() == ApplicationIdentifier.Format.CUSTOM) { - data = readDataFieldInCustomFormat(identifier, reader); - } else { - data = readDataFieldInStandardFormat(identifier, reader); - } - break; + identifier = ApplicationIdentifierTree.get(reader.peek()); + if (identifier != null) { + key = reader.read(identifier.getKey().length()); + if (identifier.getFormat() == ApplicationIdentifier.Format.CUSTOM) { + data = readDataFieldInCustomFormat(identifier, reader); + } else { + data = readDataFieldInStandardFormat(identifier, reader); } } - + // Support for AIs 703s Number of processor with three-digit ISO country code if (key == null && reader.remainingLength() >= 4 && reader.startsWith("703")) { String start = reader.peek(4); @@ -154,16 +159,6 @@ public static ParseResult parse(String sequence) { data = reader.readVariableLengthAlphanumeric(1, 20); } } - - // Support for AIs 91-99 Company internal information - if (key == null && reader.remainingLength() >= 2 && reader.startsWith("9")) { - String start = reader.peek(2); - if (start.charAt(1) >= '1' && start.charAt(1) <= '9') { - key = reader.read(2); - data = reader.readVariableLengthAlphanumeric(1, 30); - } - } - } catch (Exception e) { result.partial = true; result.errorMessage = "Error parsing data field for AI " + key + " at position " + identifierPosition + ", " + e.getMessage(); @@ -202,8 +197,9 @@ private static Object readDataFieldInStandardFormat(ApplicationIdentifier identi return reader.readDecimal(identifier.getMinLength(), identifier.getMaxLength()); case DATE: return reader.readDate(); + default: + return null; } - return null; } private static Object readDataFieldInCustomFormat(ApplicationIdentifier identifier, SequenceReader reader) { @@ -225,19 +221,42 @@ private static Object readDataFieldInCustomFormat(ApplicationIdentifier identifi return reader.readDateOrDateRange(); case PRODUCTION_DATE_AND_TIME: return reader.readDateAndTimeWithOptionalMinutesAndSeconds(); + default: + return null; } - return null; } static class SequenceReader { - private static final char SEPARATOR_CHAR = 0x1D; + private char[] separator; private String sequence; private int position = 0; SequenceReader(String sequence) { + this(sequence, "\u001d"); + } + + SequenceReader(String sequence, String separator) { this.sequence = sequence; + this.separator = separator.toCharArray(); + } + + boolean isSeparator(String sequence, int index) { + try { + for (int i = 0; i < this.separator.length; i++) { + if (this.separator[i] != sequence.charAt(index + i)) + return false; + } + return true; + } catch (IndexOutOfBoundsException e) { + return false; + } + } + + void skipSeparatorIfPresent() { + if (isSeparator(sequence, position)) + position += this.separator.length; } String readFixedLengthNumeric(int length) { @@ -268,7 +287,7 @@ Date readDate() { } } - List readDateOrDateRange() { + List readDateOrDateRange() { String dataField = readNumericDataField(6, 12); if (dataField.length() == 6) { return Collections.singletonList(parseDateAndTime(dataField.substring(0, 6))); @@ -368,7 +387,7 @@ int readDecimalPointPosition() { return decimalPointIndicator - '0'; } - List readCurrencyAndAmount() { + List readCurrencyAndAmount() { int decimalPointPosition = readDecimalPointPosition(); String currencyCode = readNumericDataField(3, 3); String digits = readDataField(1, 15); @@ -376,7 +395,7 @@ List readCurrencyAndAmount() { return Arrays.asList(currencyCode, amount); } - List readCountryList() { + List readCountryList() { String dataField = readNumericDataField(3, 15); if (dataField.length() % 3 != 0) { throw new IllegalArgumentException("invalid data field length"); @@ -400,7 +419,7 @@ String readDataField(int minLength, int maxLength) { int length = 0; int endIndex = position; while (length < maxLength && endIndex < sequence.length()) { - if (sequence.charAt(endIndex) == SEPARATOR_CHAR) { + if (isSeparator(sequence, endIndex)) { break; } endIndex++; @@ -417,12 +436,6 @@ String readDataField(int minLength, int maxLength) { return dataField; } - void skipSeparatorIfPresent() { - if (position < sequence.length() && sequence.charAt(position) == SEPARATOR_CHAR) { - position++; - } - } - int remainingLength() { return sequence.length() - position; } @@ -448,5 +461,9 @@ String read(int length) { String peek(int length) { return sequence.substring(position, position + length); } + + String peek() { + return sequence.substring(position); + } } } diff --git a/src/test/java/se/injoin/gs1utils/ElementStringsTests.java b/src/test/java/se/injoin/gs1utils/ElementStringsTests.java index 27972d9..bbd6517 100644 --- a/src/test/java/se/injoin/gs1utils/ElementStringsTests.java +++ b/src/test/java/se/injoin/gs1utils/ElementStringsTests.java @@ -275,7 +275,7 @@ public void testParseDateRange() { ElementStrings.ParseResult result = ElementStrings.parse("7007170522170702"); assertFalse(result.isPartial()); assertFalse(result.isEmpty()); - List list = result.getList(ApplicationIdentifier.HARVEST_DATE); + List list = result.getList(ApplicationIdentifier.HARVEST_DATE); assertEquals(2, list.size()); assertDate("2017-05-22", (Date) list.get(0)); assertDate("2017-07-02", (Date) list.get(1)); @@ -286,7 +286,7 @@ public void testParseDateRangeWithSingleDate() { ElementStrings.ParseResult result = ElementStrings.parse("7007170522"); assertFalse(result.isPartial()); assertFalse(result.isEmpty()); - List list = result.getList(ApplicationIdentifier.HARVEST_DATE.getKey()); + List list = result.getList(ApplicationIdentifier.HARVEST_DATE.getKey()); assertEquals(1, list.size()); assertDate("2017-05-22", (Date) list.get(0)); } @@ -377,7 +377,7 @@ public void testParseAmountPayableWithCurrency() { ElementStrings.ParseResult result = ElementStrings.parse("391212345678"); assertFalse(result.isPartial()); assertFalse(result.isEmpty()); - List list = result.getList(ApplicationIdentifier.AMOUNT_PAYABLE_WITH_CURRENCY); + List list = result.getList(ApplicationIdentifier.AMOUNT_PAYABLE_WITH_CURRENCY); assertEquals(2, list.size()); assertEquals("123", list.get(0)); assertEquals(new BigDecimal("456.78"), list.get(1)); @@ -388,7 +388,7 @@ public void testParseAmountPayablePerSingleItemWithCurrency() { ElementStrings.ParseResult result = ElementStrings.parse("393212345678"); assertFalse(result.isPartial()); assertFalse(result.isEmpty()); - List list = result.getList(ApplicationIdentifier.AMOUNT_PAYABLE_PER_SINGLE_ITEM_WITH_CURRENCY); + List list = result.getList(ApplicationIdentifier.AMOUNT_PAYABLE_PER_SINGLE_ITEM_WITH_CURRENCY); assertEquals(2, list.size()); assertEquals("123", list.get(0)); assertEquals(new BigDecimal("456.78"), list.get(1)); @@ -399,7 +399,7 @@ public void testParseShipToPostalCodeWithCountry() { ElementStrings.ParseResult result = ElementStrings.parse("421123ABCDEF"); assertFalse(result.isPartial()); assertFalse(result.isEmpty()); - List list = result.getList(ApplicationIdentifier.SHIP_TO_POSTAL_CODE_WITH_COUNTRY); + List list = result.getList(ApplicationIdentifier.SHIP_TO_POSTAL_CODE_WITH_COUNTRY); assertEquals(2, list.size()); assertEquals("123", list.get(0)); assertEquals("ABCDEF", list.get(1)); @@ -410,7 +410,7 @@ public void testParseCountryOfInitialProcessing() { ElementStrings.ParseResult result = ElementStrings.parse("423111222333"); assertFalse(result.isPartial()); assertFalse(result.isEmpty()); - List list = result.getList(ApplicationIdentifier.COUNTRY_OF_INITIAL_PROCESSING); + List list = result.getList(ApplicationIdentifier.COUNTRY_OF_INITIAL_PROCESSING); assertEquals(3, list.size()); assertEquals("111", list.get(0)); assertEquals("222", list.get(1)); @@ -438,7 +438,7 @@ public void testParseCountryOfDisassembly() { ElementStrings.ParseResult result = ElementStrings.parse("425111222333"); assertFalse(result.isPartial()); assertFalse(result.isEmpty()); - List list = result.getList(ApplicationIdentifier.COUNTRY_OF_DISASSEMBLY); + List list = result.getList(ApplicationIdentifier.COUNTRY_OF_DISASSEMBLY); assertEquals(3, list.size()); assertEquals("111", list.get(0)); assertEquals("222", list.get(1)); @@ -450,7 +450,7 @@ public void testParseNumberOfProcessor() { ElementStrings.ParseResult result = ElementStrings.parse("7030111ABCDEF\u001d7039222XYZ"); assertFalse(result.isPartial()); assertFalse(result.isEmpty()); - List list = result.getList("7030"); + List list = result.getList("7030"); assertEquals(2, list.size()); assertEquals("111", list.get(0)); assertEquals("ABCDEF", list.get(1)); @@ -562,6 +562,17 @@ public void testParseRealWorldExample7() { assertEquals("17050838", result.getString(ApplicationIdentifier.BATCH_OR_LOT_NUMBER)); } + @Test + public void testParseCustomSeparator() { + ElementStrings.ParseResult result = ElementStrings.parse("011730020641446615170628131705081017050838;;91CustomInformation1;;92CustomInformation2", ";;"); + assertFalse(result.isPartial()); + assertFalse(result.isEmpty()); + assertTrue(result.contains("91")); + assertTrue(result.contains(ApplicationIdentifier.COMPANY_INTERNAL_INFORMATION_1)); + assertEquals("CustomInformation1", result.getString("91")); + assertEquals("CustomInformation2", result.getString(ApplicationIdentifier.COMPANY_INTERNAL_INFORMATION_2)); + } + @Test public void testResolveTwoDigitYear() { ElementStrings.SequenceReader r = new ElementStrings.SequenceReader("");