@aidc-toolkit/gs1 1.0.31-beta → 1.0.33-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +290 -17
  2. package/dist/character-set.d.ts +17 -2
  3. package/dist/character-set.d.ts.map +1 -1
  4. package/dist/character-set.js +44 -11
  5. package/dist/character-set.js.map +1 -1
  6. package/dist/gcp-length-cache.d.ts +81 -0
  7. package/dist/gcp-length-cache.d.ts.map +1 -0
  8. package/dist/gcp-length-cache.js +232 -0
  9. package/dist/gcp-length-cache.js.map +1 -0
  10. package/dist/gcp-length-data.d.ts +108 -0
  11. package/dist/gcp-length-data.d.ts.map +1 -0
  12. package/dist/gcp-length-data.js +53 -0
  13. package/dist/gcp-length-data.js.map +1 -0
  14. package/dist/gcp-length.d.ts +61 -0
  15. package/dist/gcp-length.d.ts.map +1 -0
  16. package/dist/gcp-length.js +300 -0
  17. package/dist/gcp-length.js.map +1 -0
  18. package/dist/gtin-creator.d.ts +0 -17
  19. package/dist/gtin-creator.d.ts.map +1 -1
  20. package/dist/gtin-creator.js +1 -93
  21. package/dist/gtin-creator.js.map +1 -1
  22. package/dist/gtin-validator.d.ts +1 -46
  23. package/dist/gtin-validator.d.ts.map +1 -1
  24. package/dist/gtin-validator.js +31 -125
  25. package/dist/gtin-validator.js.map +1 -1
  26. package/dist/identifier-validator.d.ts +1 -4
  27. package/dist/identifier-validator.d.ts.map +1 -1
  28. package/dist/identifier-validator.js +2 -5
  29. package/dist/identifier-validator.js.map +1 -1
  30. package/dist/index.d.ts +5 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +4 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/locale/en/locale-resources.d.ts +39 -35
  35. package/dist/locale/en/locale-resources.d.ts.map +1 -1
  36. package/dist/locale/en/locale-resources.js +6 -2
  37. package/dist/locale/en/locale-resources.js.map +1 -1
  38. package/dist/locale/fr/locale-resources.d.ts +39 -35
  39. package/dist/locale/fr/locale-resources.d.ts.map +1 -1
  40. package/dist/locale/fr/locale-resources.js +6 -2
  41. package/dist/locale/fr/locale-resources.js.map +1 -1
  42. package/dist/locale/i18n.d.ts +4 -4
  43. package/dist/locale/i18n.d.ts.map +1 -1
  44. package/dist/locale/i18n.js +6 -7
  45. package/dist/locale/i18n.js.map +1 -1
  46. package/dist/non-numeric-identifier-validator.js +1 -1
  47. package/dist/non-numeric-identifier-validator.js.map +1 -1
  48. package/dist/numeric-identifier-validator.d.ts.map +1 -1
  49. package/dist/numeric-identifier-validator.js +2 -2
  50. package/dist/numeric-identifier-validator.js.map +1 -1
  51. package/dist/prefix-manager.d.ts +35 -0
  52. package/dist/prefix-manager.d.ts.map +1 -1
  53. package/dist/prefix-manager.js +56 -0
  54. package/dist/prefix-manager.js.map +1 -1
  55. package/dist/prefix-validator.d.ts +5 -4
  56. package/dist/prefix-validator.d.ts.map +1 -1
  57. package/dist/prefix-validator.js +18 -22
  58. package/dist/prefix-validator.js.map +1 -1
  59. package/dist/serializable-numeric-identifier-validator.d.ts +26 -0
  60. package/dist/serializable-numeric-identifier-validator.d.ts.map +1 -1
  61. package/dist/serializable-numeric-identifier-validator.js +19 -0
  62. package/dist/serializable-numeric-identifier-validator.js.map +1 -1
  63. package/dist/variable-measure.d.ts +68 -0
  64. package/dist/variable-measure.d.ts.map +1 -0
  65. package/dist/variable-measure.js +210 -0
  66. package/dist/variable-measure.js.map +1 -0
  67. package/dist/verified-by-gs1.d.ts +22 -0
  68. package/dist/verified-by-gs1.d.ts.map +1 -0
  69. package/dist/verified-by-gs1.js +46 -0
  70. package/dist/verified-by-gs1.js.map +1 -0
  71. package/package.json +12 -11
  72. package/src/character-set.ts +55 -11
  73. package/src/gcp-length-cache.ts +271 -0
  74. package/src/gcp-length-data.ts +136 -0
  75. package/src/gcp-length.ts +380 -0
  76. package/src/gtin-creator.ts +1 -117
  77. package/src/gtin-validator.ts +42 -173
  78. package/src/identifier-validator.ts +2 -5
  79. package/src/index.ts +7 -1
  80. package/src/locale/en/locale-resources.ts +7 -3
  81. package/src/locale/fr/locale-resources.ts +7 -3
  82. package/src/locale/i18n.ts +7 -8
  83. package/src/locale/i18next.d.ts +2 -0
  84. package/src/non-numeric-identifier-validator.ts +1 -1
  85. package/src/numeric-identifier-validator.ts +3 -3
  86. package/src/prefix-manager.ts +65 -0
  87. package/src/prefix-validator.ts +19 -23
  88. package/src/serializable-numeric-identifier-validator.ts +36 -0
  89. package/src/variable-measure.ts +268 -0
  90. package/src/verified-by-gs1.ts +54 -0
  91. package/test/character-set.test.ts +46 -0
  92. package/test/creator.test.ts +5 -5
  93. package/test/data/gcpprefixformatlist-1.json +662625 -0
  94. package/test/data/gcpprefixformatlist-2.json +735431 -0
  95. package/test/gcp-length.test.ts +344 -0
  96. package/test/gtin-creator.ts +4 -4
  97. package/test/gtin-validator.test.ts +205 -113
  98. package/test/gtin-validator.ts +30 -0
  99. package/test/identifier-creator.ts +6 -6
  100. package/test/non-numeric-identifier-creator.ts +0 -8
  101. package/test/non-serializable-numeric-identifier-creator.ts +4 -54
  102. package/test/numeric-identifier-creator.ts +4 -4
  103. package/test/prefix-manager.test.ts +5 -5
  104. package/test/serializable-numeric-identifier-creator.ts +32 -19
  105. package/test/validator.test.ts +6 -6
  106. package/test/variable-measure-rcn.test.ts +63 -68
  107. package/test/verified-by-gs1.test.ts +55 -0
  108. package/tsconfig-src.json +7 -1
  109. package/tsconfig-src.tsbuildinfo +1 -0
  110. package/tsconfig-tsup.json +7 -0
