module UpcTools

UPC Tools

Constants

VERSION
WEIGHT_FACTOR_2

@see www.gs1tw.org/twct/web/BarCode/GS1_Section3V6-0.pdf Figure 3.A.1.2 – 1 Weight Factor 2-

WEIGHT_FACTOR_3

@see www.gs1tw.org/twct/web/BarCode/GS1_Section3V6-0.pdf Figure 3.A.1.2 – 2 Weight Factor 3

WEIGHT_FACTOR_5mins

@see www.gs1tw.org/twct/web/BarCode/GS1_Section3V6-0.pdf Figure 3.A.1.2 – 4 Weight Factor 5-

WEIGHT_FACTOR_5mins_opposite

@see www.gs1tw.org/twct/web/BarCode/GS1_Section3V6-0.pdf Figure 3.A.1.2 – 4 inverse Weight Factor 5-

WEIGHT_FACTOR_5plus

@see www.gs1tw.org/twct/web/BarCode/GS1_Section3V6-0.pdf Figure 3.A.1.2 – 2 Weight Factor 5+

Public Class Methods

convert_upca_to_upce(upc_a) click to toggle source

Convert (zero-suppress) 12 digit UPC-A to 8 digit UPC-E @see www.taltech.com/barcodesoftware/symbologies/upc @see en.wikipedia.org/wiki/Universal_Product_Code#UPC-E @see www.barcodeisland.com/upce.phtml#Conversion @param upc_a [String] 12 digit UPC-A to convert @return [String] 8 digit UPC-E

# File lib/upc_tools.rb, line 262
def self.convert_upca_to_upce(upc_a)
  # todo should i zero pad upc_a?
  # todo allow without check digit?
  upc_a = upc_a.to_s
  raise ArgumentError, "Must be 12 characters long" unless upc_a.size == 12
  start = upc_a[0] # first char
  raise ArgumentError, "Must be type 0 or 1" unless ["0", "1"].include?(start)

  chk = upc_a[-1] # last char
  mfr = upc_a[1...6] # next 5 characters
  prod = upc_a[6...11] # last 4 characters w/o chk

  upc_e = if ["000", "100", "200"].include?(mfr[-3, 3])
    "#{mfr[0, 2]}#{prod[-3, 3]}#{mfr[2]}"
  elsif mfr[-2, 2] == "00" && prod.to_i <= 99
    "#{mfr[0, 3]}#{prod[-2, 2]}3"
  elsif mfr[-1] == "0" && prod.to_i <= 9
    "#{mfr[0, 4]}#{prod[-1]}4"
  elsif mfr[-1] != "0" && [5, 6, 7, 8, 9].include?(prod.to_i)
    "#{mfr}#{prod[-1]}"
  end
  raise ArgumentError, "Must meet formatting requirements" unless upc_e

  "#{start}#{upc_e}#{chk}"
end
convert_upce_to_upca(upc_e) click to toggle source

Convert short (8 digit) UPC-E to 12 digit UPC-A @see www.taltech.com/barcodesoftware/symbologies/upc @see en.wikipedia.org/wiki/Universal_Product_Code#UPC-E @param upc_e [String] 8 digit UPC-E to convert @return [String] 12 digit UPC-A

# File lib/upc_tools.rb, line 234
def self.convert_upce_to_upca(upc_e)
  # todo should i zero pad upc_e?
  # todo allow without check digit?
  upc_e = upc_e.to_s
  raise ArgumentError, "UPC-E must be 8 digits" unless upc_e.size == 8

  map_id = upc_e[-2].to_i # G
  chk = upc_e[-1] # H
  prefix = upc_e[0, 3] # ABC
  prefix_next = upc_e[3, 3] # DEF

  if map_id >= 5
    "#{prefix}#{prefix_next}0000#{map_id}#{chk}"
  elsif map_id <= 2
    "#{prefix}#{map_id}0000#{prefix_next}#{chk}"
  elsif map_id == 3
    "#{prefix}#{upc_e[3]}00000#{upc_e[4, 2]}#{chk}"
  elsif map_id == 4
    "#{prefix}#{upc_e[3, 2]}00000#{upc_e[5]}#{chk}"
  end
end
extend_upc_with_check_digit(num, extended_length = 12) click to toggle source

Add check digit and properly pad @param num [String] base number to extend @param extended_length [Integer] resulting target to pad number to @return [String] resulting UPC with check digit

# File lib/upc_tools.rb, line 37
def self.extend_upc_with_check_digit(num, extended_length = 12)
  upc = num.to_s << generate_upc_check_digit(num).to_s
  upc.rjust(extended_length, "0") # extend to at least the given length
