defmodule Barlix.UPCE do
@moduledoc """
Implements [UPC-E](https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E).
"""
@doc """
Encodes the given value using UPC-E. The given code is validated first.
## Examples
iex> Barlix.UPCE.encode("04252614")
{:ok, {:D1, [
1, 0, 1,
0, 0, 1, 1, 1, 0, 1,
0, 0, 1, 0, 0, 1, 1,
0, 1, 1, 1, 0, 0, 1,
0, 0, 1, 1, 0, 1, 1,
0, 1, 0, 1, 1, 1, 1,
0, 0, 1, 1, 0, 0, 1,
0, 1, 0, 1, 0, 1
]}}
iex> Barlix.UPCE.encode("123456")
{:error, "expected a string with exactly 8 chars, received 6 chars instead"}
iex> Barlix.UPCE.encode("06543214")
{:error, "validation failed: expected checksum digit 7 but received 4"}
"""
@spec encode(String.t()) :: {:error, String.t()} | {:ok, Barlix.code()}
def encode(value) do
with {:ok, values} <- validate(value) do
{:ok, get_code(values)}
end
end
@doc """
Accepts the same arguments as `encode/1` but raises on error.
"""
@spec encode!(String.t()) :: Barlix.code() | no_return
def encode!(value) do
case encode(value) do
{:ok, code} -> code
{:error, error} -> raise Barlix.Error, error
end
end
@doc """
Validate an UPC-E code.
## Examples
iex> Barlix.UPCE.validate("04252614")
{:ok, [0, 4, 2, 5, 2, 6, 1, 4]}
iex> Barlix.UPCE.validate("123456789")
{:error, "expected a string with exactly 8 chars, received 9 chars instead"}
iex> Barlix.UPCE.validate("16543217")
{:error, "validation failed: expected checksum digit 4 but received 7"}
iex> Barlix.UPCE.validate(123)
{:error, "unexpected input"}
"""
@spec validate(String.t()) :: {:ok, [non_neg_integer()]} | {:error, String.t()}
def validate(upc_e) when is_binary(upc_e) and byte_size(upc_e) == 8 do
if String.match?(upc_e, ~r/^\d{8}$/) do
with {:ok, _ean13_values} <-
upc_e
|> to_ean13()
|> Barlix.EAN13.validate() do
{:ok,
upc_e
|> String.split("", trim: true)
|> Enum.map(&String.to_integer/1)}
end
else
{:error, "validation failed, string must only contain digits"}
end
end
def validate(s) when is_binary(s) do
{:error, "expected a string with exactly 8 chars, received #{String.length(s)} chars instead"}
end
def validate(_), do: {:error, "unexpected input"}
defp to_ean13(upc_e) do
case upc_e do
<<system, manufacturer::binary-size(2), product::binary-size(3), "0", check_digit>> ->
<<"0", system, manufacturer::binary, "00000", product::binary, check_digit>>
<<system, manufacturer::binary-size(2), product::binary-size(3), "1", check_digit>> ->
<<"0", system, manufacturer::binary, "10000", product::binary, check_digit>>
<<system, manufacturer::binary-size(2), product::binary-size(3), "2", check_digit>> ->
<<"0", system, manufacturer::binary, "20000", product::binary, check_digit>>
<<system, manufacturer::binary-size(3), product::binary-size(2), "3", check_digit>> ->
<<"0", system, manufacturer::binary, "00000", product::binary, check_digit>>
<<system, manufacturer::binary-size(4), product, "4", check_digit>> ->
<<"0", system, manufacturer::binary, "00000", product, check_digit>>
<<system, manufacturer::binary-size(5), product, check_digit>> ->
<<"0", system, manufacturer::binary, "0000", product, check_digit>>
end
end
defp get_code(values) do
{[number_system | digits], [check_digit]} = Enum.split(values, 7)
encoded_digits =
digits
|> Enum.zip(parity_pattern(check_digit, number_system))
|> Enum.map(fn {digit, encoding_fun} -> encoding_fun.(digit) end)
{:D1, List.flatten([start_guard(), encoded_digits, end_guard()])}
end
defp parity_pattern(check_digit, number_system)
defp parity_pattern(0, 0), do: [&even/1, &even/1, &even/1, &odd/1, &odd/1, &odd/1]
defp parity_pattern(1, 0), do: [&even/1, &even/1, &odd/1, &even/1, &odd/1, &odd/1]
defp parity_pattern(2, 0), do: [&even/1, &even/1, &odd/1, &odd/1, &even/1, &odd/1]
defp parity_pattern(3, 0), do: [&even/1, &even/1, &odd/1, &odd/1, &odd/1, &even/1]
defp parity_pattern(4, 0), do: [&even/1, &odd/1, &even/1, &even/1, &odd/1, &odd/1]
defp parity_pattern(5, 0), do: [&even/1, &odd/1, &odd/1, &even/1, &even/1, &odd/1]
defp parity_pattern(6, 0), do: [&even/1, &odd/1, &odd/1, &odd/1, &even/1, &even/1]
defp parity_pattern(7, 0), do: [&even/1, &odd/1, &even/1, &odd/1, &even/1, &odd/1]
defp parity_pattern(8, 0), do: [&even/1, &odd/1, &even/1, &odd/1, &odd/1, &even/1]
defp parity_pattern(9, 0), do: [&even/1, &odd/1, &odd/1, &even/1, &odd/1, &even/1]
defp parity_pattern(0, 1), do: [&odd/1, &odd/1, &odd/1, &even/1, &even/1, &even/1]
defp parity_pattern(1, 1), do: [&odd/1, &odd/1, &even/1, &odd/1, &even/1, &even/1]
defp parity_pattern(2, 1), do: [&odd/1, &odd/1, &even/1, &even/1, &odd/1, &even/1]
defp parity_pattern(3, 1), do: [&odd/1, &odd/1, &even/1, &even/1, &even/1, &odd/1]
defp parity_pattern(4, 1), do: [&odd/1, &even/1, &odd/1, &odd/1, &even/1, &even/1]
defp parity_pattern(5, 1), do: [&odd/1, &even/1, &even/1, &odd/1, &odd/1, &even/1]
defp parity_pattern(6, 1), do: [&odd/1, &even/1, &even/1, &even/1, &odd/1, &odd/1]
defp parity_pattern(7, 1), do: [&odd/1, &even/1, &odd/1, &even/1, &odd/1, &even/1]
defp parity_pattern(8, 1), do: [&odd/1, &even/1, &odd/1, &even/1, &even/1, &odd/1]
defp parity_pattern(9, 1), do: [&odd/1, &even/1, &even/1, &odd/1, &even/1, &odd/1]
# Encoding tables
defp start_guard, do: [1, 0, 1]
defp odd(0), do: [0, 0, 0, 1, 1, 0, 1]
defp odd(1), do: [0, 0, 1, 1, 0, 0, 1]
defp odd(2), do: [0, 0, 1, 0, 0, 1, 1]
defp odd(3), do: [0, 1, 1, 1, 1, 0, 1]
defp odd(4), do: [0, 1, 0, 0, 0, 1, 1]
defp odd(5), do: [0, 1, 1, 0, 0, 0, 1]
defp odd(6), do: [0, 1, 0, 1, 1, 1, 1]
defp odd(7), do: [0, 1, 1, 1, 0, 1, 1]
defp odd(8), do: [0, 1, 1, 0, 1, 1, 1]
defp odd(9), do: [0, 0, 0, 1, 0, 1, 1]
defp even(0), do: [0, 1, 0, 0, 1, 1, 1]
defp even(1), do: [0, 1, 1, 0, 0, 1, 1]
defp even(2), do: [0, 0, 1, 1, 0, 1, 1]
defp even(3), do: [0, 1, 0, 0, 0, 0, 1]
defp even(4), do: [0, 0, 1, 1, 1, 0, 1]
defp even(5), do: [0, 1, 1, 1, 0, 0, 1]
defp even(6), do: [0, 0, 0, 0, 1, 0, 1]
defp even(7), do: [0, 0, 1, 0, 0, 0, 1]
defp even(8), do: [0, 0, 0, 1, 0, 0, 1]
defp even(9), do: [0, 0, 1, 0, 1, 1, 1]
defp end_guard, do: [0, 1, 0, 1, 0, 1]
end