@@ -2,6 +2,7 @@ import { type CharacterSetValidation, NUMERIC_CREATOR } from "@aidc-toolkit/util
2
2
  import { i18nextGS1 } from "./locale/i18n.js";
3
3
  import type { PrefixProvider } from "./prefix-provider.js";
4
4
  import { type PrefixType, PrefixTypes } from "./prefix-type.js";
5
+ import type { ParseKeys } from "i18next";
5
6
 
6
7
  /**
7
8
  * Prefix validation parameters.
@@ -36,7 +37,7 @@ const GS1_COMPANY_PREFIX_MAXIMUM_LENGTH = 12;
36
37
  /**
37
38
  * U.P.C. Company Prefix minimum length.
38
39
  */
39
- const UPC_COMPANY_PREFIX_MINIMUM_LENGTH = 6;
40
+ const UPC_COMPANY_PREFIX_MINIMUM_LENGTH = 5;
40
41
 
41
42
  /**
42
43
  * U.P.C. Company Prefix maximum length.
@@ -130,31 +131,31 @@ export const PrefixValidator = {
130
131
  * @param isNumericIdentifier
131
132
  * If true, the prefix is from a numeric identifier and its character set will be validated by the caller.
132
133
  *
133
- * @param positionOffset
134
- * Position offset within a larger string.
134
+ * @param errorMessageParseKey
135
+ * Parse key for error message when validating GS1 Company Prefix type.
135
136
  */