end
generate_type2_upc_price_check_digit_4(price) click to toggle source

Generate price check digit for type 2 upc price of 4 digits @see www.gs1tw.org/twct/web/BarCode/GS1_Section3V6-0.pdf section 3.A.1.3 @see barcodes.gs1us.org/GS1%20US%20BarCodes%20and%20eCom%20-%20The%20Global%20Language%20of%20Business.htm @param price [String] price as integer (in cents) @return [Integer] calculated price check digit

# File lib/upc_tools.rb, line 184
def self.generate_type2_upc_price_check_digit_4(price)
  # digit weighting factors 2-, 2-, 3, 5-
  digits = price.to_s.rjust(4, "0").split("").map(&:to_i)
  sum = 0
  sum += WEIGHT_FACTOR_2[digits[0]]
  sum += WEIGHT_FACTOR_2[digits[1]]
  sum += WEIGHT_FACTOR_3[digits[2]]
  sum += WEIGHT_FACTOR_5mins[digits[3]]
  (sum * 3) % 10
end
generate_type2_upc_price_check_digit_5(price) click to toggle source

Generate price check digit for type 2 upc price of 5 digits @see www.gs1tw.org/twct/web/BarCode/GS1_Section3V6-0.pdf section 3.A.1.4 @param price [String] price as integer (in cents) @return [Integer] calculated price check digit

# File lib/upc_tools.rb, line 199
def self.generate_type2_upc_price_check_digit_5(price)
  # digit weighting factors 5+, 2-, 5-, 5+, 2- => opposite of 5-
  digits = price.to_s.rjust(5, "0").split("").map(&:to_i)
  sum = 0
  sum += WEIGHT_FACTOR_5plus[digits[0]]
  sum += WEIGHT_FACTOR_2[digits[1]]
  sum += WEIGHT_FACTOR_5mins[digits[2]]
  sum += WEIGHT_FACTOR_5plus[digits[3]]
  sum += WEIGHT_FACTOR_2[digits[4]]
  sum = (10 - (sum % 10)) % 10
  WEIGHT_FACTOR_5mins_opposite[sum]
end
generate_upc_check_digit(num) click to toggle source

Generate one UPC check digit @see www.gs1.org/barcodes/support/check_digit_calculator/ @see www.gs1tw.org/twct/web/BarCode/GS1_Section3V6-0.pdf section 3.A.1.1 @param num [String] base number to generate check digit for @return [Integer] check digit (always between 0-9)

# File lib/upc_tools.rb, line 10
def self.generate_upc_check_digit(num)
  even = odd = 0
  # pad everything to max (13)
  num.to_s.rjust(13, "0").split("").each_with_index do |item, index|
    item = item.to_i
    even += item if index.odd? # opposite because of 0 indexing
    odd += item if index.even?
  end
  chk_total = (odd * 3) + even
  (10 - (chk_total % 10)) % 10
end
get_price_from_type2_upc(upc, skip_price_check = false) click to toggle source

Get the float price from a Type2 UPC @param upc [String] UPC to get price from @param skip_price_check [Boolean] Ignore price check digit (include digit in price field) @return [Float] calculated price (rounded to nearest cent)

# File lib/upc_tools.rb, line 158
def self.get_price_from_type2_upc(upc, skip_price_check = false)
  _, price = UpcTools.split_type2_upc(upc, skip_price_check)
  (price.to_f / 100.0).round(2)
end
item_price_to_type2(plu, price, opts = {}) click to toggle source

Convert item ID (PLU) and price to type2 UPC string @param plu [String] item identifier (not including leading 2) @param price [String] price as integer (in cents). Will be 0 padded if necessary @param opts [Hash] options hash @option opts [Integer] :price_length (4) price length (4 or 5). Will override given price length. @option opts [Integer] :upc_length (12) price length (12 or 13)

# File lib/upc_tools.rb, line 105
def self.item_price_to_type2(plu, price, opts = {})
  upc_length = opts[:upc_length] || 12
  price_length = opts[:price_length] || 4
  raise ArgumentError, "opts[:upc_length] must be 12 or 13" if upc_length != 12 && upc_length != 13

  if upc_length == 13
    raise ArgumentError, "Price length cannot be 4 if UPC length is 13" if opts[:price_length] == 4
    price_length = 5
    raise ArgumentError, "opts[:price_length] must be 4 or 5" if price_length != 4 && price_length != 5
  end

  plu = plu.to_s
  raise ArgumentError, "plu must be 5 digits long" if plu.size != 5

  price = price.to_s.rjust(price_length, "0")
  raise ArgumentError, "price must be less than or equal to 5 digits long" if price.size > 5

  price_chk_calc = if price.size == 4
    generate_type2_upc_price_check_digit_4(price)
  elsif price.size == 5 && upc_length == 13
    generate_type2_upc_price_check_digit_5(price)
  else
    ""
  end

  upc = "2#{plu}#{price_chk_calc}#{price}"
  upc << generate_upc_check_digit(upc).to_s
end
split_type2_upc(upc, skip_price_check = false) click to toggle source

Split a Type2 UPC into its component parts @see www.meattrack.com/Background/UPC.php @see www.iddba.org/upccharacter2.aspx @param upc [String] UPC to split up @param skip_price_check [Boolean] Ignore price check digit (include digit in price field) @return [Array<String>] elements of array: ItemID/PLU (not including leading 2), Price, UPC Check Digit, Price Check Digit

# File lib/upc_tools.rb, line 140
def self.split_type2_upc(upc, skip_price_check = false)
  upc = trim_type2_upc(upc)
  plu = upc[1, 5]
  chk = upc[-1]
  if upc.size == 13 || skip_price_check
    price = upc[-6, 5]
    price_chk = upc[-7] unless skip_price_check
  else
    price = upc[-5, 4]
    price_chk = upc[-6] unless skip_price_check
  end
  [plu, price, chk, price_chk]
end
trim_type2_upc(upc) click to toggle source

Trim UPC to proper length for type2 checking @param upc [String] UPC @return [String] trimmed string

# File lib/upc_tools.rb, line 59
def self.trim_type2_upc(upc)
  # if length is > 12, strip leading 0
  upc = upc.to_s
  upc = upc.gsub(/^0+/, "") if upc.size > 12
  upc
end
type2_number_price(number) click to toggle source

Split a type2 UPC into the UPC itself and the price contained therein. @param number [String] upc to check @return [Array<String,Float>] elements of array: type2 UPC string, Price. The UPC ends up with a 0 price if it is type2. The Price will be nil if the number passed in is not type2.

# File lib/upc_tools.rb, line 166
def self.type2_number_price(number)
  if type2_upc?(number) && valid_type2_upc_check_digit?(number)
    # looks like a type-2 and the price chk is valid
    item_code, price = split_type2_upc(number)
    price = (price.to_f / 100.0).round(2)

    upc = item_price_to_type2(item_code, 0).rjust(14, "0")
    [upc, price]
  else
    [number, nil]
  end
end
type2_upc?(upc) click to toggle source

Is this a type2 UPC? @param upc [String] upc to check @return [Boolean] is UPC a type-2?

# File lib/upc_tools.rb, line 69
def self.type2_upc?(upc)
  upc = trim_type2_upc(upc)
  return false if upc.size > 13 || upc.size < 12 # length is wrong
  upc.start_with?("2")
end
valid_type2_upc?(upc) click to toggle source

Convenience method validates that upc is type2 with valid check digit @param upc [String] Type 2 UPC to check @return [Boolean] is UPC a type-2 with valid check digit(s)?

# File lib/upc_tools.rb, line 95
def self.valid_type2_upc?(upc)
  type2_upc?(upc) && valid_type2_upc_check_digit?(upc)
end
valid_type2_upc_check_digit?(upc) click to toggle source

Validate UPC and Price check digit for a type 2 upc. Does NOT also check the UPC itself @param upc [String] Type 2 UPC to check with check digit(s) @return [Boolean] matching check digit(s)?

# File lib/upc_tools.rb, line 78
def self.valid_type2_upc_check_digit?(upc)
  upc = trim_type2_upc(upc)
  return false unless type2_upc?(upc)
  _plu, price, _chk, price_chk = split_type2_upc(upc)
  price_chk_calc = if price.size == 4
    generate_type2_upc_price_check_digit_4(price)
  elsif price.size == 5
    generate_type2_upc_price_check_digit_5(price)
  else
    raise ArgumentError, "Price is an unknown size"
  end
  price_chk == price_chk_calc.to_s
end
valid_upc_check_digit?(upc) click to toggle source

Validate UPC check digit @see www.gs1.org/barcodes/support/check_digit_calculator/ @see www.gs1tw.org/twct/web/BarCode/GS1_Section3V6-0.pdf section 3.A.1.1 @param upc [String] UPC with check digit to check @return [Boolean] truth of valid check digit

# File lib/upc_tools.rb, line 27
def self.valid_upc_check_digit?(upc)
  full_upc = upc.to_s.rjust(14, "0") # extend to full 14 digits first
  gen_check = generate_upc_check_digit(full_upc[0, full_upc.size - 1])
  full_upc[-1] == gen_check.to_s
end