Packages

Elixir Global Trade Item Number (GTIN) Validation Library for GS1, UPC-12, and GLN. Validates GTIN-8, GTIN-12 (UPC-12), GTIN-13 (GLN), GTIN-14 codes. Universal Price Code (UPC)

Current section

Files

Jump to
ex_gtin lib validation validation.ex
Raw

lib/validation/validation.ex

defmodule ExGtin.Validation do
@moduledoc """
Documentation for ExGtin. This library provides
functionality for validating GTIN compliant codes.
"""
@moduledoc since: "1.0.0"
@doc """
Check for valid GTIN-8, GTIN-12, GTIN-13, GTIN-14, GSIN, SSCC codes
Returns `{atom, String.t()}`
## Examples
iex> ExGtin.Validation.gtin_check_digit("6291041500213")
{:ok, "GTIN-13"}
iex> ExGtin.Validation.gtin_check_digit("6291041500214")
{:error, "Invalid Code"}
"""
@doc since: "1.0.0"
@spec gtin_check_digit(String.t()) :: {atom, String.t()}
def gtin_check_digit(number) when is_bitstring(number) do
number
|> String.codepoints()
|> Enum.map(&String.to_integer/1)
|> gtin_check_digit()
end
@spec gtin_check_digit(number) :: {atom, String.t()}
def gtin_check_digit(number) when is_number(number),
do: gtin_check_digit(Integer.digits(number))
@spec gtin_check_digit(list(number)) :: {atom, String.t()}
def gtin_check_digit(number) do
case check_code_length(number) do
{:ok, gtin_type} ->
{code, check_digit} = Enum.split(number, length(number) - 1)
calculated_check_digit =
code
|> multiply_and_sum_array
|> subtract_from_nearest_multiple_of_ten
case calculated_check_digit == Enum.at(check_digit, 0) do
true -> {:ok, gtin_type}
_ -> {:error, "Invalid Code"}
end
{:error, error} ->
{:error, error}
end
end
@doc """
Generate valid GTIN-8, GTIN-12, GTIN-13, GTIN-14, GSIN, SSCC codes
Returns `{atom, String.t()}`
## Examples
iex> ExGtin.Validation.generate_gtin_code("629104150021")
{:ok, "6291041500213"}
iex> ExGtin.Validation.generate_gtin_code("62921")
{:error, "Invalid GTIN Code Length"}
"""
@doc since: "1.0.0"
@spec generate_gtin_code(String.t()) :: String.t() | {atom, String.t()}
def generate_gtin_code(number) when is_bitstring(number) do
number
|> String.codepoints()
|> Enum.map(&String.to_integer/1)
|> generate_gtin_code()
end
@spec generate_gtin_code(number) :: String.t() | {atom, String.t()}
def generate_gtin_code(number) when is_number(number),
do: generate_gtin_code(Integer.digits(number))
@spec generate_gtin_code(list(number)) :: String.t() | {atom, String.t()}
def generate_gtin_code(number) do
case generate_check_code_length(number) do
{:ok, _} ->
check_digit = generate_check_digit(number)
result = Enum.join(number ++ [check_digit])
{:ok, result}
{:error, error} ->
{:error, error}
end
end
@doc """
Generate check digit GTIN-8, GTIN-12, GTIN-13, GTIN-14, GSIN, SSCC codes
Returns `number`
## Examples
iex> ExGtin.Validation.generate_check_digit([6,2,9,1,0,4,1,5,0,0,2,1])
3
"""
@doc since: "1.0.0"
@spec generate_check_digit(list(number)) :: number
def generate_check_digit(number) do
number
|> multiply_and_sum_array()
|> subtract_from_nearest_multiple_of_ten()
end
@doc """
Calculates the sum of the digits in a string and multiplied value based on index order
Returns `number`
## Examples
iex> ExGtin.Validation.multiply_and_sum_array([6,2,9,1,0,4,1,5,0,0,2,1])
57
"""
@doc since: "1.0.0"
@spec multiply_and_sum_array(list(number)) :: number
def multiply_and_sum_array(numbers) do
numbers
|> Enum.reverse()
|> Stream.with_index()
|> Enum.reduce(0, fn {num, idx}, acc ->
acc + num * mult_by_index_code(idx)
end)
end
@doc """
Calculates the difference of the highest rounded multiple of 10
Returns `number`
## Examples
iex> ExGtin.Validation.subtract_from_nearest_multiple_of_ten(57)
3
"""
@doc since: "1.0.0"
@spec subtract_from_nearest_multiple_of_ten(number) :: number
def subtract_from_nearest_multiple_of_ten(number), do: rem(10 - rem(number, 10), 10)
@doc """
By index, returns the corresponding value to multiply
the digit by
Returns `number`
## Examples
iex> ExGtin.Validation.mult_by_index_code(1)
1
iex> ExGtin.Validation.mult_by_index_code(2)
3
"""
@doc since: "1.0.0"
@spec mult_by_index_code(number) :: number
def mult_by_index_code(index) when rem(index, 2) == 1, do: 1
def mult_by_index_code(_), do: 3
@doc """
Checks the code for the proper length as specified by the
GTIN-8,12,13,14 specification
Returns {atom, String.t()}
## Examples
iex> ExGtin.Validation.check_code_length([1,2,3,4,5,6,7,8])
{:ok, "GTIN-8"}
iex> ExGtin.Validation.check_code_length([1,2,3,4,5,6,7])
{:error, "Invalid GTIN Code Length"}
"""
@doc since: "1.0.0"
@spec check_code_length([number]) :: {atom, String.t()}
def check_code_length(number) do
case length(number) do
8 -> {:ok, "GTIN-8"}
12 -> {:ok, "GTIN-12"}
13 -> {:ok, "GTIN-13"}
14 -> {:ok, "GTIN-14"}
_ -> {:error, "Invalid GTIN Code Length"}
end
end
@doc """
When generating the code, checks the code for
the proper length as specified by the GTIN-8,12,13,14 specification.
The code should be -1 the length of the GTIN code as the check digit
will be added later
Returns {atom, String.t()}
## Examples
iex> ExGtin.Validation.generate_check_code_length([1,2,3,4,5,6,7])
{:ok, "GTIN-8"}
iex> ExGtin.Validation.generate_check_code_length([1,2,3,4,5,6])
{:error, "Invalid GTIN Code Length"}
"""
@doc since: "1.0.0"
@spec generate_check_code_length(list(number)) :: {atom, String.t()}
def generate_check_code_length(number), do: check_code_length(number ++ [1])
@doc """
Find the GS1 prefix country for a GTIN number
Returns `{atom, String.t()}`
## Examples
iex> ExGtin.Validation.find_gs1_prefix_country("53523235")
{:ok, "GS1 Malta"}
iex> ExGtin.Validation.find_gs1_prefix_country("6291041500214")
{:ok, "GS1 Emirates"}
iex> ExGtin.Validation.find_gs1_prefix_country("9541041500214")
{:error, "No GS1 prefix found"}
"""
@doc since: "1.0.0"
@spec find_gs1_prefix_country(String.t()) :: {atom, String.t()}
def find_gs1_prefix_country(number) when is_bitstring(number) do
number
|> String.codepoints()
|> Enum.map(&String.to_integer/1)
|> find_gs1_prefix_country()
end
@spec find_gs1_prefix_country(number) :: {atom, String.t()}
def find_gs1_prefix_country(number) when is_number(number),
do: find_gs1_prefix_country(Integer.digits(number))
@spec find_gs1_prefix_country(list(number)) :: {atom, String.t()}
def find_gs1_prefix_country(number) do
case check_code_length(number) do
{:ok, gtin_type} ->
normalized =
case gtin_type do
"GTIN-12" -> [0] ++ number
"GTIN-14" -> Enum.drop(number, 1)
_ -> number
end
{prefix, _code} = Enum.split(normalized, 3)
prefix
|> Enum.join()
|> String.to_integer()
|> lookup_gs1_prefix
{:error, error} ->
{:error, error}
end
end
@doc """
Looks up the GS1 prefix in a table
GS1 Reference https://www.gs1.org/company-prefix
Returns {atom, String.t()}
"""
@doc since: "1.0.0"
@spec lookup_gs1_prefix(integer) :: {atom, String.t()}
# credo:disable-for-next-line
def lookup_gs1_prefix(number) do
case number do
x when x in 001..019 ->
{:ok, "GS1 US"}
x when x in 030..039 ->
{:ok, "GS1 US"}
x when x in 050..059 ->
{:ok, "GS1 US"}
x when x in 060..099 ->
{:ok, "GS1 US"}
x when x in 100..139 ->
{:ok, "GS1 US"}
x when x == 535 ->
{:ok, "GS1 Malta"}
x when x in 020..029 ->
{:ok,
"Used to issue restricted circulation numbers within a geographic region (MO defined)"}
x when x in 040..049 ->
{:ok, "Used to issue GS1 restricted circulation numbers within a company"}
x when x in 200..299 ->
{:ok,
"Used to issue GS1 restricted circulation number within a geographic region (MO defined)"}
x when x in 300..379 ->
{:ok, "GS1 France"}
x when x == 380 ->
{:ok, "GS1 Bulgaria"}
x when x == 383 ->
{:ok, "GS1 Slovenija"}
x when x == 385 ->
{:ok, "GS1 Croatia"}
x when x == 387 ->
{:ok, "GS1 BIH (Bosnia-Herzegovina)"}
x when x == 389 ->
{:ok, "GS1 Montenegro"}
x when x in 400..440 ->
{:ok, "GS1 Germany"}
x when x in 450..459 ->
{:ok, "GS1 Japan"}
x when x in 490..499 ->
{:ok, "GS1 Japan"}
x when x in 460..469 ->
{:ok, "GS1 Russia"}
x when x == 470 ->
{:ok, "GS1 Kyrgyzstan"}
x when x == 471 ->
{:ok, "GS1 Taiwan"}
x when x == 474 ->
{:ok, "GS1 Estonia"}
x when x == 475 ->
{:ok, "GS1 Latvia"}
x when x == 476 ->
{:ok, "GS1 Azerbaijan"}
x when x == 477 ->
{:ok, "GS1 Lithuania"}
x when x == 478 ->
{:ok, "GS1 Uzbekistan"}
x when x == 479 ->
{:ok, "GS1 Sri Lanka"}
x when x == 480 ->
{:ok, "GS1 Philippines"}
x when x == 481 ->
{:ok, "GS1 Belarus"}
x when x == 482 ->
{:ok, "GS1 Ukraine"}
x when x == 483 ->
{:ok, "GS1 Turkmenistan"}
x when x == 484 ->
{:ok, "GS1 Moldova"}
x when x == 485 ->
{:ok, "GS1 Armenia"}
x when x == 486 ->
{:ok, "GS1 Georgia"}
x when x == 487 ->
{:ok, "GS1 Kazakstan"}
x when x == 488 ->
{:ok, "GS1 Tajikistan"}
x when x == 489 ->
{:ok, "GS1 Hong Kong"}
x when x in 500..509 ->
{:ok, "GS1 UK"}
x when x in 520..521 ->
{:ok, "GS1 Association Greece"}
x when x == 528 ->
{:ok, "GS1 Lebanon"}
x when x == 529 ->
{:ok, "GS1 Cyprus"}
x when x == 530 ->
{:ok, "GS1 Albania"}
x when x == 531 ->
{:ok, "GS1 Macedonia"}
x when x == 535 ->
{:ok, "GS1 Malta"}
x when x == 539 ->
{:ok, "GS1 Ireland"}
x when x in 540..549 ->
{:ok, "GS1 Belgium & Luxembourg"}
x when x == 560 ->
{:ok, "GS1 Portugal"}
x when x == 569 ->
{:ok, "GS1 Iceland"}
x when x in 570..579 ->
{:ok, "GS1 Denmark"}
x when x == 590 ->
{:ok, "GS1 Poland"}
x when x == 594 ->
{:ok, "GS1 Romania"}
x when x == 599 ->
{:ok, "GS1 Hungary"}
x when x in 600..601 ->
{:ok, "GS1 South Africa"}
x when x == 603 ->
{:ok, "GS1 Ghana"}
x when x == 604 ->
{:ok, "GS1 Senegal"}
x when x == 608 ->
{:ok, "GS1 Bahrain"}
x when x == 609 ->
{:ok, "GS1 Mauritius"}
x when x == 611 ->
{:ok, "GS1 Morocco"}
x when x == 613 ->
{:ok, "GS1 Algeria"}
x when x == 615 ->
{:ok, "GS1 Nigeria"}
x when x == 616 ->
{:ok, "GS1 Kenya"}
x when x == 618 ->
{:ok, "GS1 Ivory Coast"}
x when x == 619 ->
{:ok, "GS1 Tunisia"}
x when x == 620 ->
{:ok, "GS1 Tanzania"}
x when x == 621 ->
{:ok, "GS1 Syria"}
x when x == 622 ->
{:ok, "GS1 Egypt"}
x when x == 623 ->
{:ok, "GS1 Brunei"}
x when x == 624 ->
{:ok, "GS1 Libya"}
x when x == 625 ->
{:ok, "GS1 Jordan"}
x when x == 626 ->
{:ok, "GS1 Iran"}
x when x == 627 ->
{:ok, "GS1 Kuwait"}
x when x == 628 ->
{:ok, "GS1 Saudi Arabia"}
x when x == 629 ->
{:ok, "GS1 Emirates"}
x when x in 640..649 ->
{:ok, "GS1 Finland"}
x when x in 690..699 ->
{:ok, "GS1 China"}
x when x in 700..709 ->
{:ok, "GS1 Norway"}
x when x == 729 ->
{:ok, "GS1 Israel"}
x when x in 730..739 ->
{:ok, "GS1 Sweden"}
x when x == 740 ->
{:ok, "GS1 Guatemala"}
x when x == 741 ->
{:ok, "GS1 El Salvador"}
x when x == 742 ->
{:ok, "GS1 Honduras"}
x when x == 743 ->
{:ok, "GS1 Nicaragua"}
x when x == 744 ->
{:ok, "GS1 Costa Rica"}
x when x == 745 ->
{:ok, "GS1 Panama"}
x when x == 746 ->
{:ok, "GS1 Republica Dominicana"}
x when x == 750 ->
{:ok, "GS1 Mexico"}
x when x in 754..755 ->
{:ok, "GS1 Canada"}
x when x == 759 ->
{:ok, "GS1 Venezuela"}
x when x in 760..769 ->
{:ok, "GS1 Schweiz, Suisse, Svizzera"}
x when x in 770..771 ->
{:ok, "GS1 Colombia"}
x when x == 773 ->
{:ok, "GS1 Uruguay"}
x when x == 775 ->
{:ok, "GS1 Peru"}
x when x == 777 ->
{:ok, "GS1 Bolivia"}
x when x in 778..779 ->
{:ok, "GS1 Argentina"}
x when x == 780 ->
{:ok, "GS1 Chile"}
x when x == 784 ->
{:ok, "GS1 Paraguay"}
x when x == 786 ->
{:ok, "GS1 Ecuador"}
x when x in 789..790 ->
{:ok, "GS1 Brasil"}
x when x in 800..839 ->
{:ok, "GS1 Italy"}
x when x in 840..849 ->
{:ok, "GS1 Spain"}
x when x == 850 ->
{:ok, "GS1 Cuba"}
x when x == 858 ->
{:ok, "GS1 Slovakia"}
x when x == 859 ->
{:ok, "GS1 Czech"}
x when x == 860 ->
{:ok, "GS1 Serbia"}
x when x == 865 ->
{:ok, "GS1 Mongolia"}
x when x == 867 ->
{:ok, "GS1 North Korea"}
x when x in 868..869 ->
{:ok, "GS1 Turkey"}
x when x in 870..879 ->
{:ok, "GS1 Netherlands"}
x when x == 880 ->
{:ok, "GS1 South Korea"}
x when x == 884 ->
{:ok, "GS1 Cambodia"}
x when x == 885 ->
{:ok, "GS1 Thailand"}
x when x == 888 ->
{:ok, "GS1 Singapore"}
x when x == 890 ->
{:ok, "GS1 India"}
x when x == 893 ->
{:ok, "GS1 Vietnam"}
x when x == 896 ->
{:ok, "GS1 Pakistan"}
x when x == 899 ->
{:ok, "GS1 Indonesia"}
x when x in 900..919 ->
{:ok, "GS1 Austria"}
x when x in 930..939 ->
{:ok, "GS1 Australia"}
x when x in 940..949 ->
{:ok, "GS1 New Zealand"}
x when x == 950 ->
{:ok, "GS1 Global Office"}
x when x == 951 ->
{:ok,
"Used to issue General Manager Numbers for the EPC General Identifier (GID) scheme as defined by the EPC Tag Data Standard*"}
x when x == 955 ->
{:ok, "GS1 Malaysia"}
x when x == 958 ->
{:ok, "GS1 Macau"}
x when x in 960..969 ->
{:ok, "Global Office (GTIN-8s)*"}
x when x == 977 ->
{:ok, "Serial publications (ISSN)"}
x when x in 978..979 ->
{:ok, "Bookland (ISBN)"}
x when x == 980 ->
{:ok, "Refund receipts"}
x when x in 981..984 ->
{:ok, "GS1 coupon identification for common currency areas"}
x when x in 990..999 ->
{:ok, "GS1 coupon identification"}
_ ->
{:error, "No GS1 prefix found"}
end
end
end