136
- validate(prefixType: PrefixType, allowUPCCompanyPrefix: boolean, allowGS18Prefix: boolean, prefix: string, isFromIdentifier = false, isNumericIdentifier = false, positionOffset?: number): void {
137
- let baseValidation: PrefixValidation;
137
+ validate(prefixType: PrefixType, allowUPCCompanyPrefix: boolean, allowGS18Prefix: boolean, prefix: string, isFromIdentifier = false, isNumericIdentifier = false, errorMessageParseKey?: ParseKeys): void {
138
+ let validation: PrefixValidation;
138
139
 
139
140
  // Validate the prefix type and determine the prefix validation parameters.
140
141
  switch (prefixType) {
141
142
  case PrefixTypes.GS1CompanyPrefix:
142
143
  if (!prefix.startsWith("0")) {
143
- baseValidation = GS1_COMPANY_PREFIX_VALIDATION;
144
+ validation = GS1_COMPANY_PREFIX_VALIDATION;
144
145
  } else if (!prefix.startsWith("00000")) {
145
146
  if (!allowUPCCompanyPrefix) {
146
- throw new RangeError(i18nextGS1.t("Prefix.gs1CompanyPrefixCantStartWith0"));
147
+ throw new RangeError(i18nextGS1.t(errorMessageParseKey ?? "Prefix.gs1CompanyPrefixCantStartWith0"));
147
148
  }
148
149
 
149
- baseValidation = UPC_COMPANY_PREFIX_AS_GS1_COMPANY_PREFIX_VALIDATION;
150
+ validation = UPC_COMPANY_PREFIX_AS_GS1_COMPANY_PREFIX_VALIDATION;
150
151
  } else if (!prefix.startsWith("000000")) {
151
152
  if (!allowGS18Prefix) {
152
- throw new RangeError(i18nextGS1.t("Prefix.gs1CompanyPrefixCantStartWith00000"));
153
+ throw new RangeError(i18nextGS1.t(errorMessageParseKey ?? "Prefix.gs1CompanyPrefixCantStartWith00000"));
153
154
  }
154
155
 
155
- baseValidation = GS1_8_PREFIX_AS_GS1_COMPANY_PREFIX_VALIDATION;
156
+ validation = GS1_8_PREFIX_AS_GS1_COMPANY_PREFIX_VALIDATION;
156
157
  } else {
157
- throw new RangeError(i18nextGS1.t("Prefix.gs1CompanyPrefixCantStartWith000000"));
158
+ throw new RangeError(i18nextGS1.t(errorMessageParseKey ?? "Prefix.gs1CompanyPrefixCantStartWith000000"));
158
159
  }
159
160
  break;
160
161
 
@@ -163,7 +164,7 @@ export const PrefixValidator = {
163
164
  throw new RangeError(i18nextGS1.t("Prefix.upcCompanyPrefixCantStartWith0000"));
164
165
  }
165
166
 
166
- baseValidation = UPC_COMPANY_PREFIX_VALIDATION;
167
+ validation = UPC_COMPANY_PREFIX_VALIDATION;
167
168
  break;
168
169
 
169
170
  case PrefixTypes.GS18Prefix:
@@ -171,21 +172,16 @@ export const PrefixValidator = {
171
172
  throw new RangeError(i18nextGS1.t("Prefix.gs18PrefixCantStartWith0"));
172
173
  }
173
174
 
174
- baseValidation = GS1_8_PREFIX_VALIDATION;
175
+ validation = GS1_8_PREFIX_VALIDATION;
175
176
  break;
176
177
  }
177
178
 
178
- const mergedValidation: PrefixValidation = {
179
- ...baseValidation,
180
- positionOffset
181
- };
182
-
183
- // If from key and numeric, key validation will take care of character set validation.
179
+ // If from identifier and numeric, identifier validation will take care of character set validation.
184
180
  if (!isFromIdentifier) {
185
- NUMERIC_CREATOR.validate(prefix, mergedValidation);
181
+ NUMERIC_CREATOR.validate(prefix, validation);
186
182
  } else if (!isNumericIdentifier) {
187
183
  // Validate only the minimum length, allowing at least one character for the (possibly non-numeric) reference.
188
- NUMERIC_CREATOR.validate(prefix.substring(0, Math.min(mergedValidation.minimumLength, prefix.length - 1)), mergedValidation);
184
+ NUMERIC_CREATOR.validate(prefix.substring(0, Math.min(validation.minimumLength, prefix.length - 1)), validation);
189
185
  }
190
186
  },
191
187
 
@@ -214,11 +210,11 @@ export const PrefixValidator = {
214
210
  break;
215
211
 
216
212
  case PrefixTypes.UPCCompanyPrefix:
217
- gs1CompanyPrefix = "0" + prefix;
213
+ gs1CompanyPrefix = `0${prefix}`;
218
214
  break;
219
215
 
220
216
  case PrefixTypes.GS18Prefix:
221
- gs1CompanyPrefix = "00000" + prefix;
217
+ gs1CompanyPrefix = `00000${prefix}`;
222
218
  break;
223
219
  }
224
220
 
@@ -8,6 +8,21 @@ import { NonGTINNumericIdentifierValidator } from "./non-gtin-numeric-identifier
8
8
  import type { SerializableNumericIdentifierDescriptor } from "./serializable-numeric-identifier-descriptor.js";
9
9
  import type { SerializableNumericIdentifierType } from "./serializable-numeric-identifier-type.js";
10
10
 
11
+ /**
12
+ * Serializable numeric identifier split.
13
+ */
14
+ export interface SerializableNumericIdentifierSplit {
15
+ /**
16
+ * Base identifier.
17
+ */
18
+ baseIdentifier: string;
19
+
20
+ /**
21
+ * Serial component.
22
+ */
23
+ serialComponent: string;
24
+ }
25
+
11
26
  /**
12
27
  * Serializable numeric identifier validator. Validates both serialized and non-serialized forms of numeric identifiers
13
28
  * that support serialization.
@@ -94,4 +109,25 @@ export class SerializableNumericIdentifierValidator extends NonGTINNumericIdenti
94
109
  this.serialComponentCreator.validate(identifier.substring(this.length), this.#serialComponentValidation);
95
110
  }
96
111
  }
112
+
113
+ /**
114
+ * Split a serializable numeric identifier into its component parts.
115
+ *
116
+ * @param identifier
117
+ * Identifier.
118
+ *
119
+ * @param validation
120
+ * Identifier validation parameters.
121
+ *
122
+ * @returns
123
+ * Base identifier and serial component (possibly empty) as object.
124
+ */
125
+ split(identifier: string, validation?: IdentifierValidation): SerializableNumericIdentifierSplit {
126
+ this.validate(identifier, validation);
127
+
128
+ return {
129
+ baseIdentifier: identifier.substring(0, this.length),
130
+ serialComponent: identifier.substring(this.length)
131
+ };
132
+ }
97
133
  }
