tracking_number 0.10.5 → 1.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: '0361787dd819144799899504060b5fad1c152fd8'
4
- data.tar.gz: 93e3270a902aef5ac81580c5add4c7f94872d9ac
2
+ SHA256:
3
+ metadata.gz: 7732c76f5896712c793106dbc515f67107764228741950d6eff1a73c0daeef9c
4
+ data.tar.gz: 62d14ce8cc6a9899f01963b28f0234872eba8b0a9b0d5334adb1a4d3d1c23ad0
5
5
  SHA512:
6
- metadata.gz: 771dd5aa40219a4f6386a2c73ddfda1b10e0a1230ea1b672efc4af8c0d41472080ae6a036f4c90c99384e93024feec8abc9f831b6c593d8ff2da2301690fc51d
7
- data.tar.gz: a30fbe05e145adf611bb8a13b375246926c36dd81509d47210f05c227a637e0f7daffe748df739eb692adeb6ef82713a4cf57ae8d45d30770ebec8cc1f02a10e
6
+ metadata.gz: cdfcbdbff995f2a51f4f2e3896d7abfcc49277ec4b6642a319f89dc8d4470f62903ddbf8fa0118b9c49709494976f88b5133a286362c08ec5421468b8fdfc4fb
7
+ data.tar.gz: e9410104da284a7a8dd71bb64ea1102f1aae30b504c8d4c92817110b856f40e2e327bd788f651e83f8e3c263ee00916470c7c2f14d8afa6451189a1e23c54ba5
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "lib/data"]
2
+ path = lib/data
3
+ url = https://github.com/jkeen/tracking_number_data
data/.travis.yml CHANGED
@@ -2,11 +2,19 @@ language: ruby
2
2
  sudo: false
3
3
  cache: bundler
4
4
  rvm:
5
- - "1.9.3"
6
- - "2.0.0"
7
- - "2.1.10"
8
- - "2.2.7"
9
- - "2.3.4"
10
- - "2.4.1"
5
+ - 2.0.0
6
+ - 2.1.10
7
+ - 2.2.7
8
+ - 2.3.4
9
+ - 2.4.1
11
10
  before_install:
12
- - travis_retry gem update --system
11
+ - travis_retry gem update --system
12
+ deploy:
13
+ provider: rubygems
14
+ api_key:
15
+ secure: AkaGJCc8pCkVY5IPVa3dwvS4wvPlMHHkyqrIpi8G5nFiDFjtHjWY7tDUfFCxMrF7WNXWVNg/bgkzwfvoAZcvGVU9grzCDqTC9uvsF5V3hB5FikyQ6h++mJRCiYzfLbjzyAT3FAuB49dXYeZR/eOKPPqn9jMAQPyHd6+PAGE3BeI=
16
+ gem: tracking_number
17
+ on:
18
+ tags: true
19
+ repo: jkeen/tracking_number
20
+
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source "http://rubygems.org"
2
2
  # Specify your gem's dependencies in tracking_number.gemspec
3
3
  gemspec
4
+
5
+ gem "pry"
data/README.md CHANGED
@@ -1,35 +1,105 @@
1
- ## tracking_number
1
+ ## Tracking Number (v1.x)
2
2
 
3
- This gem identifies valid tracking numbers and the service they're associated with. It can also tell you a little bit about the package purely from the number—there's quite a bit of info tucked away into those numbers, it turns out.
3
+ This gem identifies valid tracking numbers and can tell you a little bit about the shipment just from the number—there's quite a bit of info tucked away into those numbers, it turns out.
4
+
5
+ It detects tracking numbers from UPS, FedEx, DHL, USPS, OnTrac, Amazon Logistics, and 160+ countries national postal services (S10 standard).
4
6
 
5
7
  This gem does not do tracking. That is left up to you.
6
8
 
