barcode1dtools 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,246 @@
1
+ #--
2
+ # Copyright 2012 Michael Chaney Consulting Corporation
3
+ #
4
+ # Released under the terms of the MIT License or the GNU
5
+ # General Public License, v. 2
6
+ #++
7
+
8
+ require 'barcode1dtools/upc_a'
9
+
10
+ module Barcode1DTools
11
+
12
+ # Barcode1DTools::UPC_Supplemental_5 - Create pattern for UPC
13
+ # Supplemental 5 barcodes
14
+ #
15
+ # The value encoded is an 5-digit integer, and a checksum digit
16
+ # will be added. You can add the option :checksum_included => true
17
+ # when initializing to specify that you have already included a
18
+ # checksum. The bar patterns are the same as the left
19
+ # half of a standard UPC-A.
20
+ #
21
+ # num = '53999' # book price is US$39.99
22
+ # bc = Barcode1DTools::UPC_Supplemental_5.new(num)
23
+ # pattern = bc.bars
24
+ # rle_pattern = bc.rle
25
+ # bc.price # returns the "price" part as 4 digits
26
+ # bc.currency # returns the first digit currency code
27
+ # width = bc.width
28
+ # check_digit = Barcode1DTools::UPC_Supplemental_5.generate_check_digit_for(num)
29
+ #
30
+ # This type of barcode consists of 5 digits, and a check digit
31
+ # (a modulus 10 of the sum of the digits with weights of 3 and
32
+ # 9) that is encoded in the "parity" of the two barcode
33
+ # digits. It is positioned to the right of a UPC-A or EAN-13
34
+ # to create a "Bookland" code.
35
+ #
36
+ # The two are scanned together, and typically the scanner will
37
+ # return the five digits of the supplemental barcode
38
+ # immediately following the check digit from the main barcode.
39
+ # You will likely need to use the Barcode::UPC_A or
40
+ # Barcode::EAN_13 module in addition to this one to create the
41
+ # full code.
42
+ #
43
+ # The 5-digit supplement is generally used on literature, and
44
+ # represents a type-indicator digit followed by a 4-digit
45
+ # MSRP. The type is "0" for British Pound units, "5" for US
46
+ # Dollar units, and 9 for extra information. A code of
47
+ # "90000" means "no MSRP", "99991" indicates a complimentary
48
+ # copy, "99990" is used to mark used books (by college
49
+ # bookstores), and "90001" through "98999" are used internally
50
+ # by some publishers.
51
+ #
52
+ #== Rendering
53
+ #
54
+ # The 5-digit supplement is positioned to the right of the
55
+ # main UPC code, and the human-readable digits are usually
56
+ # printed above the supplemental barcode.
57
+ #
58
+ # A UPC-A is generally rendered at one inch across, then
59
+ # there's a 1/8th inch gap, then the supplemental. A UPC-A is
60
+ # 95 units wide, so the gap is 24 units wide. The 5-digit
61
+ # supplemental barcode is 47 units wide, essentially half an
62
+ # inch at this scale. Note that regardless of scale, the gap
63
+ # should be at least the smaller of 1/8th inch or 10 units.
64
+
65
+
66
+ class UPC_Supplemental_5 < Barcode1D
67
+
68
+ LEFT_PATTERNS = UPC_A::LEFT_PATTERNS
69
+ LEFT_PATTERNS_RLE = UPC_A::LEFT_PATTERNS_RLE
70
+
71
+ # parity patterns, essentially binary counting where "e" is "1"
72
+ # and "o" is "0"
73
+ PARITY_PATTERNS = {
74
+ '0' => 'eeooo',
75
+ '1' => 'eoeoo',
76
+ '2' => 'eooeo',
77
+ '3' => 'eoooe',
78
+ '4' => 'oeeoo',
79
+ '5' => 'ooeeo',
80
+ '6' => 'oooee',
81
+ '7' => 'oeoeo',
82
+ '8' => 'oeooe',
83
+ '9' => 'ooeoe'
84
+ };
85
+
86
+ LEFT_GUARD_PATTERN = '1011'
87
+ MIDDLE_GUARD_PATTERN = '01'
88
+ LEFT_GUARD_PATTERN_RLE = '112'
89
+ MIDDLE_GUARD_PATTERN_RLE = '11'
90
+
91
+ DEFAULT_OPTIONS = {
92
+ :line_character => '1',
93
+ :space_character => '0'
94
+ }
95
+
96
+ attr_reader :currency_code
97
+ attr_reader :price
98
+
99
+ class << self
100
+ # Returns true or false - must be 5 or 6 digits. This
101
+ # also handles the case where the leading 0 is added.
102
+ def can_encode?(value, options = nil)
103
+ if !options
104
+ value.to_s =~ /^\d{5,6}$/
105
+ elsif (options[:checksum_included])
106
+ value.to_s =~ /^\d{6}$/
107
+ else
108
+ value.to_s =~ /^\d{5}$/
109
+ end
110
+ end
111
+
112
+ # Generates check digit given a string to encode. It assumes there
113
+ # is no check digit on the "value".
114
+ def generate_check_digit_for(value)
115
+ mult = 9 # alternates 3 and 9
116
+ sprintf('%05d',value.to_i).reverse.chars.inject(0) { |a,c| mult = 12 - mult; a + c.to_i * mult } % 10
117
+ end
118
+
119
+ # validates the check digit given a string - assumes check digit
120
+ # is last digit of string.
121
+ def validate_check_digit_for(value)
122
+ raise UnencodableCharactersError unless self.can_encode?(value, :checksum_included => true)
123
+ md = value.match(/^(\d{5})(\d)$/)
124
+ self.generate_check_digit_for(md[1]) == md[2].to_i
125
+ end
126
+
127
+ def decode(str)
128
+ if str.length == 47
129
+ # bar pattern
130
+ str = bars_to_rle(str)
131
+ elsif str.length == 31 && str =~ /^[1-9]+$/
132
+ # rle
133
+ else
134
+ raise UnencodableCharactersError, "Pattern must be 47 unit bar pattern or 31 character rle."
135
+ end
136
+
137
+ # This string is "aaabbbb(ccdddd)" where "aaa" is the left
138
+ # guard pattern, "bbbb" is the first digit, "cc" is the
139
+ # intra-digit guard pattern, and "dddd" is the second
140
+ # digit. (ccdddd) occurs 4 times.
141
+
142
+ # See if the string is reversed
143
+ if str[28..30] == LEFT_GUARD_PATTERN_RLE.reverse && [7,13,19,25].all? { |x| str[29-x,2] == MIDDLE_GUARD_PATTERN_RLE.reverse }
144
+ str.reverse!
145
+ end
146
+
147
+ # Check the guard patterns
148
+ unless (str[0..2] == LEFT_GUARD_PATTERN_RLE && [7,13,19,25].all? { |x| str[x,2] == MIDDLE_GUARD_PATTERN_RLE.reverse })
149
+ raise UnencodableCharactersError, "Missing or incorrect guard patterns"
150
+ end
151
+
152
+ parity_sequence = ''
153
+ digits = ''
154
+ left_initial_offset = LEFT_GUARD_PATTERN_RLE.length
155
+
156
+ # Decode
157
+ (0..4).each do |left_offset|
158
+ found = false
159
+ digit_rle = str[(left_initial_offset + left_offset*6),4]
160
+ ['o','e'].each do |parity|
161
+ ('0'..'9').each do |digit|
162
+ if LEFT_PATTERNS_RLE[digit][parity] == digit_rle
163
+ parity_sequence += parity
164
+ digits += digit
165
+ found = true
166
+ break
167
+ end
168
+ end
169
+ end
170
+ raise UndecodableCharactersError, "Invalid sequence: #{digit_rle}" unless found
171
+ end
172
+
173
+ # Now, find the parity digit
174
+ parity_digit = nil
175
+ ('0'..'9').each do |x|
176
+ if PARITY_PATTERNS[x] == parity_sequence
177
+ parity_digit = x
178
+ break
179
+ end
180
+ end
181
+
182
+ raise UndecodableCharactersError, "Weird parity: #{parity_sequence}" unless parity_digit
183
+
184
+ UPC_Supplemental_5.new(digits + parity_digit, :checksum_included => true)
185
+ end
186
+
187
+ end
188
+
189
+ # Options are :line_character, :space_character, and
190
+ # :checksum_included.
191
+ def initialize(value, options = {})
192
+
193
+ @options = DEFAULT_OPTIONS.merge(options)
194
+
195
+ # Can we encode this value?
196
+ raise UnencodableCharactersError unless self.class.can_encode?(value, @options)
197
+
198
+ if @options[:checksum_included]
199
+ @encoded_string = value.to_s
200
+ raise ChecksumError unless self.class.validate_check_digit_for(@encoded_string)
201
+ md = @encoded_string.match(/^(\d+?)(\d)$/)
202
+ @value, @check_digit = md[1], md[2].to_i
203
+ else
204
+ # need to add a checksum
205
+ @value = value.to_s
206
+ @check_digit = self.class.generate_check_digit_for(@value)
207
+ @encoded_string = sprintf('%05d%1d',@value.to_i,@check_digit)
208
+ end
209
+
210
+ md = @value.match(/^(\d)(\d{4})/)
211
+ @currency_code, @price = md[1], md[2]
212
+ end
213
+
214
+ # not usable with EAN-style codes
215
+ def wn
216
+ raise NotImplementedError
217
+ end
218
+
219
+ # returns a run-length-encoded string representation
220
+ def rle
221
+ if @rle
222
+ @rle
223
+ else
224
+ md = @encoded_string.match(/(\d{5})(\d)$/)
225
+ @rle = gen_rle(md[1], md[2])
226
+ end
227
+ end
228
+
229
+ # returns 1s and 0s (for "black" and "white")
230
+ def bars
231
+ @bars ||= self.class.rle_to_bars(self.rle, @options)
232
+ end
233
+
234
+ # returns the total unit width of the bar code
235
+ def width
236
+ @width ||= rle.split('').inject(0) { |a,c| a + c.to_i }
237
+ end
238
+
239
+ private
240
+
241
+ def gen_rle(payload, parity_digit)
242
+ LEFT_GUARD_PATTERN_RLE + (0..4).collect { |n| LEFT_PATTERNS_RLE[payload[n,1]][PARITY_PATTERNS[parity_digit][n,1]] }.join(MIDDLE_GUARD_PATTERN_RLE)
243
+ end
244
+
245
+ end
246
+ end
@@ -0,0 +1,56 @@
1
+ #--
2
+ # Copyright 2012 Michael Chaney Consulting Corporation
3
+ #
4
+ # Released under the terms of the MIT License or the GNU
5
+ # General Public License, v. 2
6
+ #++
7
+
8
+ require 'test/unit'
9
+ require 'barcode1dtools'
10
+
11
+ class Barcode1DToolsTest < Test::Unit::TestCase
12
+ def setup
13
+ @options = {}
14
+ end
15
+
16
+ def teardown
17
+ end
18
+
19
+ def test_rle_to_bars
20
+ assert_equal '111001', Barcode1DTools::Barcode1D.rle_to_bars('321', @options)
21
+ assert_equal '10011100001', Barcode1DTools::Barcode1D.rle_to_bars('12341', @options)
22
+ end
23
+
24
+ def test_bars_to_rle
25
+ assert_equal '321', Barcode1DTools::Barcode1D.bars_to_rle('111001', @options)
26
+ assert_equal '12341', Barcode1DTools::Barcode1D.bars_to_rle('10011100001', @options)
27
+ end
28
+
29
+ def test_back_and_forth
30
+ random_rle = (0..19).collect { |x| (rand * 4 + 1).to_int.to_s }.join
31
+ random_bars = '1' + (0..((rand(25)+5)*2)).collect { |x| (x.odd? ? '1' : '0')*(rand(5)+1) }.join + '1'
32
+ random_wn = (0..19).collect { |x| rand < 0.5 ? 'w' : 'n' }.join
33
+ assert_equal random_rle, Barcode1DTools::Barcode1D.bars_to_rle(Barcode1DTools::Barcode1D.rle_to_bars(random_rle, @options), @options)
34
+ assert_equal random_bars, Barcode1DTools::Barcode1D.rle_to_bars(Barcode1DTools::Barcode1D.bars_to_rle(random_bars, @options), @options)
35
+ assert_equal random_wn, Barcode1DTools::Barcode1D.rle_to_wn(Barcode1DTools::Barcode1D.wn_to_rle(random_wn, @options), @options)
36
+ end
37
+
38
+ def test_bar_pair
39
+ assert_equal '01', Barcode1DTools::Barcode1D.bar_pair
40
+ assert_equal '56', Barcode1DTools::Barcode1D.bar_pair({ :space_character => 5, :line_character => 6 })
41
+ end
42
+
43
+ def test_wn_to_rle
44
+ assert_equal '12121211', Barcode1DTools::Barcode1D.wn_to_rle('nwnwnwnn')
45
+ end
46
+
47
+ def test_rle_to_wn
48
+ assert_equal 'wnwnwnww', Barcode1DTools::Barcode1D.rle_to_wn('21212122')
49
+ assert_equal 'wnwnwnww', Barcode1DTools::Barcode1D.rle_to_wn('31313133')
50
+ end
51
+
52
+ def test_wn_pair
53
+ assert_equal 'wn', Barcode1DTools::Barcode1D.wn_pair
54
+ assert_equal '65', Barcode1DTools::Barcode1D.wn_pair({ :n_character => '5', :w_character => '6' })
55
+ end
56
+ end
@@ -0,0 +1,102 @@
1
+ #--
2
+ # Copyright 2012 Michael Chaney Consulting Corporation
3
+ #
4
+ # Released under the terms of the MIT License or the GNU
5
+ # General Public License, v. 2
6
+ #++
7
+
8
+ require 'test/unit'
9
+ require 'barcode1dtools'
10
+
11
+ class Barcode1DToolsEAN13Test < Test::Unit::TestCase
12
+ def setup
13
+ end
14
+
15
+ def teardown
16
+ end
17
+
18
+ # Creates a random EAN-13 sans checksum
19
+ def random_12_digit_number
20
+ (0..11).collect { |x| ((rand * 10).to_i % 10).to_s }.join
21
+ end
22
+
23
+ def test_checksum_generation
24
+ assert_equal 7, Barcode1DTools::EAN13.generate_check_digit_for('007820601001')
25
+ end
26
+
27
+ def test_checksum_validation
28
+ assert Barcode1DTools::EAN13.validate_check_digit_for('0884088516338')
29
+ end
30
+
31
+ def test_attr_readers
32
+ ean = Barcode1DTools::EAN13.new('088408851633', :checksum_included => false)
33
+ assert_equal 8, ean.check_digit
34
+ assert_equal '088408851633', ean.value
35
+ assert_equal '0884088516338', ean.encoded_string
36
+ assert_equal '08', ean.number_system
37
+ assert_equal '84088', ean.manufacturers_code
38
+ assert_equal '51633', ean.product_code
39
+ end
40
+
41
+ def test_value_fixup
42
+ ean = Barcode1DTools::EAN13.new('088408851633', :checksum_included => false)
43
+ assert_equal 8, ean.check_digit
44
+ assert_equal '088408851633', ean.value
45
+ assert_equal '0884088516338', ean.encoded_string
46
+ end
47
+
48
+ def test_checksum_error
49
+ # proper checksum is 8
50
+ assert_raise(Barcode1DTools::ChecksumError) { Barcode1DTools::EAN13.new('0884088516331', :checksum_included => true) }
51
+ end
52
+
53
+ def test_value_length_errors
54
+ # One digit too short
55
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.new('01234567890', :checksum_included => false) }
56
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.new('012345678901', :checksum_included => true) }
57
+ # One digit too long
58
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.new('0123456789012', :checksum_included => false) }
59
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.new('01234567890123', :checksum_included => true) }
60
+ end
61
+
62
+ def test_bad_character_errors
63
+ # Characters that cannot be encoded
64
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.new('thisisnotgood', :checksum_included => false) }
65
+ end
66
+
67
+ def test_width
68
+ ean = Barcode1DTools::EAN13.new('0041343005796', :checksum_included => true)
69
+ assert_equal 95, ean.width
70
+ end
71
+
72
+ def test_barcode_generation
73
+ ean = Barcode1DTools::EAN13.new('0012676510226', :checksum_included => true)
74
+ assert_equal "10100011010011001001001101011110111011010111101010100111011001101110010110110011011001010000101", ean.bars
75
+ assert_equal "11132112221212211141312111411111123122213211212221221114111", ean.rle
76
+ end
77
+
78
+ def test_wn_raises_error
79
+ ean = Barcode1DTools::EAN13.new('0012676510226', :checksum_included => true)
80
+ assert_raise(Barcode1DTools::NotImplementedError) { ean.wn }
81
+ end
82
+
83
+ def test_decode_error
84
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.decode('x') }
85
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.decode('x'*60) }
86
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.decode('x'*96) }
87
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.decode('x'*94) }
88
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.decode('111000011111000011111') }
89
+ end
90
+
91
+ def test_decoding
92
+ random_ean_num=random_12_digit_number
93
+ ean = Barcode1DTools::EAN13.new(random_ean_num)
94
+ ean2 = Barcode1DTools::EAN13.decode(ean.bars)
95
+ assert_equal ean.value, ean2.value
96
+ ean3 = Barcode1DTools::EAN13.decode(ean.rle)
97
+ assert_equal ean.value, ean3.value
98
+ # Should also work in reverse
99
+ ean4 = Barcode1DTools::EAN13.decode(ean.bars.reverse)
100
+ assert_equal ean.value, ean4.value
101
+ end
102
+ end
@@ -0,0 +1,95 @@
1
+ #--
2
+ # Copyright 2012 Michael Chaney Consulting Corporation
3
+ #
4
+ # Released under the terms of the MIT License or the GNU
5
+ # General Public License, v. 2
6
+ #++
7
+
8
+ require 'test/unit'
9
+ require 'barcode1dtools'
10
+
11
+ class Barcode1DToolsEAN8Test < Test::Unit::TestCase
12
+ def setup
13
+ end
14
+
15
+ def teardown
16
+ end
17
+
18
+ # Creates a random EAN-8 sans checksum
19
+ def random_7_digit_number
20
+ sprintf('%07d',rand(10000000))
21
+ end
22
+
23
+ def test_checksum_generation
24
+ assert_equal 4, Barcode1DTools::EAN8.generate_check_digit_for('9638507')
25
+ end
26
+
27
+ def test_checksum_validation
28
+ assert Barcode1DTools::EAN8.validate_check_digit_for('96385074')
29
+ end
30
+
31
+ def test_attr_readers
32
+ ean = Barcode1DTools::EAN8.new('9638507', :checksum_included => false)
33
+ assert_equal 4, ean.check_digit
34
+ assert_equal '9638507', ean.value
35
+ assert_equal '96385074', ean.encoded_string
36
+ assert_equal '963', ean.number_system
37
+ assert_equal '8507', ean.product_code
38
+ end
39
+
40
+ def test_checksum_error
41
+ # proper checksum is 4
42
+ assert_raise(Barcode1DTools::ChecksumError) { Barcode1DTools::EAN8.new('96385070', :checksum_included => true) }
43
+ end
44
+
45
+ def test_value_length_errors
46
+ # One digit too short
47
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.new('123456', :checksum_included => false) }
48
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.new('1234567', :checksum_included => true) }
49
+ # One digit too long
50
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.new('12345678', :checksum_included => false) }
51
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.new('123456789', :checksum_included => true) }
52
+ end
53
+
54
+ def test_bad_character_errors
55
+ # Characters that cannot be encoded
56
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.new('thisisnotgood', :checksum_included => false) }
57
+ end
58
+
59
+ def test_width
60
+ ean = Barcode1DTools::EAN8.new(random_7_digit_number)
61
+ assert_equal 67, ean.width
62
+ end
63
+
64
+ def test_barcode_generation
65
+ ean = Barcode1DTools::EAN8.new('96385074', :checksum_included => true)
66
+ assert_equal "1010001011010111101111010110111010101001110111001010001001011100101", ean.bars
67
+
68
+ assert_equal "1113112111414111213111111231321113121132111", ean.rle
69
+ end
70
+
71
+ def test_wn_raises_error
72
+ ean = Barcode1DTools::EAN8.new(random_7_digit_number)
73
+ assert_raise(Barcode1DTools::NotImplementedError) { ean.wn }
74
+ end
75
+
76
+ def test_decode_error
77
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.decode('x') }
78
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.decode('x'*60) }
79
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.decode('x'*96) }
80
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.decode('x'*94) }
81
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.decode('111000011111000011111') }
82
+ end
83
+
84
+ def test_decoding
85
+ random_ean_num=random_7_digit_number
86
+ ean = Barcode1DTools::EAN8.new(random_ean_num)
87
+ ean2 = Barcode1DTools::EAN8.decode(ean.bars)
88
+ assert_equal ean.value, ean2.value
89
+ ean3 = Barcode1DTools::EAN8.decode(ean.rle)
90
+ assert_equal ean.value, ean3.value
91
+ # Should also work in reverse
92
+ ean4 = Barcode1DTools::EAN8.decode(ean.bars.reverse)
93
+ assert_equal ean.value, ean4.value
94
+ end
95
+ end