@@ -0,0 +1,268 @@
1
+ import { NUMERIC_CREATOR } from "@aidc-toolkit/utility";
2
+ import { checkDigit, hasValidCheckDigit, isValidPriceOrWeightCheckDigit, priceOrWeightCheckDigit } from "./check.js";
3
+ import { i18nextGS1 } from "./locale/i18n.js";
4
+
5
+ /**
6
+ * Restricted Circulation Number reference.
7
+ */
8
+ export interface RCNReference {
9
+ /**
10
+ * Item reference.
11
+ */
12
+ itemReference: number;
13
+
14
+ /**
15
+ * Price or weight (whole number only).
16
+ */
17
+ priceOrWeight: number;
18
+ }
19
+
20
+ /**
21
+ * Variable measure trade item support functions.
22
+ */
23
+ // eslint-disable-next-line @typescript-eslint/no-extraneous-class -- Wrapper for future functionality.
24
+ export class VariableMeasure {
25
+ /**
26
+ * Parse a Restricted Circulation Number (RCN) using a variable measure trade item format. The format is a 12- or
27
+ * 13-character string (for RCN-12 or RCN-13 respectively), containing the following:
28
+ *
29
+ * - '2' - The first character of the RCN.
30
+ * - '0'-'9' - The second character of the RCN (RCN-13 only).
31
+ * - 'I' - One or more, in sequence, for the item reference.
32
+ * - 'P' - One or more, in sequence, for the price or weight.
33
+ * - 'V' - Zero or one, for the price or weight check digit.
34
+ * - 'C' - The check digit of the entire RCN.
35
+ *
36
+ * The 'I', 'P', and 'V' formats may be in any order.
37
+ *
38
+ * Some examples:
39
+ *
40
+ * - `2IIIIIVPPPPC` - RCN-12 with a five-digit item reference, a price or weight check digit, and a four-digit price
41
+ * or weight.
42
+ * - `23IIIIVPPPPPC` - RCN-13 with a four-digit item reference, a price or weight check digit, and a five-digit price
43
+ * or weight.
44
+ * - `2IIIIIIPPPPC` - RCN-12 with a six-digit item reference and a four-digit price or eight.
45
+ * - `29IIIIIPPPPPC` - RCN-13 with a five-digit item reference and a five-digit price or weight.
46
+ *
47
+ * @param format
48
+ * Format.
49
+ *
50
+ * @param rcn
51
+ * RCN.
52
+ *
53
+ * @returns
54
+ * RCN reference.
55
+ */
56
+ static parseRCN(format: string, rcn: string): RCNReference {
57
+ const formatLength = format.length;
58
+
59
+ if (rcn.length !== formatLength) {
60
+ throw new RangeError(i18nextGS1.t("Identifier.invalidRCNLength"));
61
+ }
62
+
63
+ let validFormat = formatLength === 12 || formatLength === 13;
64
+ let validRCNPrefix = true;
65
+
66
+ let buildingItemReference = false;
67
+ let itemReference = "";
68
+
69
+ let buildingPriceOrWeight = false;
70
+ let priceOrWeight = "";
71
+
72
+ let priceOrWeightCheckDigit = "";
73
+
74
+ for (let index = 0; validFormat && index < formatLength; index++) {
75
+ const formatChar = format.charAt(index);
76
+ const rcnChar = rcn.charAt(index);
77
+
78
+ if (index === 0) {
79
+ validFormat = formatChar === "2";
80
+ validRCNPrefix = rcnChar === "2";
81
+ } else if (formatLength === 13 && index === 1) {
82
+ validFormat = NUMERIC_CREATOR.characterIndex(formatChar) !== undefined;
83
+ validRCNPrefix = rcnChar === formatChar;
84
+ } else if (index === formatLength - 1) {
85
+ validFormat = formatChar === "C";
86
+ } else {
87
+ switch (formatChar) {
88
+ case "I":
89
+ if (!buildingItemReference) {
90
+ // Item reference can't appear more than once.
91
+ validFormat = itemReference === "";
92
+
93
+ buildingItemReference = true;
94
+ buildingPriceOrWeight = false;
95
+ }
96
+
97
+ itemReference += rcnChar;
98
+ break;
99
+
100
+ case "P":
101
+ if (!buildingPriceOrWeight) {
102
+ // Price or weight can't appear more than once.
103
+ validFormat = priceOrWeight === "";
104
+
105
+ buildingPriceOrWeight = true;
106
+ buildingItemReference = false;
107
+ }
108
+
109
+ priceOrWeight += rcnChar;
110
+ break;
111
+
112
+ case "V":
113
+ // Price or weight check digit can't appear more than once.
114
+ validFormat = priceOrWeightCheckDigit === "";
115
+
116
+ buildingItemReference = false;
117
+ buildingPriceOrWeight = false;
118
+
119
+ priceOrWeightCheckDigit = rcnChar;
120
+ break;
121
+
122
+ default:
123
+ validFormat = false;
124
+ break;
125
+ }
126
+ }
127
+ }
128
+
129
+ validFormat &&= itemReference !== "" && priceOrWeight !== "";
130
+
131
+ if (!validFormat) {
132
+ throw new RangeError(i18nextGS1.t("Identifier.invalidVariableMeasureRCNFormat"));
133
+ }
134
+
135
+ if (!validRCNPrefix) {
136
+ throw new RangeError(i18nextGS1.t("Identifier.invalidVariableMeasureRCNPrefix"));
137
+ }
138
+
139
+ if (priceOrWeightCheckDigit !== "" && !isValidPriceOrWeightCheckDigit(priceOrWeight, priceOrWeightCheckDigit)) {
140
+ throw new RangeError(i18nextGS1.t("Identifier.invalidVariableMeasurePriceOrWeight"));
141
+ }
142
+
143
+ if (!hasValidCheckDigit(rcn)) {
144
+ throw new RangeError(i18nextGS1.t("Identifier.invalidCheckDigit"));
145
+ }
146
+
147
+ return {
148
+ itemReference: Number(itemReference),
149
+ priceOrWeight: Number(priceOrWeight)
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Create a Restricted Circulation Number (RCN) using a variable measure trade item format. See {@linkcode parseRCN}
155
+ * for format details.
156
+ *
157
+ * @param format
158
+ * Format.
159
+ *
160
+ * @param itemReference
161
+ * Item reference.
162
+ *
163
+ * @param priceOrWeight
164
+ * Price or weight (whole number only).
165
+ *
166
+ * @returns
167
+ * RCN-12 or RCN-13.
168
+ */
169
+ static createRCN(format: string, itemReference: number, priceOrWeight: number): string {
170
+ const formatLength = format.length;
171
+
172
+ let validFormat = formatLength === 12 || formatLength === 13;
173
+
174
+ let rcnPrefix = "";
175
+
176
+ let buildingItemReference = false;
177
+ let itemReferenceString = "";
178
+ let itemReferenceLength = 0;
179
+
180
+ let buildingPriceOrWeight = false;
181
+ let priceOrWeightString = "";
182
+ let priceOrWeightLength = 0;
183
+
184
+ let calculatePriceOrWeightCheckDigit = false;
185
+
186
+ // RCN may be built in almost any order, so defer to builders that will be in ordered array.
187
+ const rcnPrefixBuilder = (partialRCN: string): string => partialRCN + rcnPrefix;
188
+ const itemReferenceBuilder = (partialRCN: string): string => partialRCN + itemReferenceString;
189
+ const priceOrWeightBuilder = (partialRCN: string): string => partialRCN + priceOrWeightString;
190
+ const priceOrWeightCheckDigitBuilder = (partialRCN: string): string => partialRCN + priceOrWeightCheckDigit(priceOrWeightString);
191
+ const checkDigitBuilder = (partialRCN: string): string => partialRCN + checkDigit(partialRCN);
192
+
193
+ const rcnBuilders = [rcnPrefixBuilder];
194
+
195
+ for (let index = 0; validFormat && index < formatLength; index++) {
196
+ const formatChar = format.charAt(index);
197
+
198
+ if (index === 0) {
199
+ validFormat = formatChar === "2";
200
+ rcnPrefix = formatChar;
201
+ } else if (formatLength === 13 && index === 1) {
202
+ validFormat = NUMERIC_CREATOR.characterIndex(formatChar) !== undefined;
203
+ rcnPrefix += formatChar;
204
+ } else if (index === formatLength - 1) {
205
+ validFormat = formatChar === "C";
206
+ } else {
207
+ switch (formatChar) {
208
+ case "I":
209
+ if (!buildingItemReference) {
210
+ // Item reference can't appear more than once.
211
+ validFormat = itemReferenceLength === 0;
212
+
213
+ buildingItemReference = true;
214
+ buildingPriceOrWeight = false;
215
+
216
+ rcnBuilders.push(itemReferenceBuilder);
217
+ }
218
+
219
+ itemReferenceLength++;
220
+ break;
221
+
222
+ case "P":
223
+ if (!buildingPriceOrWeight) {
224
+ // Price or weight can't appear more than once.
225
+ validFormat = priceOrWeightLength === 0;
226
+
227
+ buildingPriceOrWeight = true;
228
+ buildingItemReference = false;
229
+
230
+ rcnBuilders.push(priceOrWeightBuilder);
231
+ }
232
+
233
+ priceOrWeightLength++;
234
+ break;
235
+
236
+ case "V":
237
+ // Price or weight check digit can't appear more than once.
238
+ validFormat = !calculatePriceOrWeightCheckDigit;
239
+
240
+ buildingItemReference = false;
241
+ buildingPriceOrWeight = false;
242
+
243
+ calculatePriceOrWeightCheckDigit = true;
244
+
245
+ rcnBuilders.push(priceOrWeightCheckDigitBuilder);
246
+ break;
247
+
248
+ default:
249
+ validFormat = false;
250
+ break;
251
+ }
252
+ }
253
+ }
254
+
255
+ validFormat &&= itemReferenceLength !== 0 && priceOrWeightLength !== 0;
256
+
257
+ if (!validFormat) {
258
+ throw new RangeError(i18nextGS1.t("Identifier.invalidVariableMeasureRCNFormat"));
259
+ }
260
+
261
+ itemReferenceString = NUMERIC_CREATOR.create(itemReferenceLength, itemReference);
262
+ priceOrWeightString = NUMERIC_CREATOR.create(priceOrWeightLength, priceOrWeight);
263
+
264
+ rcnBuilders.push(checkDigitBuilder);
265
+
266
+ return rcnBuilders.reduce((partialRCN, rcnBuilder) => rcnBuilder(partialRCN), "");
267
+ }
268
+ }
@@ -0,0 +1,54 @@
1
+ import type { Hyperlink } from "@aidc-toolkit/core";
2
+ import { GTINValidator } from "./gtin-validator.js";
3
+ import { type IdentifierType, IdentifierTypes } from "./identifier-type.js";
4
+ import { IdentifierValidators } from "./identifier-validators.js";
5
+
6
+ const VERIFIED_BY_GS1_REFERENCE_BASE = "https://www.gs1.org/services/verified-by-gs1/results?";
7
+
8
+ /**
9
+ * Create a Verified by GS1 hyperlink.
10
+ *
11
+ * @param identifierType
12
+ * Identifier type.
13
+ *
14
+ * @param identifier
15
+ * Identifier.
16
+ *
17
+ * @param text
18
+ * Text for hyperlink. If not provided, the identifier is used.
19
+ *
20
+ * @param details
21
+ * Details to display when hovering over hyperlink.
22
+ *
23
+ * @returns
24
+ * Verified by GS1 hyperlink.
25
+ */
26
+ // eslint-disable-next-line @typescript-eslint/no-useless-default-assignment -- Undefined is necessary to allow bypass of text.
27
+ export function verifiedByGS1(identifierType: IdentifierType, identifier: string, text: string | undefined = undefined, details?: string): Hyperlink {
28
+ let normalizedIdentifier: string;
29
+ let useKeyTypeParameter: boolean;
30
+
31
+ if (identifierType === IdentifierTypes.GTIN) {
32
+ // Normalization will validate resulting GTIN.
33
+ normalizedIdentifier = GTINValidator.normalize(identifier);
34
+
35
+ useKeyTypeParameter = true;
36
+ } else {
37
+ const identifierValidator = IdentifierValidators[identifierType];
38
+
39
+ identifierValidator.validate(identifier);
40
+
41
+ normalizedIdentifier = identifier;
42
+ useKeyTypeParameter = identifierType === IdentifierTypes.GLN;
43
+ }
44
+
45
+ const lowerCaseIdentifierType = identifierType.toLowerCase();
46
+
47
+ const reference = useKeyTypeParameter ? `${VERIFIED_BY_GS1_REFERENCE_BASE}${lowerCaseIdentifierType}=${normalizedIdentifier}` : `${VERIFIED_BY_GS1_REFERENCE_BASE}key=${normalizedIdentifier}&key_type=${lowerCaseIdentifierType}`;
48
+
49
+ return {
50
+ reference,
51
+ text: text ?? identifier,
52
+ details
53
+ };
54
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { AI64_VALIDATOR } from "../src/index.js";
3
+
4
+ describe("AI 64 character set validator", () => {
5
+ test("Character set", () => {
6
+ expect(() => {
7
+ AI64_VALIDATOR.validate("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz");
8
+ }).not.toThrow(RangeError);
9
+
10
+ expect(() => {
11
+ AI64_VALIDATOR.validate("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxy");
12
+ }).toThrow("Length 63 must be a multiple of 4");
13
+
14
+ expect(() => {
15
+ AI64_VALIDATOR.validate("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwx");
16
+ }).toThrow("Length 62 must be a multiple of 4");
17
+
18
+ expect(() => {
19
+ AI64_VALIDATOR.validate("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvw");
20
+ }).toThrow("Length 61 must be a multiple of 4");
21
+
22
+ expect(() => {
23
+ AI64_VALIDATOR.validate("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyzABC=");
24
+ }).not.toThrow(RangeError);
25
+
26
+ expect(() => {
27
+ AI64_VALIDATOR.validate("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyzAB==");
28
+ }).not.toThrow(RangeError);
29
+
30
+ expect(() => {
31
+ AI64_VALIDATOR.validate("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyzA===");
32
+ }).toThrow("Invalid character '=' at position 65");
33
+
34
+ expect(() => {
35
+ AI64_VALIDATOR.validate("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz====");
36
+ }).toThrow("Invalid character '=' at position 64");
37
+
38
+ expect(() => {
39
+ AI64_VALIDATOR.validate("=-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxy");
40
+ }).toThrow("Invalid character '=' at position 0");
41
+
42
+ expect(() => {
43
+ AI64_VALIDATOR.validate("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ=_abcdefghijklmnopqrstuvwxy");
44
+ }).toThrow("Invalid character '=' at position 37");
45
+ });
46
+ });
@@ -1,7 +1,7 @@
1
1
  import { describe } from "vitest";
2
2
  import { PrefixManager, PrefixTypes } from "../src/index.js";
3
3
  import { testGTINCreator } from "./gtin-creator.js";
4
- import { testNonGTINNumericIdentifierCreator } from "./non-gtin-numeric-identifier-creator.js";
4
+ import { testNonSerializableNumericIdentifierCreator } from "./non-serializable-numeric-identifier-creator.js";
5
5
  import { testNonNumericIdentifierCreator } from "./non-numeric-identifier-creator.js";
6
6
  import { testSerializableNumericIdentifierCreator } from "./serializable-numeric-identifier-creator.js";
7
7
 
@@ -14,14 +14,14 @@ describe("Creators", () => {
14
14
  const shortPrefixManager = PrefixManager.get(PrefixTypes.GS1CompanyPrefix, "952123456");
15
15
  const longPrefixManager = PrefixManager.get(PrefixTypes.GS1CompanyPrefix, "952123456789");
16
16
 
17
- testNonGTINNumericIdentifierCreator(shortPrefixManager.glnCreator);
18
- testNonGTINNumericIdentifierCreator(longPrefixManager.ssccCreator);
17
+ testNonSerializableNumericIdentifierCreator(shortPrefixManager.glnCreator);
18
+ testNonSerializableNumericIdentifierCreator(longPrefixManager.ssccCreator);
19
19
  testSerializableNumericIdentifierCreator(shortPrefixManager.graiCreator);
20
20
  testNonNumericIdentifierCreator(shortPrefixManager.giaiCreator);
21
- testNonGTINNumericIdentifierCreator(longPrefixManager.gsrnCreator);
21
+ testNonSerializableNumericIdentifierCreator(longPrefixManager.gsrnCreator);
22
22
  testSerializableNumericIdentifierCreator(shortPrefixManager.gdtiCreator);
23
23
  testNonNumericIdentifierCreator(shortPrefixManager.gincCreator);
24
- testNonGTINNumericIdentifierCreator(longPrefixManager.gsinCreator);
24
+ testNonSerializableNumericIdentifierCreator(longPrefixManager.gsinCreator);
25
25
  testSerializableNumericIdentifierCreator(shortPrefixManager.gcnCreator);
26
26
  testNonNumericIdentifierCreator(shortPrefixManager.cpidCreator);
27
27
  testNonNumericIdentifierCreator(shortPrefixManager.gmnCreator);