ExGtin
A GTIN (Global Trade Item Number) & UPC (Universal Price Code) Generation and Validation Library in Elixir under the GS1 specification.
- GTIN-8 (EAN/UCC-8): this is an 8-digit number used predominately outside of North America
- GTIN-12 (UPC-A): this is a 12-digit number used primarily in North America
- GTIN-13 (EAN/UCC-13): this is a 13-digit number used predominately outside of North America - Global Location Number (GLN)
- GTIN-14 (EAN/UCC-14 or ITF-14): this is a 14-digit number used to identify trade items at various packaging levels
Features
- Supports GTIN-8, GTIN-12 (UPC-12), GTIN-13 (GLN), GTIN-14
- Generate GTIN
- Check GTIN validity
- Lookup GS1 country prefix
- Convert (normalize) GTIN-13 to GTIN-14
- Parse GS1-128 / Application Identifier (AI) element strings (both the
parenthesized
(01)...form and the raw FNC1 scanner form) - Recognize and decode variable-measure restricted-circulation numbers (RCNs) against an explicit region/retailer scheme
Features to Come:
- Global Shipment Identification Number (GSIN)
- Serial Shipping Container Code (SSCC)
Installation
WARNING 1.0.1 contains breaking changes from 1.0.0 (I know this is a patch release but the breaking changes related to the deprecation of check_gtin and generate_gtin were noted in the changelog and docs over a year ago)
WARNING 1.0.0 contains breaking changes from 0.4.0
Add :ex_gtin as a dependency to your project's mix.exs:
def deps do
[{:ex_gtin, "~> 1.5.0"}]
end
and run mix deps.get to install the :ex_gtin dependency
mix deps.get
Usage
- Check GTIN codes
iex> ExGtin.validate("6291041500213")
{:ok, "GTIN-13"}
iex> ExGtin.validate("6291041500214")
{:error, "Invalid Code"}
iex> ExGtin.validate!("6291041500213")
"GTIN-13"
Pass GTIN numbers in as a String, Number or an Array
iex> number = [6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1, 3]
iex> ExGtin.validate(number)
{:ok, "GTIN-13"}
iex> number = 6_291_041_500_213
iex> ExGtin.validate(number)
{:ok, "GTIN-13"}
- Generate GTIN codes
iex> ExGtin.generate("629104150021")
{:ok, "6291041500213"}
iex> ExGtin.generate!("629104150021")
"6291041500213"
- Lookup GS1 Prefix
iex> ExGtin.Validation.find_gs1_prefix_country("53523235")
{:ok, "GS1 Malta"}
- Convert GTIN-13 to GTIN 14
iex> ExGtin.normalize("6291041500213")
{:ok, "16291041500210"}
- Parse GS1-128 / Application Identifier (AI) element strings
ExGtin.parse_gs1/1 accepts both the parenthesized human-readable form and the
raw FNC1 scanner form of the same payload and returns an %{ai => value} map.
# Parenthesized form (starts with `(`)
iex> ExGtin.parse_gs1("(01)06291041500213(17)261231(10)ABC123")
{:ok, %{"01" => "06291041500213", "17" => "261231", "10" => "ABC123"}}
# Raw FNC1 form of the same payload (variable-length AIs are terminated by the
# FNC1/GS separator, ASCII 29)
iex> ExGtin.parse_gs1("010629104150021310ABC123" <> <<29>> <> "21XYZ789")
{:ok, %{"01" => "06291041500213", "10" => "ABC123", "21" => "XYZ789"}}
The embedded GTIN (AI 01) is check-digit validated, and date-format AIs are
returned as their raw YYMMDD string by default. Pass dates: :parsed to
interpret them into Elixir Date structs:
iex> ExGtin.parse_gs1("(01)06291041500213(17)261231", dates: :parsed)
{:ok, %{"01" => "06291041500213", "17" => ~D[2026-12-31]}}
The initially supported AI set is:
01— GTIN10— Batch/Lot Number11— Production Date13— Packaging Date15— Best Before Date17— Expiration Date21— Serial Number- the 3xx variable-measure weight AIs:
3100–3105(net weight, kg),3200–3205(net weight, lb),3300–3305(gross weight, kg),3400–3405(gross weight, lb), and3560–3565(net weight, troy ounce)
An AI outside this set is unsupported and produces an error. When it appears
at the very start (nothing parsed yet), parse_gs1/1 returns
{:error, {:unknown_ai, code, position}}:
iex> ExGtin.parse_gs1("(99)ABC")
{:error, {:unknown_ai, "99", 0}}
When an unsupported AI (or otherwise uninterpretable input) follows one or more successfully parsed AIs, the unparsed remainder is reported alongside the parsed map as a 3-tuple rather than dropped:
iex> ExGtin.parse_gs1("(01)06291041500213(99)ABC")
{:ok, %{"01" => "06291041500213"}, "(99)ABC"}
ExGtin.parse_gs1!/1 is the raising variant: it returns the parsed map on a
full parse, returns a {map, unparsed} tuple on a partial parse, and raises
ArgumentError only on a hard error.
- Parse variable-measure restricted-circulation numbers (RCNs)
Restricted-circulation numbers are GTINs whose leading digits fall in the GS1
restricted-circulation ranges — the 02 prefix (020–029) and the 20–29
prefixes (200–299). These codes are issued for in-store or in-company use
(for example a variable-measure retail item priced by weight) and are not
globally unique, so ExGtin.RCN.recognize/1 lets you tell an RCN apart from a
normal GTIN.
iex> ExGtin.RCN.recognize("2001234500009")
{:ok, "2001234500009"}
iex> ExGtin.RCN.recognize("6291041500213")
{:error, "Not a restricted-circulation number"}
An RCN's embedded price or weight has no single, universal layout: the exact
digit positions of the item reference, the embedded amount, and any internal
check digit are defined by the region or retailer that issues the code. For
that reason decoding never assumes one global layout — you always pass an
explicit scheme, either a shipped scheme's named atom or your own scheme map.
ExGtin.parse_rcn/2 is the public entry point (backed by ExGtin.RCN.decode/2,
which is available today) and returns the raw item reference plus an embedded
map carrying the raw :price or :weight digit substring:
iex> ExGtin.RCN.decode("2123451789012", :gs1_germany_price)
{:ok, %{item: "12345", embedded: %{price: "78901"}}}
# Supply your own layout as a scheme map
iex> scheme = %{prefix: ["2"], item: 2..6, embedded: {:price, 8..12}, check: nil}
iex> ExGtin.RCN.decode("2123456789012", scheme)
{:ok, %{item: "12345", embedded: %{price: "78901"}}}
The schemes shipped in ExGtin.RCN.Schemes are documented, illustrative
defaults on the "2" prefix; treat them as sensible starting points and
override with your own scheme map as your market requires:
:gs1_germany_price— a GS1 Germany / EU-style price-embedded layout with an internal price check digit (a mod-10 check digit over the embedded price field):gs1_embedded_weight— a weight-embedded layout with no internal check digit
Using Strings, Arrays or Numbers
- String
iex> ExGtin.validate("6291041500213")
{:ok, "GTIN-13"}
- Array of Integers
iex> ExGtin.validate([6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1, 3])
{:ok, "GTIN-13"}
- Integer
iex> ExGtin.validate(6291041500213)
{:ok, "GTIN-13"}
Integers with leading zeros may not process properly
Reference
Documentation can be found at https://hexdocs.pm/ex_gtin on HexDocs.
Tests
Run tests with
mix test
Run test coverage
MIX_ENV=test mix coveralls
Produce Coverage report in HTML to cover/excoveralls.html
MIX_ENV=test mix coveralls.html
Contributing
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
When making pull requests, please be sure to update the CHANGELOG.md with the corresponding changes. Please make sure that all tests pass, add tests for new functionality and that the static analysis checker credo is run. Use mix pull_request_checkout.task to ensure that everything checks out.
Run static code analysis
mix credo
Generate Documentation
mix docs
Run the gambit of tests, static analysis and coverage
mix pull_request_checkout.task
Sponsors
This project is sponsored by KickinEspresso
Versioning
We use SemVer for versioning. For the versions available, see the tags on this repository.
Code of Conduct
Please refer to the Code of Conduct for details
Security
Please refer to the Security for details
License
This project is licensed under the MIT License - see the LICENSE.md file for details
Publish & Releasing
mix hex.publish