ExGtin

CI Status Static Badge Hex.pm Version Packagist contributions welcome

A GTIN (Global Trade Item Number) & UPC (Universal Price Code) Generation and Validation Library in Elixir under the GS1 specification.

Features

Features to Come:

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

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"}
iex> ExGtin.generate("629104150021")
{:ok, "6291041500213"}
iex> ExGtin.generate!("629104150021")
"6291041500213"
iex> ExGtin.Validation.find_gs1_prefix_country("53523235")
{:ok, "GS1 Malta"}
iex> ExGtin.normalize("6291041500213")
{:ok, "16291041500210"}

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:

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.

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:

Using Strings, Arrays or Numbers

iex> ExGtin.validate("6291041500213")
{:ok, "GTIN-13"}
iex> ExGtin.validate([6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1, 3])
{:ok, "GTIN-13"}
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