Functional Weave
Code in TypeScript

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.

  1. gs1CheckDigit (data: string) -> int
  2. 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
datastringthe key's digits without its check digit, any length from 1; ASCII digits only, no spaces
returnsint0 to 9, the digit to append

For example

  • gs1CheckDigit(629104150021) → 3 GS1's worked example: GTIN-13 629104150021 takes check digit 3
  • gs1CheckDigit(37610425002123456) → 9 GS1's worked example: SSCC 37610425002123456 takes check digit 9
  • gs1CheckDigit(10614141123456789) → 7 GS1's SSCC label example (00) 1 0614141 123456789 7
import { gs1CheckDigit } from "#fune/validation.gs1-check-digit@^1";
impl/typescript/gs1_check_digit.ts · 20 lines · open · raw
/**
 * 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
keystringa whole key, check digit last; ASCII digits only, at least 2
returnsboolfalse 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 6291041500213
  • isGs1CheckDigitValid(376104250021234569) → true GS1's worked example SSCC 376104250021234569
  • isGs1CheckDigitValid(6291041500214) → false the GTIN-13 example with its check digit one off
import { isGs1CheckDigitValid } from "#fune/validation.gs1-check-digit@^1";
impl/typescript/is_gs1_check_digit_valid.ts · 12 lines · open · raw

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
Download for TypeScript validation.gs1-check-digit-1.0.0-typescript.fune · 11,312 bytes sha256 51d57a6838b35f7ea5fdfa5834077ee673e0a224e3369e28d1def002e4177296

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

CaseArgumentsExpected
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
CaseArgumentsExpected
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

CaseArgumentsExpected
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
CaseArgumentsExpected
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

Files

PathBytes
README.md2,669
impl/python/gs1_check_digit.py1,009
impl/python/is_gs1_check_digit_valid.py592
impl/rust/gs1_check_digit.rs1,223
impl/rust/is_gs1_check_digit_valid.rs841
impl/typescript/gs1_check_digit.ts869
impl/typescript/is_gs1_check_digit_valid.ts587
vectors.json3,916