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.0"

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:



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/upc_tools.rb', line 263

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:



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/upc_tools.rb', line 235

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



38
39
40
41
# File 'lib/upc_tools.rb', line 38

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:



185
186
187
188
189
190
191
192
193
194
# File 'lib/upc_tools.rb', line 185

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:



200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/upc_tools.rb', line 200

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:



11
12
13
14
15
16
17
18
19
20
21
# File 'lib/upc_tools.rb', line 11

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)



159
160
161
162
# File 'lib/upc_tools.rb', line 159

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)


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
133
# File 'lib/upc_tools.rb', line 106

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:



141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/upc_tools.rb', line 141

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



60
61
62
63
64
65
# File 'lib/upc_tools.rb', line 60

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.



167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/upc_tools.rb', line 167

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?



70
71
72
73
74
# File 'lib/upc_tools.rb', line 70

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)?



96
97
98
# File 'lib/upc_tools.rb', line 96

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)?



79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/upc_tools.rb', line 79

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:



28
29
30
31
32
# File 'lib/upc_tools.rb', line 28

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