tracking_number 2.3.0 → 2.5.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b89a47ae97b7827ccdcba6c1c58aefb21868fb44810727983d451ebc8ff7448
4
- data.tar.gz: f0a25b8c5152b426887c4afcc2060b737471a90db462cd48ed516c09be1f3476
3
+ metadata.gz: 75fcefe87240b40ddf1bd16c155f13f93f46b9cbe96b343fb9b480875dd82ef6
4
+ data.tar.gz: f7ff78fe0acdc1a65ae6ddc63fe6e975ee4481c05e8d934b5fc8f09b1d2990ae
5
5
  SHA512:
6
- metadata.gz: 355632def57fa6426ca6d1d901f2c6e06fee9f7c54d6d295ee31fdbba65e62ff9fa79e28a6a935f83858c964ca7cb06606fad2add55ff4fb9306ec3b79f1369c
7
- data.tar.gz: c9f3f31326e0ee37948a16ece6fedec22ec87316250d03f99fafa9a510c9ddecdb68cff7bafe0afa17a9fe1f01b995c2bf7a70c9f92555764ca604f364b3242e
6
+ metadata.gz: e4d70982f20080077fff3be398bc0f99baabd1d67c8a579e2253735a532dbfe0d1bc38edb91df9e28f873ce1683ba26177a447c26d0503fa1c5fc2047f156a0e
7
+ data.tar.gz: 1b22b16347f3ed186f7f539dc4bfc1a32ff07719faaf1cebc5a0983ced2139db4436303ab3405d4ad9b62081bdd0b632e769dc3822a1f7a5d47709e2832ca8f1
@@ -0,0 +1,2 @@
1
+ github: [jkeen]
2
+ buy_me_a_coffee: jeffkeen
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  tracking_number changelog
2
2
 