9
+ ## Usage
10
+
11
+ #### Checking an individual tracking number
7
12
  ```ruby
8
13
  t = TrackingNumber.new("MYSTERY_TRACKING_NUMBER")
9
14
  # => #<TrackingNumber::Unknown MYSTERY_TRACKING_NUMBER>
10
-
11
15
  t.valid? #=> false
12
- t.carrier #=> :unknown
13
16
 
14
17
  t = TrackingNumber.new("1Z879E930346834440")
15
18
  # => #<TrackingNumber::UPS 1Z879E930346834440>
16
-
17
19
  t.valid? #=> true
18
- t.carrier #=> :ups
19
20
  ```
20
- Also can take a block of text and find all the valid tracking numbers within it
21
+
22
+ #### Searching a block of text
23
+ This will return valid tracking numbers contained within a block of text.
21
24
 
22
25
  ```ruby
23
- TrackingNumber.search("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
24
- tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 1Z879E930346834440
25
- nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis
26
- aute 9611020987654312345672 dolor in reprehenderit in voluptate velit esse cillum dolore eu
27
- fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
28
- officia deserunt mollit anim id est laborum.")
26
+ 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.")
29
27
 
30
28
  #=> [#<TrackingNumber::UPS 1Z879E930346834440>, #<TrackingNumber::FedExGround96 9611020987654312345672>]
31
29
  ```
32
30
 
31
+ #### Courier Info
32
+ As of 1.0, the possible courier codes are `[:usps, :fedex, :ups, :ontrac, :dhl, :amazon, :s10, :unknown]`. S10 is the international standard used by local government post offices. When packages are shipped internationally via normal post, it's usually an S10 number.
33
+
34
+ ```ruby
35
+ t = TrackingNumber.new("1Z879E930346834440")
36
+ # => #<TrackingNumber::UPS 1Z879E930346834440>
37
+
38
+ t.valid? #=> true
39
+ t.courier_code #=> :ups
40
+ t.courier_name #=> "UPS"
41
+
42
+
43
+ t = TrackingNumber.new("RB123456785GB")
44
+ t.courier_name #=> "Royal Mail Group plc"
45
+ t.courier_code #=> :s10
46
+
47
+ t = TrackingNumber.new("RB123456785US")
48
+ t.courier_name #=> "United States Postal Service"
49
+ ```
50
+
51
+ #### Service Type
52
+ Some tracking numbers indicate their service type
53
+
54
+ ```ruby
55
+ t = TrackingNumber.new("1Z879E930346834440")
56
+ t.service_type #=> "UPS United States Ground""
57
+
58
+ t = TrackingNumber.new("1ZXX3150YW44070023")
59
+ t.service_type #=> "UPS SurePost - Delivered by the USPS"
60
+
61
+ t = TrackingNumber.new("RB123456785US")
62
+ t.service_type #=> "Letter Post Registered"
63
+ ```
64
+
65
+ #### Shipper ID
66
+ Some tracking numbers indicate information about their package
67
+ ```ruby
68
+ t = TrackingNumber.new("1Z6072AF0320751583")
69
+ t.shipper_id #=> "6072AF" <-- this is Target
70
+ ```
71
+
72
+ #### Destination Zip
73
+ Some tracking numbers indicate their destination
74
+
75
+ ```ruby
76
+ t = TrackingNumber.new("1001901781990001000300617767839437")
77
+ t.destination_zip #=> "10003"
78
+ ```
79
+
80
+ #### Package Info
81
+ Some tracking numbers indicate information about their package
82
+
83
+ ```ruby
84
+ t = TrackingNumber.new("012345000000002")
85
+ t.package_type #=> "case/carton"
86
+ ```
87
+
88
+ #### Decoding
89
+ Most tracking numbers have a format where each part of the number has meaning. `decode` splits up the number into its known named parts.
90
+ ```ruby
91
+ t = TrackingNumber.new("1Z879E930346834440")
92
+ t.decode
93
+
94
+ #=> {
95
+ # :serial_number => "879E93034683444",
96
+ # :shipper_id => "879E93",
97
+ # :service_type => "03",
98
+ # :package_id => "4683444",
99
+ # :check_digit => "0"
100
+ # }
101
+ ```
102
+
33
103
  ## ActiveModel validation
