# gs1-core

Parse, validate and generate GS1 strings in JavaScript/TypeScript. Zero dependencies.

Supports the three ways GS1 data travels:

| Format | Example |
| --- | --- |
| Human-readable element string | `(01)09506000134352(17)261231(10)ABC123` |
| Raw barcode data (GS1-128, DataMatrix, QR) | `]d201095060001343521726123110ABC123` |
| GS1 Digital Link URI | `https://id.gs1.org/01/09506000134352/10/ABC123?17=261231` |

## Features

- Full AI dictionary: ~180 Application Identifiers from the GS1 General Specifications, including the decimal families (`310n`-`369n`, `390n`-`395n`)
- Validation of format specs, CSET 82 / CSET 39 character sets, dates, and check digits (GTIN, SSCC, GLN, GSIN, GSRN, GDTI, GCN, GRAI, ITIP)
- One `parse()` call handles element strings and raw scanner data, including symbology identifiers (`]C1`, `]d2`, `]Q3`, ...)
- Dates, weights and measures with implied decimal point, currency amounts and counts are returned as typed values, not strings
- GS1 Digital Link URIs with correct primary key and qualifier ordering
- ESM, fully typed, works in Node.js >= 18 and modern bundlers

## Installation

```sh
npm install gs1-core
```

## Quick start

```ts
import { parse } from 'gs1-core';

const elements = parse('(01)09506000134352(17)261231(3103)000189(10)ABC123');

for (const el of elements) {
  console.log(el.ai, el.definition.title, el.value, el.interpreted);
}
// 01    GTIN              09506000134352
// 17    USE BY OR EXPIRY  261231   { year: 2026, month: 12, day: 31, iso: '2026-12-31' }
// 3103  NET WEIGHT (kg)   000189   0.189
// 10    BATCH/LOT         ABC123
```

## Usage

### Parsing scanner output

Raw data uses ASCII GS (`\x1D`) as the FNC1 separator by default, and a leading symbology identifier is skipped automatically:

```ts
import { parseRawString, toRecord } from 'gs1-core';

const elements = parseRawString(']C101095060001343521726123110ABC123\x1D21SER99');

toRecord(elements);
// { '01': '09506000134352', '17': '261231', '10': 'ABC123', '21': 'SER99' }

// Some scanners are configured to substitute FNC1 with another character:
parseRawString('10LOT1|21SER1', { fnc1: '|' });
```

Parsing validates by default. Unknown AIs, wrong lengths, illegal characters, bad check digits and impossible dates all throw a `GS1Error` that tells you which AI failed. Pass `{ validate: false }` to parse structure only.

```ts
import { GS1Error, parse } from 'gs1-core';

try {
  parse('(01)09506000134353'); // wrong check digit
} catch (err) {
  if (err instanceof GS1Error) console.error(err.ai, err.message);
  // '01' 'AI (01): invalid check digit in "09506000134353" (position 14)'
}
```

### Generating strings

```ts
import { generateElementString, generateRawString } from 'gs1-core';

const items = [
  { ai: '01', value: '09506000134352' },
  { ai: '10', value: 'ABC123' },
  { ai: '17', value: '261231' },
];

generateElementString(items);
// '(01)09506000134352(10)ABC123(17)261231'

generateRawString(items);
// '010950600013435217261231' + '10ABC123'
```

`generateRawString` produces the content to encode after the leading FNC1 of a GS1-128 / GS1 DataMatrix / GS1 QR symbol. Predefined-length AIs are moved to the front so fewer separators are needed (disable with `{ optimize: false }`), and variable-length values are GS-terminated only when another element follows.

Values are validated before generation, so you cannot emit an invalid GS1 string. Check digit helpers are available if you need to compute one first:

```ts
import { withCheckDigit, computeCheckDigit, verifyCheckDigit } from 'gs1-core';

withCheckDigit('0950600013435');    // '09506000134352'
computeCheckDigit('0950600013435'); // 2
verifyCheckDigit('09506000134352'); // true
```

### GS1 Digital Link

