tracking_number 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/lib/tracking_number.rb +134 -127
- data/test/test_helper.rb +10 -0
- data/test/test_tracking_number.rb +78 -43
- data/tracking_number.gemspec +2 -2
- metadata +4 -4
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/lib/tracking_number.rb
CHANGED
@@ -5,12 +5,12 @@
|
|
5
5
|
|
6
6
|
class TrackingNumber
|
7
7
|
SEARCH_PATTERNS = {
|
8
|
-
:ups =>
|
9
|
-
:fedex_express =>
|
10
|
-
:fedex_ground_96 =>
|
11
|
-
:fedex_ground_sscc18 =>
|
12
|
-
:dhl =>
|
13
|
-
:usps =>
|
8
|
+
:ups => /(\b1\s*Z\s*(\w\s*){16,16}\b)/,
|
9
|
+
:fedex_express => /(\b([0-9]\s*){12,12}\b)/,
|
10
|
+
:fedex_ground_96 => /(\b9\s*6\s*([0-9]\s*){20,20}\b)/,
|
11
|
+
:fedex_ground_sscc18 => /(\b([0-9]\s*){18,18}\b)/,
|
12
|
+
:dhl => /(\b([0-9]\s*){11,11}\b)/,
|
13
|
+
:usps => /(\b([0-9]\s*){22,22}\b)/
|
14
14
|
}
|
15
15
|
|
16
16
|
VERIFY_PATTERNS = {
|
@@ -22,151 +22,158 @@ class TrackingNumber
|
|
22
22
|
:usps => /^([0-9]{21,21})([0-9])/
|
23
23
|
}
|
24
24
|
|
25
|
+
def self.scan(body)
|
26
|
+
possibles = SEARCH_PATTERNS.values.collect { |pattern|
|
27
|
+
body.scan(pattern)
|
28
|
+
}.uniq.flatten
|
29
|
+
end
|
30
|
+
|
25
31
|
def self.search(body)
|
26
|
-
|
27
|
-
body.scan(pattern)
|
28
|
-
}.uniq.flatten
|
29
|
-
|
30
|
-
possibles.collect { |possible| TrackingNumber.new(possible) }.select { |t| t.valid? }
|
32
|
+
self.scan(body).uniq.collect { |possible| TrackingNumber.new(possible) }.select { |t| t.valid? }
|
31
33
|
end
|
32
34
|
|
33
35
|
def initialize(tracking_number)
|
34
|
-
|
35
|
-
|
36
|
+
@original_number = tracking_number
|
37
|
+
@tracking_number = tracking_number.to_s.upcase.gsub(/\s/,"")
|
38
|
+
end
|
36
39
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
+
def valid?
|
41
|
+
carrier != :unknown
|
42
|
+
end
|
40
43
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
end
|
44
|
+
def carrier
|
45
|
+
@carrier = if ups?(@tracking_number)
|
46
|
+
:ups
|
47
|
+
elsif fedex_express?(@tracking_number)
|
48
|
+
:fedex
|
49
|
+
elsif fedex_ground_96?(@tracking_number)
|
50
|
+
:fedex
|
51
|
+
elsif fedex_ground_sscc18?(@tracking_number)
|
52
|
+
:fedex
|
53
|
+
elsif usps?(@tracking_number)
|
54
|
+
:usps
|
55
|
+
elsif dhl?(@tracking_number)
|
56
|
+
:dhl
|
57
|
+
else
|
58
|
+
:unknown
|
57
59
|
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def original_number
|
63
|
+
@original_number
|
64
|
+
end
|
65
|
+
|
66
|
+
def tracking_number
|
67
|
+
@tracking_number
|
68
|
+
end
|
58
69
|
|
59
|
-
|
60
|
-
|
61
|
-
|
70
|
+
def to_s
|
71
|
+
@tracking_number
|
72
|
+
end
|
62
73
|
|
63
|
-
|
64
|
-
|
65
|
-
|
74
|
+
private
|
75
|
+
|
76
|
+
def ups?(tracking_number)
|
77
|
+
results = tracking_number.scan(VERIFY_PATTERNS[:ups])
|
78
|
+
return false if results.nil? || results.empty?
|
66
79
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
total = 0
|
75
|
-
sequence.chars.each_with_index do |c, i|
|
76
|
-
x = if c[/[0-9]/] # numeric
|
77
|
-
c.to_i
|
78
|
-
else
|
79
|
-
(c[0].ord - 3) % 10
|
80
|
-
end
|
81
|
-
x *= 2 if i.odd?
|
82
|
-
total += x
|
80
|
+
sequence, check_digit = results.flatten
|
81
|
+
total = 0
|
82
|
+
sequence.chars.each_with_index do |c, i|
|
83
|
+
x = if c[/[0-9]/] # numeric
|
84
|
+
c.to_i
|
85
|
+
else
|
86
|
+
(c[0].ord - 3) % 10
|
83
87
|
end
|
88
|
+
x *= 2 if i.odd?
|
89
|
+
total += x
|
90
|
+
end
|
84
91
|
|
85
|
-
|
86
|
-
|
92
|
+
check = (total % 10)
|
93
|
+
check = (10 - check) unless (check.zero?)
|
87
94
|
|
88
|
-
|
89
|
-
|
95
|
+
return true if (check.to_i == check_digit.to_i)
|
96
|
+
end
|
90
97
|
|
91
98
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
99
|
+
def fedex_express?(tracking_number)
|
100
|
+
results = tracking_number.scan(VERIFY_PATTERNS[:fedex_express]).flatten
|
101
|
+
return false if results.nil? || results.empty?
|
102
|
+
sequence, check = results
|
96
103
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
104
|
+
sequence = sequence.chars.to_a.map(&:to_i)
|
105
|
+
total = 0
|
106
|
+
sequence.zip([3,1,7,3,1,7,3,1,7,3,1]).collect { |pair| pair[0] * pair[1] }.each { |t| total += t.to_i }
|
107
|
+
return true if (total % 11) == check.to_i
|
108
|
+
end
|
102
109
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
end
|
123
|
-
check = total % 10
|
124
|
-
check = (10 - check) unless (check.zero?)
|
125
|
-
return true if check == check_digit.to_i
|
110
|
+
def fedex_ground_96?(tracking_number)
|
111
|
+
# 22 numbers
|
112
|
+
# http://fedex.com/us/solutions/ppe/FedEx_Ground_Label_Layout_Specification.pdf
|
113
|
+
# 96 - UCC/EAN Application Identifier
|
114
|
+
# [0-9]{2,2} - SCNC
|
115
|
+
# [0-9]{3,3} - Class Of Service
|
116
|
+
|
117
|
+
# [0-9]{7,7} - RPS Shipper ID (used in calculation)
|
118
|
+
# [0-9]{7,7} - Package Number (used in calculation)
|
119
|
+
|
120
|
+
# [0-9] - Check Digit
|
121
|
+
results = tracking_number.scan(VERIFY_PATTERNS[:fedex_ground_96]).flatten
|
122
|
+
return false if results.nil? || results.empty?
|
123
|
+
|
124
|
+
sequence, check_digit = results.flatten
|
125
|
+
total = 0
|
126
|
+
sequence.chars.to_a.reverse.map(&:to_i).each_with_index do |x, i|
|
127
|
+
x *= 3 if i.even?
|
128
|
+
total += x
|
126
129
|
end
|
130
|
+
check = total % 10
|
131
|
+
check = (10 - check) unless (check.zero?)
|
132
|
+
return true if check == check_digit.to_i
|
133
|
+
end
|
127
134
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
end
|
139
|
-
|
140
|
-
check = total % 10
|
141
|
-
check = (10 - check) unless (check.zero?)
|
142
|
-
return true if check == check_digit.to_i
|
135
|
+
def fedex_ground_sscc18?(tracking_number)
|
136
|
+
# [0-9]{2,2} - Not used
|
137
|
+
# [0-9]{15, 15} - used for calculation
|
138
|
+
results = tracking_number.scan(VERIFY_PATTERNS[:fedex_ground_sscc18]).flatten
|
139
|
+
return false if results.nil? || results.empty?
|
140
|
+
sequence, check_digit = results
|
141
|
+
total = 0
|
142
|
+
sequence.chars.to_a.map(&:to_i).reverse.each_with_index do |x, i|
|
143
|
+
x *= 3 if i.even?
|
144
|
+
total += x
|
143
145
|
end
|
144
146
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
147
|
+
check = total % 10
|
148
|
+
check = (10 - check) unless (check.zero?)
|
149
|
+
return true if check == check_digit.to_i
|
150
|
+
end
|
149
151
|
|
150
|
-
|
151
|
-
|
152
|
-
|
152
|
+
def dhl?(tracking_number)
|
153
|
+
# standard mod 7 check
|
154
|
+
results = tracking_number.scan(VERIFY_PATTERNS[:dhl]).flatten
|
155
|
+
return false if results.nil? || results.empty?
|
153
156
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
return false if results.nil? || results.empty?
|
157
|
+
sequence, check_digit = results
|
158
|
+
return true if sequence.to_i % 7 == check_digit.to_i
|
159
|
+
end
|
158
160
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
x *= 3 if i.even?
|
161
|
+
def usps?(tracking_number)
|
162
|
+
# standard mod 10 check
|
163
|
+
results = tracking_number.scan(VERIFY_PATTERNS[:usps]).flatten
|
164
|
+
return false if results.nil? || results.empty?
|
164
165
|
|
165
|
-
|
166
|
-
|
166
|
+
sequence, check_digit = results
|
167
|
+
total = 0
|
168
|
+
sequence.chars.to_a.reverse.each_with_index do |c, i|
|
169
|
+
x = c.to_i
|
170
|
+
x *= 3 if i.even?
|
167
171
|
|
168
|
-
|
169
|
-
check = 10 - check unless (check.zero?)
|
170
|
-
return true if check == check_digit.to_i
|
171
|
-
end
|
172
|
+
total += x
|
172
173
|
end
|
174
|
+
|
175
|
+
check = total % 10
|
176
|
+
check = 10 - check unless (check.zero?)
|
177
|
+
return true if check == check_digit.to_i
|
178
|
+
end
|
179
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -15,4 +15,14 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
15
15
|
require 'tracking_number'
|
16
16
|
|
17
17
|
class Test::Unit::TestCase
|
18
|
+
def possible_numbers(tracking)
|
19
|
+
possible_numbers = []
|
20
|
+
possible_numbers << tracking
|
21
|
+
possible_numbers << tracking.chars.to_a.join(" ")
|
22
|
+
possible_numbers << tracking.chars.to_a.join(" ")
|
23
|
+
possible_numbers << tracking.slice(0, (tracking.length / 2)) + " " + tracking.slice((tracking.length / 2), tracking.length)
|
24
|
+
|
25
|
+
possible_numbers
|
26
|
+
end
|
27
|
+
|
18
28
|
end
|
@@ -1,61 +1,96 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
-
class TrackingNumberTest < Test::Unit::TestCase
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
class TrackingNumberTest < Test::Unit::TestCase
|
4
|
+
context "a UPS tracking number" do
|
5
|
+
should "return ups as the carrier" do
|
6
|
+
assert_equal :ups, TrackingNumber.new("1Z5R89390357567127").carrier
|
7
|
+
assert_equal :ups, TrackingNumber.new("1Z879E930346834440").carrier
|
8
|
+
assert_equal :ups, TrackingNumber.new("1Z410E7W0392751591").carrier
|
9
|
+
assert_equal :ups, TrackingNumber.new("1Z8V92A70367203024").carrier
|
10
|
+
end
|
9
11
|
end
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
|
13
|
+
context "a FedEx tracking number" do
|
14
|
+
should "return fedex as the carrier if it's a valid fedex express number" do
|
15
|
+
t = TrackingNumber.new("986578788855")
|
16
|
+
assert_equal :fedex, t.carrier
|
17
|
+
assert t.valid?
|
15
18
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
19
|
+
t = TrackingNumber.new("477179081230")
|
20
|
+
assert_equal :fedex, t.carrier
|
21
|
+
assert t.valid?
|
22
|
+
end
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
should "return fedex as the carrier with valid fedex ground (96) number" do
|
25
|
+
t = TrackingNumber.new("9611020987654312345672")
|
26
|
+
assert_equal :fedex, t.carrier
|
27
|
+
assert t.valid?
|
28
|
+
end
|
26
29
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
30
|
+
should "return fedex as the carrier with valid fedex ground (SSCC18) number" do
|
31
|
+
t = TrackingNumber.new("000123450000000027")
|
32
|
+
assert_equal :fedex, t.carrier
|
33
|
+
assert t.valid?
|
34
|
+
end
|
31
35
|
end
|
32
36
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
+
context "a DHL tracking number" do
|
38
|
+
should "return dhl if it's a valid number" do
|
39
|
+
t = TrackingNumber.new("73891051146")
|
40
|
+
assert_equal :dhl, t.carrier
|
41
|
+
assert t.valid?
|
42
|
+
end
|
37
43
|
end
|
38
44
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
45
|
+
context "a USPS tracking number" do
|
46
|
+
should "return usps with if number" do
|
47
|
+
t = TrackingNumber.new("9101 1234 5678 9000 0000 13")
|
48
|
+
assert_equal :usps, t.carrier
|
49
|
+
assert t.valid?
|
50
|
+
end
|
43
51
|
end
|
44
52
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
53
|
+
context "a tracking number" do
|
54
|
+
should "return unknown when given invalid number" do
|
55
|
+
t = TrackingNumber.new("101")
|
56
|
+
assert_equal :unknown, t.carrier
|
57
|
+
assert !t.valid?
|
58
|
+
end
|
50
59
|
|
51
|
-
|
52
|
-
|
53
|
-
|
60
|
+
should "upcase and remove spaces from tracking number" do
|
61
|
+
t = TrackingNumber.new("abc 123 def")
|
62
|
+
assert_equal "ABC123DEF", t.tracking_number
|
63
|
+
end
|
54
64
|
end
|
55
65
|
|
56
|
-
|
57
|
-
|
66
|
+
context "tracking number search" do
|
67
|
+
[{"73891051146" => :dhl},
|
68
|
+
{"1Z8V92A70367203024" => :ups},
|
69
|
+
{"000123450000000027" => :fedex}, {"9611020987654312345672" => :fedex}, {"477179081230" => :fedex},
|
70
|
+
{"9101123456789000000013" => :usps}].each do |pair|
|
71
|
+
expected_service = pair.to_a.flatten[1]
|
72
|
+
tracking = pair.to_a.flatten[0]
|
73
|
+
|
74
|
+
should "detect #{expected_service} with number #{tracking} regardless of spacing" do
|
75
|
+
possible_numbers(tracking).each do |spaced_tracking|
|
76
|
+
results = TrackingNumber.search("blah blah #{spaced_tracking} blah blah")
|
77
|
+
assert_equal 1, results.size, "#{spaced_tracking} did not match #{expected_service}"
|
78
|
+
assert_equal expected_service, results.first.carrier
|
79
|
+
end
|
80
|
+
end
|
58
81
|
|
59
|
-
|
82
|
+
should "not detect #{expected_service} when word word boundaries are not in tact" do
|
83
|
+
possible_numbers(tracking).each do |spaced_tracking|
|
84
|
+
bad_number = "x1#{spaced_tracking}1x"
|
85
|
+
results = TrackingNumber.search("blah blah #{bad_number} blah blah")
|
86
|
+
assert_equal 0, results.size, "#{bad_number} should not match #{expected_service}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
should "return two tracking numbers when given string with two" do
|
91
|
+
s = TrackingNumber.search("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 1Z879E930346834440 nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute 9611020987654312345672 dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
|
92
|
+
assert_equal 2, s.size
|
93
|
+
end
|
94
|
+
end
|
60
95
|
end
|
61
96
|
end
|
data/tracking_number.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{tracking_number}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Jeff Keen"]
|
12
|
-
s.date = %q{
|
12
|
+
s.date = %q{2011-01-29}
|
13
13
|
s.description = %q{Match tracking numbers to a service, and search blocks of text and pull out valid tracking numbers.}
|
14
14
|
s.email = %q{jeff@keen.me}
|
15
15
|
s.extra_rdoc_files = [
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
7
|
+
- 2
|
8
8
|
- 0
|
9
|
-
version: 0.
|
9
|
+
version: 0.2.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Jeff Keen
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date:
|
17
|
+
date: 2011-01-29 00:00:00 -06:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -108,7 +108,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
108
|
requirements:
|
109
109
|
- - ">="
|
110
110
|
- !ruby/object:Gem::Version
|
111
|
-
hash:
|
111
|
+
hash: -1308548554367875831
|
112
112
|
segments:
|
113
113
|
- 0
|
114
114
|
version: "0"
|