Validates scanned SSCC-18 labels against the GS1 data rules, for the assignment
described in NALOGA SSCC (TSX GmbH pallet labels, GS1 Company Prefix
34260311).
Portable C++17, no third-party runtime dependencies.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/sscc # scope note, the six supplied samples, then interactive input
./build/sscc 00034260311130776594 # validate values passed as arguments, then exit
cd build && ctest --output-on-failureRequires CMake 3.16+ and any C++17 compiler. Built and tested with
-Wall -Wextra -Wpedantic -Wshadow -Wconversion (MSVC: /W4 /permissive-),
warning-free.
Exit status reports whether the program ran, not whether the barcodes were valid. The default run deliberately demonstrates invalid samples; a successful demonstration of a bad label is not an execution failure. Validity is reported through the structured result and the console output.
The RF terminal types the decoded data into an editor as 20 characters:
0 0 | 0 | 3 4 2 6 0 3 1 1 | 1 3 0 7 7 6 5 9 | 4
AI | E | company prefix | serial ref. | check
(2) |(1)| (8) | (8) | (1)
\_____________ 18-digit SSCC _____________/
| Field | Length | Meaning |
|---|---|---|
| Application Identifier | 2 | 00 declares that an SSCC-18 follows |
| Extension digit | 1 | No defined logic; it only multiplies the serial capacity by 10 |
| GS1 Company Prefix | Variable — 8 for TSX | Licensed to one company; what makes the number globally unique |
| Serial Reference | 16 − prefix length | Assigned by the prefix holder; identifies one physical pallet |
| Check digit | 1 | GS1 Modulo-10 over the 17 preceding digits |
Two facts drive most of the code:
- AI
00has a predefined length. Exactly 18 numeric characters follow it, so the line is always 20 characters and no FNC1/GS separator may follow the data — the parser already knows where the field ends. - Company prefix + serial reference is always 16 digits, but the split between them is not encoded in the barcode. We only know TSX's prefix is 8 digits because the assignment says so. The code therefore derives the split from the configured prefix length rather than hard-coding 8 + 8.
GS1 Company Prefix lengths are assigned by the issuing GS1 Member Organisation and vary; gs1-128.info gives 7–10 digits, while GS1's own material describes a broader range. The program deliberately enforces no length rule — it only requires that the configured prefix is numeric and leaves at least one digit for the serial reference.
Over the 17 SSCC digits (the two AI characters are excluded, whatever they are; so is the printed check digit itself):
- Walking right to left, weight the digits
3, 1, 3, 1, …so the rightmost data digit always gets weight 3. - Sum the products.
check = (10 − (sum mod 10)) mod 10
Worked example — supplied sample 1, 00034260311130776594:
| Pos | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Digit | 0 | 3 | 4 | 2 | 6 | 0 | 3 | 1 | 1 | 1 | 3 | 0 | 7 | 7 | 6 | 5 | 9 |
| Weight | 3 | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 |
| Product | 0 | 3 | 12 | 2 | 18 | 0 | 9 | 1 | 3 | 1 | 9 | 0 | 21 | 7 | 18 | 5 | 27 |
Sum = 136 → 136 mod 10 = 6 → 10 − 6 = 4 → check digit 4. Matches the
printed digit.
The outer mod 10 is essential, not decoration. When the weighted sum is
already a multiple of 10, 10 − 0 = 10, which is not a digit; it must fold to
0. Supplied sample 4 is exactly this case (sum 130 → check digit 0), and it is
covered by both a static_assert and a unit test.
Indexing from the right rather than using the "odd positions get weight 3" shortcut keeps the function correct for other GS1 keys: a GTIN-13 has 12 data digits, so its leftmost digit gets weight 1.
It detects 100 % of single-digit substitution errors: changing one digit by
d ≠ 0 shifts the sum by d·w with w ∈ {1,3}, and since gcd(3,10) = 1,
d·w ≡ 0 (mod 10) is impossible for |d| ≤ 9.
It does not detect every adjacent transposition: swapping neighbours a, b
shifts the sum by 2(b − a), which vanishes mod 10 whenever the digits differ
by 5 (27 ↔ 72, 38 ↔ 83).
And it never authenticates ownership — see sample 6 below.
- An SSCC is a licence plate, not a quantity. Nothing is ever added to it.
- Leading zeroes carry meaning.
00034260311130776594begins with the AI00and an extension digit0. Parsing to an integer would produce34260311130776594and destroy three significant characters. (Anyone who has pasted barcodes into Excel has seen this bug in the wild.) - An 18-digit value happens to fit in
int64_t, but a 20-digit GTIN-style key would not — a size-dependent representation is a latent bug. - Validation is positional: every rule is "the characters at offsets i…j". That is a string operation.
34260311 is passed into sscc::Validator at construction, from
app/main.cpp. The validation logic is not coupled to it: the check-digit
algorithm is a general GS1 rule, while "is this ours?" is configuration. A unit
test constructs a validator with a different prefix and asserts the verdicts
flip.
No 7-to-10-digit rule is enforced. GS1 company prefix lengths vary by issuing GS1 Member Organisation, so the constructor only requires that the prefix is numeric and leaves at least one digit for the serial reference.
A check runs only while every position and value it depends on is still trustworthy.
| # | Check | On failure |
|---|---|---|
| 1 | EmptyInput |
stop |
| 2 | UnsupportedInputFormat — parentheses, or ASCII GS 0x1D |
stop |
| 3 | InvalidLength — not 20 characters |
record; blocks step 6 |
| 4 | NonNumericInput — any character outside 0–9 |
record; blocks step 6 |
| 5 | WrongApplicationIdentifier — first two characters ≠ 00 |
record; does not block step 6 |
| 6 | Company prefix and check digit | only if exactly 20 characters, all numeric |
Steps 3–5 all run even when an earlier one failed, because the assignment asks the program to report precisely what is wrong. One complete report beats three round-trips with the printing company.
Step 6 is different. It reads fixed offsets, so a missing or extra character shifts every boundary to its right by an unknown amount. Sample 5 is one character short and there is no way to know which character was dropped — so the program reports the length error and explicitly refuses to guess. Saying "company prefix OK" there would be a false reassurance the data does not support.
If the AI is wrong but the value is still 20 numeric characters, step 6 runs into a separate list of hypothetical candidate findings (see sample 2).
normalizeScannedInput() trims leading and trailing spaces, tabs, \r and \n
— and nothing else. RF terminals append an Enter keystroke to every scan and
users paste with stray spaces; neither is a property of the label.
It never touches interior characters and never strips leading zeroes. Stripping
"all non-digits" would silently turn 000342603111 3077659A4 into a valid-
looking value — erasing the very evidence a defect detector exists to find.
Parenthesised human-readable input such as (00)034… is therefore rejected
with an explanation, not quietly repaired.
These are two different questions, and only one is answerable from a text capture.
Layer B — decoded data (what this program does). AI, length, numeric content, company prefix, check digit. All visible in the scanned string.
Layer A — the physical symbol (what this program cannot do).
- the mandatory FNC1 in the first symbol position;
- whether the symbol is GS1-128 or plain Code 128;
- the Code 128 modulo-103 symbol check character and subset switching;
- quiet zones, X-dimension, print contrast, ISO/IEC 15416 grade;
- that the prefix is genuinely licensed to TSX;
- serial-reference uniqueness and the 12-month non-reuse rule.
The crux: FNC1 is exactly what distinguishes GS1-128 from plain Code 128, and
a keyboard-wedge scanner does not transmit it — the assignment says so itself.
A plain Code 128 symbol encoding the literal text 00034260311130776594, with no
FNC1 anywhere, produces a byte-for-byte identical line in Notepad and would pass
every check this program can make while being non-conformant to GS1-128.
Because AI 00 is a predefined-length AI, no separator FNC1 follows the data
either, so the leading one is the only FNC1 in the whole symbol.
The program prints this limitation once at start-up rather than hiding it.
Practical way to close the gap: configure the scanner to transmit AIM
symbology identifiers and re-scan one label. ]C1 means GS1-128; ]C0 means
plain Code 128. That two-minute settings change answers the question the six
samples cannot.
Note also that the modulo-10 check digit (data level, in the number) and the Code 128 modulo-103 symbol check character (symbology level, in the bars) are completely different mechanisms at different layers. A wrong modulo-103 character means the label does not decode at all, so it never reaches this program.
| # | Scanned value | Verdict | Finding |
|---|---|---|---|
| 1 | 00034260311130776594 |
VALID | — |
| 2 | 02044260311130776512 |
INVALID | AI is 02, not 00 |
| 3 | 00034260311130776144 |
INVALID | check digit 4, calculated 3 |
| 4 | 00034260311130776570 |
VALID | exercises the sum-divisible-by-10 case |
| 5 | 0003426031113077646 |
INVALID | 19 characters, expected 20 |
| 6 | 00034260321130774636 |
INVALID | prefix 34260321, expected 34260311 |
The AI is 02 (GTIN of trade items contained in a logistic unit, a 14-digit
field), so the digits that follow are not an SSCC. That is the single primary
error.
But the printer was clearly attempting an SSCC, and Bonus 1 asks what is wrong
with the label, so the program also reports what would be wrong under the
explicitly stated assumption that an SSCC was intended: the prefix segment is
44260311 (expected 34260311) and the check digit would have to be 7, not
2. These live in a separate candidate list, never influence the verdict, and
are labelled as hypothetical in the output.
Compare with sample 1: the serial reference changed (13077659 → 13077614) but
the check digit stayed 4. That is consistent with a check digit carried over
from a previous label rather than recalculated — though the string alone cannot
prove what the printing system actually did.
19 characters. A digit is missing and its position is unknowable, so field boundaries after the loss may have shifted. The program reports the length error and states why it stops there.
AI 00 ✓, length ✓, numeric ✓, check digit 6 correct ✓. The only mismatch
is at the company-prefix positions: 34260321 where TSX's prefix is 34260311.
The check digit passes because it is consistent with the 17 digits actually present — and that is the whole lesson: the modulo-10 algorithm validates whatever number it is attached to. It protects a number against corruption in transit; it does not authenticate who holds the prefix. Detecting this requires business context that no arithmetic supplies — which is precisely why the expected prefix is a separate, configurable rule rather than part of the check-digit code.
The program does not claim that 34260321 is licensed, or that it belongs to
any particular company. That cannot be established without a GS1 registry lookup.
It also does not present 34260321 | 13077463 as an authoritative prefix/serial
split: an SSCC does not encode where a prefix ends, so that split is only the
layout configured for TSX.
include/sscc/validation.hpp layout constants, check digit, diagnostics, Validator
include/sscc/report.hpp presentation layer interface
src/validation.cpp normalisation + validation (facts only, no prose, no I/O)
src/report.cpp ValidationResult -> human-readable text
app/main.cpp CLI: argv, stdin, printing (the only layer doing I/O)
tests/sscc_tests.cpp 24 test cases, 174 assertions, dependency-free harness
The validator returns a structured ValidationResult and never prints. Tests
assert on diagnostic codes and payload values, never on English text, so the
wording can change without breaking a single test.
CheckState { NotEvaluated, Passed, Failed } is a tri-state on purpose: for
sample 5 the honest answer to "is the prefix correct?" is unknown, not false,
and collapsing those two is exactly the misleading diagnostic this program exists
to avoid.
Primary, as referenced by the assignment:
- Wikipedia — GS1-128 — GS1-128 as a
subset of Code 128; FNC1's dual role; the mandatory modulo-103 symbol check
character;
FNC4 is not used by GS1-128. - gs1-128.info — SSCC-18 — AI
00semantics; prefix 7–10 digits; "The combined length of the GS1 Company Prefix and Serial Reference is always 16 digits"; "The Extension Digit has no defined logic"; the 12-month non-reuse rule.
Supplemental, and why they were needed: neither primary source states the Modulo-10 formula — gs1-128.info defers to an external calculator, and the Wikipedia GS1-128 article covers only the symbology's modulo-103 check character, a different mechanism at a different layer.
- GS1 General Specifications — the
normative source for AI
00= N2+N18 and the predefined-length AI table. - Wikipedia — Serial Shipping Container Code — the four-component structure.
- GS1 Canada check digit calculator and
ActiveBarcode —
corroboration of the 3/1 weighting and
(10 − sum mod 10) mod 10.
The formula was not taken on trust: it reproduces the printed check digit exactly for samples 1, 4 and 6 — three independent 17-digit strings — which is decisive confirmation.