```ts
import { generateDigitalLink, parseDigitalLink } from 'gs1-core';

generateDigitalLink(
  [
    { ai: '01', value: '09506000134352' },
    { ai: '10', value: 'ABC123' },
    { ai: '17', value: '261231' },
  ],
  { domain: 'https://id.example.com' }, // default: https://id.gs1.org
);
// 'https://id.example.com/01/09506000134352/10/ABC123?17=261231'

parseDigitalLink('https://id.gs1.org/01/4006381333931?17=261231');
// GTINs are normalised to 14 digits, resolver path prefixes are handled,
// and non-AI query parameters (linkType, ...) are ignored.
```

### AI dictionary

```ts
import { AI_DEFINITIONS, getAIDefinition } from 'gs1-core';

getAIDefinition('3103');
// {
//   ai: '310n', title: 'NET WEIGHT (kg)', spec: 'N6',
//   predefinedLength: true, fixedLength: 6, kind: 'decimal', ...
// }

for (const def of AI_DEFINITIONS.values()) {
  console.log(def.ai, def.spec, def.title);
}
```

## Interpreted values

`element.interpreted` carries a typed value when the AI has a known semantic:

| Kind | AIs (examples) | Type |
| --- | --- | --- |
| `date` | 11, 13, 15, 17, 7006, ... | `{ year, month, day, iso }`. `DD = 00` resolves to the last day of the month; 2-digit years follow the GS1 -49/+50 sliding window |
| `datetime` | 7003, 7011, 8008, 4324, 4325 | `{ year, ..., hour, minute?, second?, iso }` |
| `dateRange` | 7007 | `{ start, end? }` |
| `decimal` | 310n-369n, 390n, 392n, 394n, 395n | `number` (implied decimal point from the 4th AI digit) |
| `currencyDecimal` | 391n, 393n | `{ currency, amount }` (ISO 4217 numeric code) |
| `count` | 30, 37, 8111, ... | `number` |

## API

| Function | Description |
| --- | --- |
| `parse(input, options?)` | Parse any GS1 string, auto-detecting the format |
| `parseElementString(input, options?)` | Parse `(01)...(10)...` element strings |
| `parseRawString(input, options?)` | Parse raw barcode data (`fnc1` option for custom separators) |
| `parseDigitalLink(uri, options?)` | Parse a GS1 Digital Link URI |
| `generateElementString(input, options?)` | Build a human-readable element string |
| `generateRawString(input, options?)` | Build raw barcode content (`fnc1`, `optimize` options) |
| `generateDigitalLink(input, options?)` | Build a Digital Link URI (`domain` option) |
| `computeCheckDigit(digits)` / `verifyCheckDigit(digits)` / `withCheckDigit(digits)` | GS1 mod-10 check digit helpers |
| `getAIDefinition(ai)` / `AI_DEFINITIONS` | AI dictionary lookup / full table |
| `toRecord(elements)` | Convert parsed elements to `{ ai: value }` |
| `parseDate6`, `parseDateTime`, `resolveYear`, `interpretValue` | Low-level interpretation helpers |

Generator functions accept either an array of `{ ai, value }` pairs (order preserved) or a plain record `{ '01': '...', '10': '...' }`. Note that JavaScript iterates integer-like keys such as `'10'` before keys like `'01'`, so use the array form when order matters.

All errors are instances of `GS1Error`, which exposes the offending `ai` when known.

## Known limitations

- In human-readable element strings, a value that itself contains a `"(AI)"`-shaped sequence (parentheses are legal CSET 82 characters) is ambiguous and will be split. Use the raw format for such data; this ambiguity is inherent to the human-readable notation.
- Trailing components documented as mandatory by GS1 for a few AIs (e.g. the postal code of AI 421) are treated as optional.

## Contributing

Issues and pull requests are welcome.

```sh
git clone https://github.com/rmingon/gs1-core.git
cd gs1-core
npm install
npm test        # run the vitest suite
npm run build   # type-check and emit dist/
```

If you spot a missing or outdated Application Identifier, please open an issue with a reference to the relevant GS1 General Specifications section. The whole dictionary lives in [src/ai.ts](src/ai.ts).

## License

[MIT](./LICENSE)
