Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SSCC / GS1-128 validator

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.


Build, run, test

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

Requires 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.


What a scanned SSCC label looks like

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:

  1. AI 00 has 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.
  2. 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.


The GS1 Modulo-10 check digit

Over the 17 SSCC digits (the two AI characters are excluded, whatever they are; so is the printed check digit itself):

  1. Walking right to left, weight the digits 3, 1, 3, 1, … so the rightmost data digit always gets weight 3.
  2. Sum the products.
  3. 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 = 136136 mod 10 = 610 − 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.

What the check digit does and does not catch

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.


Why values are stored as strings, never numbers

  • An SSCC is a licence plate, not a quantity. Nothing is ever added to it.
  • Leading zeroes carry meaning. 00034260311130776594 begins with the AI 00 and an extension digit 0. Parsing to an integer would produce 34260311130776594 and 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 ij". That is a string operation.

Expected company prefix

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.


Validation order, and where it stops

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 09 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).

Input normalisation

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.


Decoded-data validation vs. physical-symbol validation

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.


Results for the six supplied samples

# 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

Sample 2 — one error, plus clearly separated hypothesis

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.

Sample 3 — a check digit that was not recalculated

Compare with sample 1: the serial reference changed (1307765913077614) 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.

Sample 5 — why nothing else is reported

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.

Sample 6 — structurally correct, but not TSX's

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.


Project layout

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.


Sources

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 00 semantics; 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.

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages