Module: UpcTools

Defined in:
lib/upc_tools.rb,
lib/upc_tools/version.rb

Overview

UPC Tools

Constant Summary collapse

WEIGHT_FACTOR_2 =
[0, 2, 4, 6, 8, 9, 1, 3, 5, 7]
WEIGHT_FACTOR_3 =
[0, 3, 6, 9, 2, 5, 8, 1, 4, 7]
WEIGHT_FACTOR_5plus =
[0, 5, 1, 6, 2, 7, 3, 8, 4, 9]
WEIGHT_FACTOR_5mins =
[0, 5, 9, 4, 8, 3, 7, 2, 6, 1]
WEIGHT_FACTOR_5mins_opposite =
[0, 9, 7, 5, 3, 1, 8, 6, 4, 2]
VERSION =
"0.2.1"

Class Method Summary collapse

Class Method Details

.convert_upca_to_upce(upc_a) ⇒ String

Convert (zero-suppress) 12 digit UPC-A to 8 digit UPC-E

Parameters:

  • upc_a (String)

    12 digit UPC-A to convert

Returns:

  • (String)

    8 digit UPC-E

Raises:

  • (ArgumentError)

See Also:



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# 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) ⇒ String

Convert short (8 digit) UPC-E to 12 digit UPC-A

Parameters:

  • upc_e (String)

    8 digit UPC-E to convert

Returns:

  • (String)

    12 digit UPC-A

Raises:

  • (ArgumentError)

See Also:



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# 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) ⇒ String

Add check digit and properly pad

Parameters:

  • num (String)

    base number to extend

  • extended_length (Integer) (defaults to: 12)

    resulting target to pad number to

Returns:

  • (String)

    resulting UPC with check digit



37
38
39
40
# 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) ⇒ Integer

Generate price check digit for type 2 upc price of 4 digits

Parameters:

  • price (String)

    price as integer (in cents)

Returns:

  • (Integer)

    calculated price check digit

See Also:



184
185
186
187
188
189
190
191
192
193
# 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) ⇒ Integer

Generate price check digit for type 2 upc price of 5 digits

Parameters:

  • price (String)

    price as integer (in cents)

Returns:

  • (Integer)

    calculated price check digit

See Also:



199
200
201
202
203
204
205
206
207
208
209
210
# 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) ⇒ Integer

Generate one UPC check digit

Parameters:

  • num (String)

    base number to generate check digit for

Returns:

  • (Integer)

    check digit (always between 0-9)

See Also:



10
11
12
13
14
15
16
17
18
19
20
# 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) ⇒ Float

Get the float price from a Type2 UPC

Parameters:

  • upc (String)

    UPC to get price from

  • skip_price_check (Boolean) (defaults to: false)

    Ignore price check digit (include digit in price field)

Returns:

  • (Float)

    calculated price (rounded to nearest cent)



158
159
160
161
# 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 = {}) ⇒ Object

Convert item ID (PLU) and price to type2 UPC string

Parameters:

  • plu (String)

    item identifier (not including leading 2)

  • price (String)

    price as integer (in cents). Will be 0 padded if necessary

  • opts (Hash) (defaults to: {})

    options hash

Options Hash (opts):

  • :price_length (Integer) — default: 4

    price length (4 or 5). Will override given price length.

  • :upc_length (Integer) — default: 12

    price length (12 or 13)

Raises:

  • (ArgumentError)


105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# 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) ⇒ Array<String>

Split a Type2 UPC into its component parts

Parameters:

  • upc (String)

    UPC to split up

  • skip_price_check (Boolean) (defaults to: false)

    Ignore price check digit (include digit in price field)

Returns:

  • (Array<String>)

    elements of array: ItemID/PLU (not including leading 2), Price, UPC Check Digit, Price Check Digit

See Also:



140
141
142
143
144
145
146
147
148
149
150
151
152
# 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) ⇒ String

Trim UPC to proper length for type2 checking

Parameters:

  • upc (String)

    UPC

Returns:

  • (String)

    trimmed string



59
60
61
62
63
64
# 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) ⇒ Array<String,Float>

Split a type2 UPC into the UPC itself and the price contained therein.

Parameters:

  • number (String)

    upc to check

Returns:

  • (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.



166
167
168
169
170
171
172
173
174
175
176
177
# 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) ⇒ Boolean

Is this a type2 UPC?

Parameters:

  • upc (String)

    upc to check

Returns:

  • (Boolean)

    is UPC a type-2?



69
70
71
72
73
# 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) ⇒ Boolean

Convenience method validates that upc is type2 with valid check digit

Parameters:

  • upc (String)

    Type 2 UPC to check

Returns:

  • (Boolean)

    is UPC a type-2 with valid check digit(s)?



95
96
97
# 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) ⇒ Boolean

Validate UPC and Price check digit for a type 2 upc. Does NOT also check the UPC itself

Parameters:

  • upc (String)

    Type 2 UPC to check with check digit(s)

Returns:

  • (Boolean)

    matching check digit(s)?



78
79
80
81
82
83
84
85
86
87
88
89
90
# 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) ⇒ Boolean

Validate UPC check digit

Parameters:

  • upc (String)

    UPC with check digit to check

Returns:

  • (Boolean)

    truth of valid check digit

See Also:



27
28
29
30
31
# 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