34
104
 
35
105
  For Rails 3 (or any ActiveModel client), validate your fields as a tracking number:
@@ -46,8 +116,10 @@ class Shipment < ActiveRecord::Base
46
116
  end
47
117
  ```
48
118
 
49
- ## Contributing to tracking_number
119
+ ## Where the data comes from
120
+ Starting with the 1.0 release of this gem the data for tracking numbers has been extracted into a separate repository ([tracking_number_data](http://github.com/jkeen/tracking_number_data)) so non-ruby clients can benefit from the detection/documentation that used to be contained deep in the code of this gem. If you want to write a client in some other language, that's the stuff you want.
50
121
 
122
+ ## Contributing to tracking_number
51
123
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
52
124
  * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
53
125
  * Fork the project
@@ -58,5 +130,5 @@ end
58
130
 
59
131
  ## Copyright
60
132
 
61
- Copyright (c) 2010 Jeff Keen. See LICENSE.txt for
133
+ Copyright (c) 2010-2018 Jeff Keen. See LICENSE.txt for
62
134
  further details.
data/Rakefile CHANGED
@@ -3,6 +3,9 @@ require 'bundler'
3
3
  Bundler::GemHelper.install_tasks
4
4
  require 'rake'
5
5
  require 'rake/testtask'
6
+
7
+ import './lib/tasks/stats.rake'
8
+
6
9
  Rake::TestTask.new(:test) do |test|
7
10
  test.libs << 'lib' << 'test'
8
11
  test.pattern = 'test/**/*_test.rb'
@@ -0,0 +1,63 @@
1
+ module ChecksumValidations
2
+ class << self
3
+ def validates_s10?(sequence, check_digit, extras = {})
4
+ weighting = [8,6,4,2,3,5,9,7]
5
+
6
+ total = 0
7
+ sequence.chars.to_a.zip(weighting).each do |(a,b)|
8
+ total += a.to_i * b.to_i
9
+ end
10
+
11
+ remainder = total % 11
12
+ check = case remainder
13
+ when 1
14
+ 0
15
+ when 0
16
+ 5
17
+ else
18
+ 11 - remainder
19
+ end
20
+
21
+ return check.to_i == check_digit.to_i
22
+ end
23
+
24
+ def validates_sum_product_with_weightings_and_modulo?(sequence, check_digit, extras = {})
25
+ weighting = extras[:weightings] || []
26
+
27
+ total = 0
28
+ sequence.chars.to_a.zip(weighting).each do |(a,b)|
29
+ total += a.to_i * b
30
+ end
31
+ return (total % extras[:modulo1] % extras[:modulo2]) == check_digit.to_i
32
+ end
33
+
34
+ def validates_mod10?(sequence, check_digit, extras = {})
35
+ total = 0
36
+ sequence.chars.each_with_index do |c, i|
37
+ x = if c[/[0-9]/] # numeric
38
+ c.to_i
39
+ else
40
+ (c[0].ord - 3) % 10
41
+ end
42
+
43
+ if extras[:odds_multiplier] && i.odd?
44
+ x *= extras[:odds_multiplier].to_i
45
+ elsif extras[:evens_multiplier] && i.even?
46
+ x *= extras[:evens_multiplier].to_i
47
+ end
48
+
49
+ total += x
50
+ end
51
+
52
+ check = (total % 10)
53
+ check = (10 - check) unless (check.zero?)
54
+
55
+ return (check.to_i == check_digit.to_i)
56
+ end
57
+
58
+ def validates_mod7?(sequence, check_digit, extras = {})
59
+ # standard mod 7 check
60
+ return true if sequence.to_i % 7 == check_digit.to_i
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,90 @@
1
+ require 'terminal-table'
2
+
3
+ def courier_data
4
+ courier_data = Dir.glob(File.join(File.dirname(__FILE__), "../../lib/data/couriers/*.json")).collect do |file|
5
+ JSON.parse(File.read(file)).deep_symbolize_keys!
6
+ end
7
+ end
8
+
9
+ def all_number_groups
10
+ number_groups = []
11
+ courier_data.each do |courier_info|
12
+ courier_info[:tracking_numbers].each do |tracking_info|
13
+ test_number = tracking_info[:test_numbers][:valid].first
14
+ t = TrackingNumber.new(test_number)
15
+ # keys = t.decode.keys.select { |s| ![:serial_number, :check_digit].include?(s) }
16
+ number_groups << t.decode.keys
17
+ end
18
+ end
19
+ groups = number_groups.flatten.uniq.sort
20
+ end
21
+
22
+ def has_key?(tracking_numbers, key)
23
+ tracking_numbers = [tracking_numbers].flatten
24
+ tracking_numbers.first.decode[key]
25
+ end
26
+
27
+ def has_additional_key_info?(tracking_numbers, key)
28
+ tracking_numbers = [tracking_numbers].flatten
29
+
30
+ if key == :service_type
31
+ tracking_numbers.any? { |t| t.service_type }
32
+ elsif key == :shipping_container_type
33
+ tracking_numbers.any? { |t| t.package_info }
34
+ elsif key == :destination_zip
35
+ tracking_numbers.any? { |t| t.destination }
36
+ end
37
+ end
38
+
39
+
40
+ desc "show stats for tracking number types"
41
+ task :stats do
42
+ rows = []
43
+
44
+ courier_data.each do |courier_info|
45
+ courier_name = courier_info[:name]
46
+ courier_code = courier_info[:courier_code].to_sym
47
+
48
+ courier_info[:tracking_numbers].each do |tracking_info|
49
+ tracking_type = tracking_info[:name]
50
+
51
+ if tracking_type == "S10"
52
+ country_count = tracking_info[:additional].detect { |r| r[:regex_group_name] == "CountryCode" }[:lookup].size
53
+ tracking_type = "S10 (#{country_count} types)"
54
+ end
55
+
56
+ tracking_numbers = tracking_info[:test_numbers][:valid].collect { |n| TrackingNumber.new(n) }
57
+
58
+ checksum_status = tracking_numbers.first.valid_checksum? &&
59
+
60
+ checksum = tracking_numbers.first.class.const_get(:VALIDATION)[:checksum]
61
+ if checksum
62
+ checksum_status = '✓'
63
+ else
64
+ checksum_status = ''
65
+ end
66
+
67
+
68
+ status = all_number_groups.collect do |key|
69
+ s = has_key?(tracking_numbers, key) ? '✓' : ''
70
+ s = has_additional_key_info?(tracking_numbers, key) ? '✓+' : s
71
+
72
+ s
73
+ end
74
+
75
+ rows << [tracking_type, checksum_status, *status]
76
+ end
77
+ end
78
+
79
+ additional_headers = all_number_groups.collect { |g|
80
+ if g.length > 10
81
+ g.to_s.split('_').join("_\n")
82
+ else
83
+ g
84
+ end
85
+ }
86
+
87
+ table = Terminal::Table.new :title => "Tracking Number Stats [✓ = information present in number, ✓+ = additional information available]", :headings => ['Tracking Number', 'Checksum', *additional_headers], :rows => rows
88
+
89
+ puts table
90
+ end
@@ -2,21 +2,83 @@
2
2
 
3
3
  # Information on validating tracking numbers found here:
4
4
  # http://answers.google.com/answers/threadview/id/207899.html
5
-
5
+ require 'json'
6
6
  require 'tracking_number/base'
7
- require 'tracking_number/usps'
8
- require 'tracking_number/fedex'
9
- require 'tracking_number/ups'
10
- require 'tracking_number/dhl'
11
- require 'tracking_number/ontrac'
7
+ require 'tracking_number/info'
8
+ require 'tracking_number/unknown'
9
+ require 'checksum_validations'
10
+ require 'active_support'
11
+ require 'active_support/core_ext/string'
12
+ require 'active_support/core_ext/hash'
13
+ require "awesome_print"
12
14
 
13
15
  if defined?(ActiveModel::EachValidator)
14
16
  require 'tracking_number/active_model_validator'
15
17
  end
16
18
 
17
- module TrackingNumber
18
- TYPES = [UPS, UPSTest, FedExExpress, FedExSmartPost, FedExGround, FedExGround18, FedExGround96, USPS91, USPS20, USPS13, USPSTest, DHLExpress, DHLExpressAir, OnTrac]
19
+ def has_test_numbers?(tracking)
20
+ return tracking[:test_numbers] && tracking[:test_numbers][:valid]
21
+ end
22
+
23
+ def test_numbers_return_required_groups?(tracking, regex)
24
+ test_number = tracking[:test_numbers][:valid][0]
25
+ matches = test_number.match(regex)
26
+
27
+ return matches["SerialNumber"]
28
+ end
29
+
30
+ def read_courier_info(file)
31
+ return JSON.parse(File.read(file)).deep_symbolize_keys!
32
+ end
19
33
 
34
+ def create_class(klass, courier_info, tracking_info)
35
+ klass = Class.new(TrackingNumber::Base)
36
+ klass.const_set("COURIER_CODE", courier_info[:courier_code])
37
+ info = courier_info.dup
38
+ info.delete(:tracking_numbers)
39
+ klass.const_set("COURIER_INFO", info)
40
+
41
+ pattern = tracking_info[:regex]
42
+ pattern = tracking_info[:regex].join if tracking_info[:regex].is_a?(Array)
43
+
44
+ verify_pattern = "^#{pattern}$"
45
+ search_pattern = "\\b#{pattern}\\b"
46
+
47
+ klass.const_set("SEARCH_PATTERN", Regexp.new(search_pattern))
48
+ klass.const_set("VERIFY_PATTERN", Regexp.new(verify_pattern))
49
+
50
+ klass.const_set("VALIDATION", tracking_info[:validation])
51
+ klass.const_set("ADDITIONAL", tracking_info[:additional])
52
+
53
+ return klass
54
+ end
55
+
56
+ def register_class(klass, tracking_name)
57
+ klass_name = tracking_name.gsub(/[^0-9A-Za-z]/, '')
58
+ return TrackingNumber.const_set(klass_name, klass)
59
+ end
60
+
61
+ tracking_number_types = []
62
+
63
+ Dir.glob(File.join(File.dirname(__FILE__), "data/couriers/*.json")).each do |file|
64
+ courier_info = read_courier_info(file)
65
+
66
+ courier_info[:tracking_numbers].each do |tracking_info|
67
+ tracking_name = tracking_info[:name]
68
+ klass = create_class(klass, courier_info, tracking_info)
69
+
70
+ # Do some basic checks on the data file
71
+ throw 'missing test numbers' unless has_test_numbers?(tracking_info)
72
+ throw 'missing regex match groups' unless test_numbers_return_required_groups?(tracking_info, Regexp.new(klass::VERIFY_PATTERN))
73
+
74
+ const = register_class(klass, tracking_name)
75
+ tracking_number_types.push(const)
76
+ end
77
+ end
78
+
79
+ TrackingNumber.const_set("TYPES", tracking_number_types)
80
+
81
+ module TrackingNumber
20
82
  def self.search(body)
21
83
  TYPES.collect { |type| type.search(body) }.flatten
22
84
  end
@@ -30,6 +92,15 @@ module TrackingNumber
30
92
  return tn
31
93
  end
32
94
 
95
+ def self.detect_all(tracking_number)
96
+ matches = []
97
+ for test_klass in (TYPES+[Unknown])
98
+ tn = test_klass.new(tracking_number)
99
+ matches << tn if tn.valid?
100
+ end
101
+ return matches
102
+ end
103
+
33
104
  def self.new(tracking_number)
34
105
  self.detect(tracking_number)
35
106
  end