validation.gs1-check-digit
Compute or check the GS1 mod-10 check digit of any GS1 key: GTIN, SSCC, GLN, GSRN, GDTI, GSIN and the rest.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 30 tests, run in TypeScript, Python and Rust.gs1CheckDigit 15 · isGs1CheckDigitValid 15
What it does
The GS1 mod-10 check digit, on its own, for every GS1 identification key that ends in one: GTIN-8, GTIN-12 (UPC-A), GTIN-13 (EAN-13), GTIN-14, SSCC-18, GLN-13, GSRN-18, GSIN-17, the 13-digit base of a GDTI, GRAI or GCN, and so on. It is the arithmetic those keys share, for the capabilities that know what each key means (`validation.gtin`, `logistics.sscc`).
- `gs1CheckDigit(data)` returns the digit (0-9) to append to `data`, the key's digits without its check digit. - `isGs1CheckDigitValid(key)` says whether the last digit of a whole key is the check digit of the digits before it.
The functions
A group: 2 functions that work together, each in its own file, each pinned by its own tests in TypeScript, Python and Rust. A project can install only the ones it calls.
- gs1CheckDigit (data: string) -> int
- isGs1CheckDigitValid (key: string) -> bool
Once installed, your code imports each one from the group's module.
gs1CheckDigit throws on bad input 15 tests
export function gs1CheckDigit(data: string): number
| data | string | the key's digits without its check digit, any length from 1; ASCII digits only, no spaces |
| returns | int | 0 to 9, the digit to append |
For example
gs1CheckDigit(629104150021)→ 3 GS1's worked example: GTIN-13 629104150021 takes check digit 3gs1CheckDigit(37610425002123456)→ 9 GS1's worked example: SSCC 37610425002123456 takes check digit 9gs1CheckDigit(10614141123456789)→ 7 GS1's SSCC label example (00) 1 0614141 123456789 7
import { gs1CheckDigit } from "#fune/validation.gs1-check-digit@^1";
/**
* The GS1 mod-10 check digit for `data`, the digits of a GS1 key without its
* check digit. One rule serves every key length because the weights are
* counted from the right: 3, 1, 3, 1 ... starting with the digit that will sit
* next to the check digit. Counting from the left instead only works for
* EAN-13 and the other even-length data strings.
*/
export function gs1CheckDigit(data: string): number {
if (typeof data !== "string" || !/^[0-9]+$/.test(data)) {
throw new RangeError(`GS1 data must be one or more ASCII digits, received ${JSON.stringify(data)}`);
}
let total = 0;
let weight = 3;
for (let i = data.length - 1; i >= 0; i--) {
// Reducing as we go keeps the sum small for data of any length.
total = (total + (data.charCodeAt(i) - 48) * weight) % 10;
weight = weight === 3 ? 1 : 3;
}
return (10 - total) % 10;
}isGs1CheckDigitValid 15 tests
export function isGs1CheckDigitValid(key: string): boolean
| key | string | a whole key, check digit last; ASCII digits only, at least 2 |
| returns | bool | false for anything that is not two or more ASCII digits, rather than an error |
For example
isGs1CheckDigitValid(6291041500213)→ true GS1's worked example GTIN-13 6291041500213isGs1CheckDigitValid(376104250021234569)→ true GS1's worked example SSCC 376104250021234569isGs1CheckDigitValid(6291041500214)→ false the GTIN-13 example with its check digit one off
import { isGs1CheckDigitValid } from "#fune/validation.gs1-check-digit@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { gs1CheckDigit } from "./validation_gs1_check_digit_gs1_check_digit.ts"; ← gs1CheckDigit, another function of this group · built into the same file, even by a slim install
/**
* Whether the last digit of `key` is the GS1 check digit of the digits before
* it. Only the arithmetic: the length, the prefix and an all-zero key are the
* caller's to judge. Validators answer rather than throw, so anything that is
* not two or more ASCII digits is false.
*/
export function isGs1CheckDigitValid(key: string): boolean {
if (typeof key !== "string" || !/^[0-9]{2,}$/.test(key)) return false;
return gs1CheckDigit(key.slice(0, -1)) === key.charCodeAt(key.length - 1) - 48;
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add validation.gs1-check-digit
That builds the whole group. To build only what you call, and whatever it uses inside the group:
fune add validation.gs1-check-digit --only gs1CheckDigit
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./validation.gs1-check-digit-1.0.0-typescript.fune, or fetch it from a terminal with fune pull validation.gs1-check-digit@1.0.0:typescript.
The whole function, every language, is one file too: validation.gs1-check-digit-1.0.0.fune, 15,242 bytes, sha256 f57294b4fe86f2e8ced3fffedc28f4fba3738cd0d9037d8ce207562cf69d30ca. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before validation.gs1-check-digit.gs1CheckDigit
// fune: before validation.gs1-check-digit.isGs1CheckDigitValid
after — your function gets the result and the arguments, and returns the final result.
// fune: after validation.gs1-check-digit.gs1CheckDigit
// fune: after validation.gs1-check-digit.isGs1CheckDigitValid
replace — it requires no other capability, so there is no dependency to replace.
step — your function runs at a numbered point inside a function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show validation.gs1-check-digit --steps.
// fune: step validation.gs1-check-digit.<fn> after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
gs1CheckDigit 15 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| GS1's worked example: GTIN-13 629104150021 takes check digit 3 | 629104150021 | → | 3 |
| GS1's worked example: SSCC 37610425002123456 takes check digit 9 | 37610425002123456 | → | 9 |
| GS1's SSCC label example (00) 1 0614141 123456789 7 | 10614141123456789 | → | 7 |
| seven digits, a GTIN-8: its first digit is weighted 3 | 9638507 | → | 4 |
| eleven digits, a UPC-A | 03600029145 | → | 2 |
| thirteen digits, a GTIN-14 case code | 1003600029145 | → | 9 |
| a sum that is already a multiple of ten gives 0, not 10 | 35012345678000042 | → | 0 |
| all zeros gives 0: the arithmetic has no opinion on it | 000000000000 | → | 0 |
| a single data digit is weighted 3 | 1 | → | 7 |
| a longer key than any GTIN, thirty data digits | 123456789012345678901234567890 | → | 5 |
Show the other 5 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| the empty string has no check digit | → | error: GS1 data must be one or more ASCII digits | |
| a letter is refused, not skipped | 62910415002A | → | error: GS1 data must be one or more ASCII digits |
| spaces are not tidied: strip print groups before calling | 6291041 50021 | → | error: GS1 data must be one or more ASCII digits |
| an Arabic-Indic digit is not an ASCII digit | 62910415002١ | → | error: GS1 data must be one or more ASCII digits |
| a non-string is refused | — | → | error: GS1 data must be one or more ASCII digits |
isGs1CheckDigitValid 15 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| GS1's worked example GTIN-13 6291041500213 | 6291041500213 | → | true |
| GS1's worked example SSCC 376104250021234569 | 376104250021234569 | → | true |
| the GTIN-13 example with its check digit one off | 6291041500214 | → | false |
| a GTIN-8 | 96385074 | → | true |
| a UPC-A, twelve digits | 036000291452 | → | true |
| a GTIN-14 | 10036000291459 | → | true |
| an SSCC whose check digit is 0 | 350123456780000420 | → | true |
| two adjacent digits transposed | 4006383133931 | → | false |
| all zeros passes the arithmetic; refusing it is the key's own rule | 0000000000000 | → | true |
| the shortest key, one data digit and its check digit | 17 | → | true |
Show the other 5 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a lone digit is not a key | 0 | → | false |
| the empty string | → | false | |
| a letter makes it false rather than an error | 629104150021X | → | false |
| print groups are not tidied | 629104 1500213 | → | false |
| a non-string is false | — | → | false |
More from the author
## The rule
Weight the data digits 3, 1, 3, 1 ... starting from the rightmost and moving left, add them up, and the check digit is whatever brings the sum up to a multiple of ten: `(10 - sum mod 10) mod 10`. A sum that is already a multiple of ten gives 0, not 10.
Weighting from the right is what lets one rule serve every length, and it is the detail home-grown code gets wrong. An EAN-13's first digit is weighted 1, but a GTIN-8's, a UPC-A's and an SSCC's first digit is weighted 3, so code that always starts at the left with 1 is right only for keys with an even number of data digits.
GS1's worked example: the GTIN-13 data `629104150021` weighted from the right sums to 57, so the check digit is 3 and the GTIN is `6291041500213`. The SSCC data `37610425002123456` sums to 101, check digit 9. Both are vectors.
## Input
ASCII digits only, of any length (at least one data digit; at least two digits for a whole key). Spaces, hyphens and the `(00)`-style application identifiers printed on labels are not tidied away: strip them before calling, because what counts as a separator is the key's business, not the checksum's. Non-ASCII digits (Arabic-Indic, superscripts) are refused in every language.
- `gs1CheckDigit` throws on anything else, empty included, with `GS1 data must be one or more ASCII digits`: there is no digit to return. - `isGs1CheckDigitValid` answers `false` instead, as validators do.
## What it does not check
Only the arithmetic. It does not know which lengths a key may have, what a GS1 prefix means, or whether GS1 allocated the number. An all-zero key passes the arithmetic (its check digit is 0); `validation.gtin` refuses it as a GTIN, and any other key that should do the same says so itself. A passing key is well formed: every single mistyped digit and most adjacent transpositions (all except those of two digits differing by 5) change the check digit.
## Source
GS1 General Specifications, section 7.9 "Check digit calculation"; https://www.gs1.org/services/how-calculate-check-digit-manually