3
+ # [2.5.0](https://github.com/jkeen/tracking_number/compare/v2.4.0...v2.5.0) (2026-08-13)
4
+
5
+
6
+ ### Features
7
+
8
+ * update to tracking_number_data 1.12, which includes support for Canpar, Purolator, Spee-Dee, YunExpress, and an updated FedEx matcher ([ca67edf](https://github.com/jkeen/tracking_number/commit/ca67edf0eaeab33171008bf115e96bdcf55c0d4b))
9
+
10
+ # [2.4.0](https://github.com/jkeen/tracking_number/compare/v2.3.0...v2.4.0) (2025-08-21)
11
+
12
+
13
+ ### Features
14
+
15
+ * update to tracking_number_data 1.9.0 ([f885bfd](https://github.com/jkeen/tracking_number/commit/f885bfd956ffacb22f38967754ec14551bca8d50))
16
+
3
17
  # [2.3.0](https://github.com/jkeen/tracking_number/compare/v2.2.0...v2.3.0) (2025-08-21)
4
18
 
5
19
 
data/README.md CHANGED
@@ -2,21 +2,15 @@
2
2
  [![Gem Version](https://badge.fury.io/rb/tracking_number.svg)](https://badge.fury.io/rb/tracking_number)
3
3
  [![Gem](https://img.shields.io/gem/dt/tracking_number.svg)]()
4
4
 
5
- > Hey there tracking number enthusiast! I don't use this project in any production capacity, and really never have. I am not a tracking number expert, and I don't have inside connections to a shipping company—I'm just a guy that once tried to make a package tracking app and this gem is all that survived. When I have absolutely nothing to do it's kinda fun to tinker with, but time has become more and more of a precious resource. Anyway, maintaining this is thankless work, and if this project has been useful for you I sure would appreciate a cup or two of coffee slid my way as a token of appreciation. A PR would also be nice.
6
- >
7
- > <a href="https://www.buymeacoffee.com/jeffkeen" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: auto !important;width: 150px !important;" ></a>
8
-
9
- ## Tracking Number (v1.x)
5
+ ## Tracking Number
10
6
 
11
7
  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.
12
8
 
13
- It detects tracking numbers from UPS, FedEx, DHL, USPS, OnTrac, Amazon Logistics, and 160+ countries national postal services (S10 standard).
9
+ It detects tracking numbers from USPS, UPS, FedEx, Amazon, DHL, OnTrac, LaserShip, Canada Post, DPD, Purolator, Old Dominion Freight Line, Spee-Dee Delivery, Canpar, YunExpress, Landmark Global, GOFO Express, and any carrier using the S10 international standard.
14
10
 
15
11
  This gem does not do tracking. That is left up to you.
16
12
 
17
- #### New in 1.0
18
-
19
- Starting with the 1.0 release the specifications for detecting tracking numbers have been moved into a separate repository ([tracking_number_data](http://github.com/jkeen/tracking_number_data)) that this gem relies on. I did this so a) we can have a single place to document all tracking number types and it can be more of a crowdsourced effort, and b) so clients can be written in other languages easier.
13
+ All the matching data is kept in [tracking_number_data](https://github.com/jkeen/tracking_number_data) and displayed in full on [trackingnumber.fyi](https://trackingnumber.fyi).
20
14
 
21
15
  ## Usage
22
16
 
@@ -9,8 +9,10 @@
9
9
  "validation": {
10
10
  "checksum": {
11
11
  "name": "mod10",
12
+ "modulo": 10,
12
13
  "evens_multiplier": 3,
13
- "odds_multiplier": 1
14
+ "odds_multiplier": 1,
15
+ "reverse": false
14
16
  }
15
17
  },
16
18
  "tracking_url": "https://www.canadapost-postescanada.ca/track-reperage/en#/search?searchFor=%s",
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "Canpar",
3
+ "courier_code": "canpar",
4
+ "tracking_numbers": [
5
+ {
6
+ "name": "Canpar (22)",
7
+ "id": "canpar_22",
8
+ "regex": "\\s*(?<SerialNumber>[CDKLSUXZ]\\s*([0-9]\\s*){21})\\s*",
9
+ "validation": {},
10
+ "tracking_url": "https://www.canpar.com/en/track/tracking.jsp?reference=%s&locale=en",
11
+ "test_numbers": {
12
+ "valid": [
13
+ "D576002440000001718010",
14
+ "D576002440000001428004",
15
+ "D576002440000002496001",
16
+ "L576002440000000001004",
17
+ "L576002440000000004001",
18
+ "S576002440000000004001",
19
+ "S576002440000000025001"
20
+ ],
21
+ "invalid": [
22
+ "1576002440000002510003",
23
+ "D57600244000000251000",
24
+ "M576002440000002510003"
25
+ ]
26
+ }
27
+ }
28
+ ]
29
+ }
@@ -8,7 +8,8 @@
8
8
  "regex": "\\s*(?<SerialNumber>([0-9]\\s*){9,10})(?<CheckDigit>([0-9]\\s*))",
9
9
  "validation": {
10
10
  "checksum": {
11
- "name": "mod7"
11
+ "name": "mod7",
12
+ "modulo": 7
12
13
  }
13
14
  },
14
15
  "tracking_url": "http://www.dhl.com/en/express/tracking.html?brand=DHL&AWB=%s",
@@ -89,6 +90,81 @@
89
90
  "GR295117494011169045"
90
91
  ]
91
92
  }
93
+ },
94
+ {
95
+ "name": "DHL E-Commerce (30)",
96
+ "id": "dhl_ecommerce_30",
97
+ "regex": [
98
+ "\\s*(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5})",
99
+ "(?!\\s*9\\s*[1-5]\\b)",
100
+ "(?<SerialNumber>",
101
+ "(?<ServiceType>(9[2-5]\\s*))",
102
+ "(?<ShipperId>([0-9]\\s*){9})",
103
+ "(?<PackageId>([0-9]\\s*){10})",
104
+ ")",
105
+ "(?<CheckDigit>[0-9]\\s*)"
106
+ ],
107
+ "validation": {
108
+ "checksum": {
109
+ "name": "mod10",
110
+ "modulo": 10,
111
+ "evens_multiplier": 3,
112
+ "odds_multiplier": 1,
113
+ "reverse": true
114
+ }
115
+ },
116
+ "tracking_url": "https://ecommerceportal.dhl.com/track?trackingnumber=%s",
117
+ "partners": [{
118
+ "description": "DHL eCommerce hands off to USPS for the last mile",
119
+ "partner_type": "carrier",
120
+ "partner_id": "usps_22",
121
+ "validation": {
122
+ "matches_all": [
123
+ { "regex_group_name": "RoutingApplicationId", "matches": "420" },
124
+ { "regex_group_name": "ServiceType", "matches_regex": "9[2345]" }
125
+ ]
126
+ }
127
+ },
128
+ {
129
+ "description": "DHL eCommerce hands off to USPS for the last mile",
130
+ "partner_type": "carrier",
131
+ "partner_id": "usps_91",
132
+ "validation": {
133
+ "matches_all": [
134
+ { "regex_group_name": "RoutingApplicationId", "matches": "420" },
135
+ { "regex_group_name": "ServiceType", "matches_regex": "9[2345]" }
136
+ ]
137
+ }
138
+ }],
139
+ "additional": [
140
+ {
141
+ "name": "Service Type",
142
+ "regex_group_name": "ServiceType",
143
+ "lookup": [
144
+ { "matches": "92", "name": "USPS Tracking (Commercial)" },
145
+ { "matches": "93", "name": "Collect on Delivery (COD)" },
146
+ { "matches": "94", "name": "USPS Tracking" },
147
+ { "matches": "95", "name": "Certified Mail" }
148
+ ]
149
+ }
150
+ ],
151
+ "test_numbers": {
152
+ "valid": [
153
+ "420902459261290336128704042634",
154
+ "420941179261290336128704062441",
155
+ "420926029261290336128704067248",
156
+ "420902729261290336128704124163",
157
+ "420950549261290336128704133837",
158
+ "420193809261290336128704280838",
159
+ "420900469261290336128704216936"
160
+ ],
161
+ "invalid": [
162
+ "420941179261290336128704062442",
163
+ "420941179261290336128704062400",
164
+ "420941179261290336128704062422"
165
+ ]
166
+ }
92
167
  }
168
+
93
169
  ]
94
170
  }
@@ -17,7 +17,9 @@
17
17
  ],
18
18
  "validation": {
19
19
  "checksum": {
20
- "name": "mod_37_36"
20
+ "name": "mod_37_36",
21
+ "modulo": 36,
22
+ "alphabet": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
21
23
  }
22
24
  },
23
25
  "tracking_url": "https://www.dpdgroup.com/nl/mydpd/my-parcels/track?lang=en&parcelNumber=%s",
@@ -323,7 +325,9 @@
323
325
  ],
324
326
  "validation": {
325
327
  "checksum": {
326
- "name": "mod_37_36"
328
+ "name": "mod_37_36",
329
+ "modulo": 36,
330
+ "alphabet": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
327
331
  }
328
332
  },
329
333
  "tracking_url": "https://www.dpdgroup.com/nl/mydpd/my-parcels/track?lang=en&parcelNumber=%s",
@@ -45,8 +45,7 @@
45
45
  "name": "FedEx Express (34)",
46
46
  "id": "fedex_34",
47
47
  "regex": [
48
- "\\s*1\\s*0\\s*[0-9]\\s*[0-9]\\s*[0-9]\\s*",
49
- "([0-9]\\s*){10}",
48
+ "\\s*[0-8]\\s*([0-9]\\s*){14}",
50
49
  "(?<DestinationZip>([0-9]\\s*){5})",
51
50
  "(?<SerialNumber>([0-9]\\s*){13})",
52
51
  "(?<CheckDigit>[0-9]\\s*)"
@@ -81,7 +80,11 @@
81
80
  "1001901781990001000300617767839437",
82
81
  " 1 0 0 1 9 0 1 7 8 1 9 9 0 0 0 1 0 0 0 3 0 0 6 1 7 7 6 7 8 3 9 4 3 7 ",
83
82
  "1002297871540001000300790695517286",
84
- "1027590111820004833500785458233610"
83
+ "1027590111820004833500785458233610",
84
+ "1195266473930007521100871079919940",
85
+ "1 1 9 52 6 64739 3000 75211 00 8710 799 199 40",
86
+ "1195271874060007521100871140316925",
87
+ "1195583874210007521100871140316936"
85
88
  ],
86
89
  "invalid": [
87
90
  "1001901781990001000300617767839438"
@@ -92,13 +95,16 @@
92
95
  "name": "FedEx SmartPost",
93
96
  "id": "fedex_smartpost",
94
97
  "description": "Shipped by FedEx, Delivered by USPS",
98
+ "glossary": {
99
+ "ApplicationIdentifier": { "description": "Always 92 here, the code for a parcel handed to USPS for delivery." }
100
+ },
95
101
  "regex": [
96
102
  "\\s*(?:",
97
103
  "(?:(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5}))?",
98
104
  "(?<ApplicationIdentifier>9\\s*2\\s*)",
99
105
  ")?",
100
106
  "(?<SerialNumber>",
101
- "(?<SCNC>([0-9]\\s*){2})",
107
+ "(?<SCNC>6\\s*1\\s*)",
102
108
  "(?<ServiceType>2\\s*9\\s*)",
103
109
  "(?<ShipperId>([0-9]\\s*){8})",
104
110
  "(?<PackageId>([0-9]\\s*){11}|([0-9]\\s*){7})",
@@ -125,8 +131,10 @@
125
131
  "validation": {
126
132
  "checksum": {
127
133
  "name": "mod10",
134
+ "modulo": 10,
128
135
  "evens_multiplier": 3,
129
- "odds_multiplier": 1
136
+ "odds_multiplier": 1,
137
+ "reverse": false
130
138
  },
131
139
  "serial_number_format": {
132
140
  "prepend_if": {
@@ -138,11 +146,9 @@
138
146
  "tracking_url": "https://www.fedex.com/apps/fedextrack/?tracknumbers=%s",
139
147
  "test_numbers": {
140
148
  "valid": [
141
- "61299998820821171811",
142
149
  "9261292700768711948021",
143
150
  "420 11213 92 6129098349792366623 8",
144
- "92 6129098349792366623 8",
145
- "6129098349792366623 8"
151
+ "92 6129098349792366623 8"
146
152
  ],
147
153
  "invalid": [
148
154
  "9261292700768711948020",
@@ -161,8 +167,10 @@
161
167
  "validation": {
162
168
  "checksum": {
163
169
  "name": "mod10",
170
+ "modulo": 10,
164
171
  "evens_multiplier": 1,
165
- "odds_multiplier": 3
172
+ "odds_multiplier": 3,
173
+ "reverse": false
166
174
  }
167
175
  },
168
176
  "tracking_url": "https://www.fedex.com/apps/fedextrack/?tracknumbers=%s",
@@ -186,8 +194,10 @@
186
194
  "validation": {
187
195
  "checksum": {
188
196
  "name": "mod10",
197
+ "modulo": 10,
189
198
  "evens_multiplier": 3,
190
- "odds_multiplier": 1
199
+ "odds_multiplier": 1,
200
+ "reverse": false
191
201
  }
192
202
  },
193
203
  "test_numbers": {
@@ -227,6 +237,10 @@
227
237
  {
228
238
  "name": "FedEx Ground 96 (22)",
229
239
  "id": "fedex_ground_96",
240
+ "glossary": {
241
+ "ApplicationIdentifier": { "description": "Always 96, marking a ground shipping label." },
242
+ "SCNC": { "description": "FedEx Ground uses 11, 12 and 13." }
243
+ },
230
244
  "regex": [
231
245
  "\\s*(?<ApplicationIdentifier>9\\s*6\\s*)",
232
246
  "(?<SCNC>([0-9]\\s*){2})",
@@ -237,8 +251,10 @@
237
251
  "validation": {
238
252
  "checksum": {
239
253
  "name": "mod10",
254
+ "modulo": 10,
240
255
  "evens_multiplier": 1,
241
- "odds_multiplier": 3
256
+ "odds_multiplier": 3,
257
+ "reverse": false
242
258
  }
243
259
  },
244
260
  "tracking_url": "https://www.fedex.com/apps/fedextrack/?tracknumbers=%s",
@@ -255,6 +271,10 @@
255
271
  {
256
272
  "name": "FedEx Ground GSN",
257
273
  "id": "fedex_ground_gsn",
274
+ "glossary": {
275
+ "ApplicationIdentifier": { "description": "Always 96, marking a ground shipping label." },
276
+ "SCNC": { "description": "FedEx Ground uses 11, 12 and 13." }
277
+ },
258
278
  "regex": [
259
279
  "\\s*(?<ApplicationIdentifier>9\\s*6\\s*)",
260
280
  "(?<SCNC>([0-9]\\s*){2})",
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "GOFO Express",
3
+ "courier_code": "gofo",
4
+ "tracking_numbers": [
5
+ {
6
+ "name": "GOFO Express (US)",
7
+ "id": "gofo_us",
8
+ "regex": [
9
+ "\\s*G\\s*F\\s*U\\s*S\\s*(?<SerialNumber>([0-9]\\s*){14})\\s*"
10
+ ],
11
+ "tracking_url": "https://www.gofoexpress.com/tracking.html?searchID=%s",
12
+ "validation": {},
13
+ "test_numbers": {
14
+ "valid": [
15
+ "GFUS01011884214464",
16
+ "GFUS01011884214272"
17
+ ],
18
+ "invalid": [
19
+ "GFUS0101188421446"
20
+ ]
21
+ }
22
+ }
23
+ ]
24
+ }
@@ -79,6 +79,27 @@
79
79
  "1LS7119013618127-2"
80
80
  ]
81
81
  }
82
+ },
83
+ {
84
+ "name": "LaserShip 1LSCX (15)",
85
+ "id": "lasership_1lscx",
86
+ "regex": [
87
+ "\\s*1\\s*L\\s*S\\s*",
88
+ "\\s*C\\s*X\\s*",
89
+ "(?<SerialNumber>",
90
+ "([0-9A-Z]\\s*){10}",
91
+ ")"
92
+ ],
93
+ "validation": {},
94
+ "test_numbers": {
95
+ "valid": [
96
+ "1LS CX VE00 58631Y",
97
+ "1LSCXVE005BUEFX"
98
+ ],
99
+ "invalid": [
100
+ "1LSCZVE0058631Y"
101
+ ]
102
+ }
82
103
  }
83
104
  ]
84
105
  }
@@ -13,7 +13,8 @@
13
13
  ],
14
14
  "validation": {
15
15
  "checksum": {
16
- "name": "luhn"
16
+ "name": "luhn",
17
+ "modulo": 10
17
18
  }
18
19
  },
19
20
  "tracking_url": "https://www.odfl.com/us/en/tools/trace-track-ltl-freight/trace.html?proNumbers=%s",
@@ -43,7 +44,8 @@
43
44
  ],
44
45
  "validation": {
45
46
  "checksum": {
46
- "name": "luhn"
47
+ "name": "luhn",
48
+ "modulo": 10
47
49
  }
48
50
  },
49
51
  "tracking_url": "https://www.odfl.com/us/en/tools/trace-track-ltl-freight/trace.html?proNumbers=%s",
@@ -9,8 +9,10 @@
9
9
  "validation": {
10
10
  "checksum": {
11
11
  "name": "mod10",
12
+ "modulo": 10,
12
13
  "evens_multiplier": 1,
13
- "odds_multiplier": 2
14
+ "odds_multiplier": 2,
15
+ "reverse": false
14
16
  },
15
17
  "serial_number_format": {
16
18
  "prepend_if": {
@@ -42,8 +44,10 @@
42
44
  "validation": {
43
45
  "checksum": {
44
46
  "name": "mod10",
47
+ "modulo": 10,
45
48
  "evens_multiplier": 1,
46
- "odds_multiplier": 2
49
+ "odds_multiplier": 2,
50
+ "reverse": false
47
51
  },
48
52
  "serial_number_format": {
49
53
  "prepend_if": {
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "Purolator",
3
+ "courier_code": "purolator",
4
+ "tracking_numbers": [
5
+ {
6
+ "name": "Purolator (12)",
7
+ "id": "purolator_numeric",
8
+ "regex": "\\s*(?<SerialNumber>[0-5]\\s*([0-9]\\s*){10})(?<CheckDigit>[0-9])\\s*",
9
+ "validation": {
10
+ "checksum": {
11
+ "name": "luhn"
12
+ }
13
+ },
14
+ "tracking_url": "https://www.purolator.com/en/shipping/tracker?searchValue=%s",
15
+ "test_numbers": {
16
+ "valid": [
17
+ "320595463938",
18
+ "320606146993",
19
+ "287809468872",
20
+ "320610804900",
21
+ "331426749957",
22
+ "331434972567"
23
+ ],
24
+ "invalid": [
25
+ "320595463939",
26
+ "287809468875"
27
+ ]
28
+ }
29
+ },
30
+ {
31
+ "name": "Purolator (alpha + 9)",
32
+ "id": "purolator_alpha",
33
+ "regex": "\\s*(?<SerialNumber>([A-Z]\\s*){3}([0-9]\\s*){9})\\s*",
34
+ "validation": {},
35
+ "tracking_url": "https://www.purolator.com/en/shipping/tracker?searchValue=%s",
36
+ "test_numbers": {
37
+ "valid": [
38
+ "KYV009956937",
39
+ "CGK002986959",
40
+ "JFV247545960",
41
+ "TLR000083964"
42
+ ],
43
+ "invalid": [
44
+ "KY009956937",
45
+ "331426749957"
46
+ ]
47
+ }
48
+ }
49
+ ]
50
+ }
@@ -7,7 +7,9 @@
7
7
  "name": "S10",
8
8
  "validation": {
9
9
  "checksum": {
10
- "name": "s10"
10
+ "name": "s10",
11
+ "weightings": [8, 6, 4, 2, 3, 5, 9, 7],
12
+ "modulo": 11
11
13
  },
12
14
  "additional": {
13
15
  "exists": [
@@ -17,6 +19,11 @@
17
19
  },
18
20
  "regex": "\\s*(?<ServiceType>([A-Z]\\s*){2})(?<SerialNumber>([0-9]\\s*){8})(?<CheckDigit>([0-9]\\s*))(?<CountryCode>([A-Z]\\s*){2})",
19
21
  "tracking_url": null,
22
+ "glossary": {
23
+ "ServiceType": { "description": "Two letters for the class of postal service, the first of which identifies the service and the second the variant." },
24
+ "SerialNumber": { "description": "Eight digits assigned by the issuing postal service." },
25
+ "CountryCode": { "description": "Two letters for the country whose postal service issued the number, following ISO 3166." }
26
+ },
20
27
  "test_numbers": {
21
28
  "valid": [
22
29
  "RB123456785GB",
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "Spee-Dee Delivery",
3
+ "courier_code": "speedee",
4
+ "tracking_numbers": [
5
+ {
6
+ "name": "Spee-Dee (20)",
7
+ "id": "speedee",
8
+ "regex": "\\s*S\\s*P\\s*(?<SerialNumber>([0-9]\\s*){18})\\s*",
9
+ "validation": {},
10
+ "tracking_url": "https://www.speedeedelivery.com/track/?tracking=%s",
11
+ "test_numbers": {
12
+ "valid": [
13
+ "SP029692510000920746",
14
+ "SP029692510000901479",
15
+ "SP029692510000688332",
16
+ "SP029692510000279301",
17
+ "SP029692450001607639"
18
+ ],
19
+ "invalid": [
20
+ "029692510000920746",
21
+ "SX029692510000920746"
22
+ ]
23
+ }
24
+ }
25
+ ]
26
+ }
@@ -15,8 +15,10 @@
15
15
  "validation": {
16
16
  "checksum": {
17
17
  "name": "mod10",
18
+ "modulo": 10,
18
19
  "evens_multiplier": 1,
19
- "odds_multiplier": 2
20
+ "odds_multiplier": 2,
21
+ "reverse": false
20
22
  }
21
23
  },
22
24
  "tracking_url": "https://wwwapps.ups.com/WebTracking/track?track=yes&trackNums=%s",
@@ -146,8 +148,10 @@
146
148
  "validation": {
147
149
  "checksum": {
148
150
  "name": "mod10",
151
+ "modulo": 10,
149
152
  "evens_multiplier": 1,
150
- "odds_multiplier": 2
153
+ "odds_multiplier": 2,
154
+ "reverse": false
151
155
  }
152
156
  },
153
157
  "additional": [
@@ -3,10 +3,10 @@
3
3
  "courier_code": "usps",
4
4
  "tracking_numbers": [
5
5
  {
6
- "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=%s",
7
6
  "name": "USPS 20",
8
7
  "id": "usps_20",
9
8
  "description": "20 digit USPS numbers",
9
+ "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=%s",
10
10
  "regex": [
11
11
  "\\s*(?<SerialNumber>",
12
12
  "(?<ServiceType>([0-9]\\s*){2})",
@@ -18,8 +18,10 @@
18
18
  "validation": {
19
19
  "checksum": {
20
20
  "name": "mod10",
21
+ "modulo": 10,
21
22
  "evens_multiplier": 3,
22
- "odds_multiplier": 1
23
+ "odds_multiplier": 1,
24
+ "reverse": false
23
25
  }
24
26
  },
25
27
  "test_numbers": {
@@ -57,10 +59,84 @@
57
59
  }
58
60
  ]
59
61
  },
62
+ {
63
+ "name": "USPS 22",
64
+ "id": "usps_22",
65
+ "description": "22 digit USPS numbers",
66
+ "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=%s",
67
+ "regex": [
68
+ "\\s*((?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5}))?",
69
+ "(?<SerialNumber>",
70
+ "(?<ServiceType>(9\\s*[4-8]\\s*))",
71
+ "(?<ShipperId>([0-9]\\s*){9})",
72
+ "(?<PackageId>([0-9]\\s*){10})",
73
+ ")",
74
+ "(?<CheckDigit>[0-9]\\s*)"
75
+ ],
76
+ "validation": {
77
+ "checksum": {
78
+ "name": "mod10",
79
+ "modulo": 10,
80
+ "evens_multiplier": 3,
81
+ "odds_multiplier": 1,
82
+ "reverse": true
83
+ }
84
+ },
85
+ "test_numbers": {
86
+ "valid": [
87
+ "420787459400111206206406260787",
88
+ "9400111206206406260787",
89
+ "9434611206206406227577",
90
+ "9434611206206407667136",
91
+ "9400111206206407628746",
92
+ "9400111206206407628845"
93
+ ],
94
+ "invalid": [
95
+ "9434611206206407667131",
96
+ "9434611306206407667222",
97
+ "2334611306206407667222"
98
+ ]
99
+ },
100
+ "partners": [{
101
+ "description": "DHL ECommerce uses USPS for last mile delivery",
102
+ "partner_type": "shipper",
103
+ "partner_id": "dhl_ecommerce_30",
104
+ "validation": {
105
+ "matches_all": [
106
+ {
107
+ "regex_group_name": "RoutingApplicationId",
108
+ "matches": "420"
109
+ },
110
+ {
111
+ "matches_regex": "9[24]",
112
+ "regex_group_name": "ServiceType"
113
+ }
114
+ ]
115
+ }
116
+ }],
117
+ "additional": [
118
+ {
119
+ "name": "Service Type",
120
+ "regex_group_name": "ServiceType",
121
+ "lookup": [
122
+ { "matches": "92", "name": "Signature Confirmation" },
123
+ { "matches": "93", "name": "Collect on Delivery (COD)" },
124
+ { "matches": "94", "name": "USPS Tracking" },
125
+ { "matches": "95", "name": "Certified Mail" },
126
+ { "matches": "96", "name": "Insured Mail" },
127
+ { "matches": "97", "name": "Registered Mail" },
128
+ { "matches": "98", "name": "Return Receipt for Merchandise" }
129
+ ]
130
+ }
131
+ ]
132
+ },
60
133
  {
61
134
  "name": "USPS 34v2",
62
135
  "id": "usps_32V2",
63
136
  "description": "variation on 34 digit USPS IMpd numbers",
137
+ "glossary": {
138
+ "ApplicationIdentifier": { "description": "92 through 95, saying which USPS service and mailer ID scheme the rest belongs to." }
139
+ },
64
140
  "regex": [
65
141
  "\\s*(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5})",
66
142
  "(?<RoutingNumber>([0-9]\\s*){4})",
@@ -74,8 +150,10 @@
74
150
  "validation": {
75
151
  "checksum": {
76
152
  "name": "mod10",
153
+ "modulo": 10,
77
154
  "evens_multiplier": 3,
78
- "odds_multiplier": 1
155
+ "odds_multiplier": 1,
156
+ "reverse": false
79
157
  }
80
158
  },
81
159
  "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=%s",
@@ -94,6 +172,9 @@
94
172
  "name": "USPS 91",
95
173
  "id": "usps_91",
96
174
  "description": "USPS now calls this the IMpd barcode format",
175
+ "glossary": {
176
+ "ApplicationIdentifier": { "description": "91 through 95, saying which USPS service and mailer ID scheme the rest belongs to." }
177
+ },
97
178
  "regex": [
98
179
  "\\s*(?:(?<RoutingApplicationId>4\\s*2\\s*0\\s*)(?<DestinationZip>([0-9]\\s*){5}))?",
99
180
  "(?<SerialNumber>",
@@ -108,8 +189,10 @@
108
189
  "validation": {
109
190
  "checksum": {
110
191
  "name": "mod10",
192
+ "modulo": 10,
111
193
  "evens_multiplier": 3,
112
- "odds_multiplier": 1
194
+ "odds_multiplier": 1,
195
+ "reverse": false
113
196
  },
114
197
  "serial_number_format": {
115
198
  "prepend_if": {
@@ -131,6 +214,31 @@
131
214
  {
132
215
  "matches": "61",
133
216
  "regex_group_name": "SCNC"
217
+ },
218
+ {
219
+ "regex_group_name": "ApplicationIdentifier",
220
+ "matches": "92"
221
+ }
222
+ ]
223
+ }
224
+ },
225
+ {
226
+ "description": "DHL uses USPS for last mile delivery, but not all USPS91 numbers are DHL Ecommerce",
227
+ "partner_type": "shipper",
228
+ "partner_id": "dhl_ecommerce_30",
229
+ "validation": {
230
+ "matches_all": [
231
+ {
232
+ "regex_group_name": "ServiceType",
233
+ "matches": "29"
234
+ },
235
+ {
236
+ "matches": "61",
237
+ "regex_group_name": "SCNC"
238
+ },
239
+ {
240
+ "matches": "420",
241
+ "regex_group_name": "RoutingApplicationId"
134
242
  }
135
243
  ]
136
244
  }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "YunExpress",
3
+ "courier_code": "yunexpress",
4
+ "tracking_numbers": [
5
+ {
6
+ "name": "YunExpress",
7
+ "id": "yunexpress",
8
+ "tracking_url": "https://www.yuntrack.com/parcelTracking?id=%s",
9
+ "regex": [
10
+ "\\s*Y\\s*T\\s*(?<SerialNumber>([0-9]\\s*){16})\\s*"
11
+ ],
12
+ "validation": {},
13
+ "test_numbers": {
14
+ "valid": [
15
+ "YT2229521272164446",
16
+ "YT2229521272164447",
17
+ " Y T 2 2 2 9 5 2 1 2 7 2 1 6 4 4 4 7 "
18
+ ],
19
+ "invalid": [
20
+ "YT123456789012345"
21
+ ]
22
+ }
23
+ }
24
+ ]
25
+ }
@@ -126,16 +126,16 @@ module TrackingNumber
126
126
 
127
127
  def info
128
128
  Info.new({
129
- courier: courier_info,
130
- service_type: service_type,
131
- service_description: service_description,
132
- destination_zip: destination_zip,
133
- shipper_id: shipper_id,
134
- package_type: package_type,
135
- tracking_url: tracking_url,
136
- partners: partners,
137
- decode: decode
138
- })
129
+ courier: courier_info,
130
+ service_type: service_type,
131
+ service_description: service_description,
132
+ destination_zip: destination_zip,
133
+ shipper_id: shipper_id,
134
+ package_type: package_type,
135
+ tracking_url: tracking_url,
136
+ partners: partners,
137
+ decode: decode
138
+ })
139
139
  end
140
140
 
141
141
  def courier_code
@@ -186,8 +186,7 @@ module TrackingNumber
186
186
  partner_hash = {}
187
187
 
188
188
  return unless (partner_tn = find_matching_partner)
189
-
190
- possible_twin = partner_tn.send(:find_matching_partner)
189
+ possible_twin = partner_tn.send(:find_matching_partner, self.class.const_get("ID"))
191
190
 
192
191
  if possible_twin.instance_of?(self.class) && possible_twin.tracking_number == tracking_number
193
192
  partner_hash[partner_data[:partner_type].to_sym] = partner_tn
@@ -295,8 +294,12 @@ module TrackingNumber
295
294
  nil
296
295
  end
297
296
 
298
- def find_matching_partner
299
- partner_info = self.class.const_get(:PARTNERS) || []
297
+ def find_matching_partner(partner_id = nil)
298
+ partner_info = if partner_id
299
+ (self.class.const_get(:PARTNERS) || []).select { |info| info[:partner_id]&.to_sym == partner_id.to_sym }
300
+ else
301
+ self.class.const_get(:PARTNERS) || []
302
+ end
300
303
 
301
304
  partner_info.each do |partner_data|
302
305
  klass = find_tracking_class_by_id(partner_data[:partner_id])
@@ -304,7 +307,6 @@ module TrackingNumber
304
307
  return false unless klass
305
308
 
306
309
  tn = klass.new(tracking_number)
307
-
308
310
  valid = if partner_data.dig(:validation, :matches_all)
309
311
  tn.valid? && match_all(partner_data.dig(:validation, :matches_all))
310
312
  elsif partner_data.dig(:validation, :matches_any)
@@ -1,3 +1,3 @@
1
1
  module TrackingNumber
2
- VERSION = "2.3.0"
2
+ VERSION = "2.5.0"
3
3
  end
data/package.json CHANGED
@@ -38,31 +38,7 @@
38
38
  "release": "minor"
39
39
  },
40
40
  {
41
- "type": "build",
42
- "release": "patch"
43
- },
44
- {
45
- "type": "ci",
46
- "release": "patch"
47
- },
48
- {
49
- "type": "chore",
50
- "release": "patch"
51
- },
52
- {
53
- "type": "docs",
54
- "release": "patch"
55
- },
56
- {
57
- "type": "refactor",
58
- "release": "patch"
59
- },
60
- {
61
- "type": "style",
62
- "release": "patch"
63
- },
64
- {
65
- "type": "test",
41
+ "type": "fix",
66
42
  "release": "patch"
67
43
  }
68
44
  ],
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tracking_number
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Keen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-21 00:00:00.000000000 Z
11
+ date: 2026-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -146,6 +146,7 @@ extra_rdoc_files:
146
146
  - LICENSE.txt
147
147
  files:
148
148
  - ".document"
149
+ - ".github/FUNDING.yml"
149
150
  - ".github/dependabot.yml"
150
151
  - ".github/workflows/release.yml"
151
152
  - ".github/workflows/ruby.yml"
@@ -160,16 +161,21 @@ files:
160
161
  - bin/console
161
162
  - lib/data/couriers/amazon.json
162
163
  - lib/data/couriers/canadapost.json
164
+ - lib/data/couriers/canpar.json
163
165
  - lib/data/couriers/dhl.json
164
166
  - lib/data/couriers/dpd.json
165
167
  - lib/data/couriers/fedex.json
168
+ - lib/data/couriers/gofo.json
166
169
  - lib/data/couriers/landmark.json
167
170
  - lib/data/couriers/lasership.json
168
171
  - lib/data/couriers/old_dominion.json
169
172
  - lib/data/couriers/ontrac.json
173
+ - lib/data/couriers/purolator.json
170
174
  - lib/data/couriers/s10.json
175
+ - lib/data/couriers/speedee.json
171
176
  - lib/data/couriers/ups.json
172
177
  - lib/data/couriers/usps.json
178
+ - lib/data/couriers/yunexpress.json
173
179
  - lib/tracking_number.rb
174
180
  - lib/tracking_number/active_model_validator.rb
175
181
  - lib/tracking_number/base.rb