Functional Weave
Code in Python

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. gs1_check_digit (data: string) -> int
  2. is_gs1_check_digit_valid (key: string) -> bool

Once installed, your code imports each one from the group's module.

gs1_check_digit throws on bad input 15 tests

def gs1_check_digit(data: str) -> int
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

  • gs1_check_digit(629104150021) → 3 GS1's worked example: GTIN-13 629104150021 takes check digit 3
  • gs1_check_digit(37610425002123456) → 9 GS1's worked example: SSCC 37610425002123456 takes check digit 9
  • gs1_check_digit(10614141123456789) → 7 GS1's SSCC label example (00) 1 0614141 123456789 7
from fune.validation.gs1_check_digit import gs1_check_digit  # validation.gs1-check-digit@^1
impl/python/gs1_check_digit.py · 23 lines · open · raw
def _ascii_digits(value: object) -> bool:
    # Compare against ASCII explicitly: str.isdigit() accepts Arabic-Indic and
    # superscript digits, which the other two languages would reject.
    return isinstance(value, str) and value != "" and all("0" <= ch <= "9" for ch in value)


def gs1_check_digit(data: str) -> int:
    """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.
    """
    if not _ascii_digits(data):
        raise ValueError("GS1 data must be one or more ASCII digits, received %r" % (data,))
    total = 0
    weight = 3
    for ch in reversed(data):
        total = (total + (ord(ch) - 48) * weight) % 10
        weight = 1 if weight == 3 else 3
    return (10 - total) % 10

is_gs1_check_digit_valid 15 tests

def is_gs1_check_digit_valid(key: str) -> bool
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

  • is_gs1_check_digit_valid(6291041500213) → true GS1's worked example GTIN-13 6291041500213
  • is_gs1_check_digit_valid(376104250021234569) → true GS1's worked example SSCC 376104250021234569
  • is_gs1_check_digit_valid(6291041500214) → false the GTIN-13 example with its check digit one off
from fune.validation.gs1_check_digit import is_gs1_check_digit_valid  # validation.gs1-check-digit@^1
impl/python/is_gs1_check_digit_valid.py · 14 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.

from .validation_gs1_check_digit_gs1_check_digit import gs1_check_digit  ← gs1CheckDigit, another function of this group · built into the same file, even by a slim install


def is_gs1_check_digit_valid(key: str) -> bool:
    """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 raise, so anything that is
    not two or more ASCII digits is False.
    """
    if not isinstance(key, str) or len(key) < 2 or any(ch < "0" or ch > "9" for ch in key):
        return False
    return gs1_check_digit(key[:-1]) == ord(key[-1]) - 48

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Python 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 Python validation.gs1-check-digit-1.0.0-python.fune · 11,468 bytes sha256 77bed52f4133d6ee152efeba9e5bfc30911ecd95f9cc1b04040cce923a1827ec

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./validation.gs1-check-digit-1.0.0-python.fune, or fetch it from a terminal with fune pull validation.gs1-check-digit@1.0.0:python.

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