ups-ruby 0.8.3
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 +7 -0
- data/.gitignore +22 -0
- data/.hound.yml +2 -0
- data/.rubocop.yml +1064 -0
- data/.travis.yml +10 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +46 -0
- data/LICENSE.txt +14 -0
- data/README.md +78 -0
- data/Rakefile +17 -0
- data/lib/ups-ruby.rb +2 -0
- data/lib/ups.rb +33 -0
- data/lib/ups/builders/address_builder.rb +135 -0
- data/lib/ups/builders/builder_base.rb +216 -0
- data/lib/ups/builders/organisation_builder.rb +74 -0
- data/lib/ups/builders/rate_builder.rb +21 -0
- data/lib/ups/builders/ship_accept_builder.rb +30 -0
- data/lib/ups/builders/ship_confirm_builder.rb +103 -0
- data/lib/ups/builders/shipper_builder.rb +88 -0
- data/lib/ups/connection.rb +124 -0
- data/lib/ups/data.rb +50 -0
- data/lib/ups/data/canadian_states.rb +21 -0
- data/lib/ups/data/ie_counties.rb +10 -0
- data/lib/ups/data/ie_county_prefixes.rb +15 -0
- data/lib/ups/data/us_states.rb +59 -0
- data/lib/ups/exceptions.rb +7 -0
- data/lib/ups/packaging.rb +27 -0
- data/lib/ups/parsers/parser_base.rb +48 -0
- data/lib/ups/parsers/rates_parser.rb +60 -0
- data/lib/ups/parsers/ship_accept_parser.rb +52 -0
- data/lib/ups/parsers/ship_confirm_parser.rb +16 -0
- data/lib/ups/services.rb +21 -0
- data/lib/ups/version.rb +10 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/stubs/rates_negotiated_success.xml +227 -0
- data/spec/stubs/rates_success.xml +196 -0
- data/spec/stubs/ship_accept_failure.xml +12 -0
- data/spec/stubs/ship_accept_success.xml +56 -0
- data/spec/stubs/ship_confirm_failure.xml +12 -0
- data/spec/stubs/ship_confirm_success.xml +50 -0
- data/spec/support/RateRequest.xsd +1 -0
- data/spec/support/ShipAcceptRequest.xsd +36 -0
- data/spec/support/ShipConfirmRequest.xsd +996 -0
- data/spec/support/schema_path.rb +5 -0
- data/spec/support/shipping_options.rb +48 -0
- data/spec/support/xsd_validator.rb +11 -0
- data/spec/ups/builders/address_builder_spec.rb +97 -0
- data/spec/ups/builders/rate_builder_spec.rb +20 -0
- data/spec/ups/builders/ship_accept_builder_spec.rb +16 -0
- data/spec/ups/builders/ship_confirm_builder_spec.rb +23 -0
- data/spec/ups/connection/rates_negotiated_spec.rb +69 -0
- data/spec/ups/connection/rates_standard_spec.rb +71 -0
- data/spec/ups/connection/ship_spec.rb +111 -0
- data/spec/ups/connection_spec.rb +20 -0
- data/ups.gemspec +24 -0
- metadata +166 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'ox'
|
2
|
+
|
3
|
+
module UPS
|
4
|
+
module Builders
|
5
|
+
# The {OrganisationBuilder} class builds UPS XML Organization Objects.
|
6
|
+
#
|
7
|
+
# @author Paul Trippett
|
8
|
+
# @since 0.1.0
|
9
|
+
# @attr [String] name The Containing XML Element Name
|
10
|
+
# @attr [Hash] opts The Organization and Address Parts
|
11
|
+
class OrganisationBuilder < BuilderBase
|
12
|
+
include Ox
|
13
|
+
|
14
|
+
attr_accessor :name, :opts
|
15
|
+
|
16
|
+
# Initializes a new {AddressBuilder} object
|
17
|
+
#
|
18
|
+
# @param [Hash] opts The Organization and Address Parts
|
19
|
+
# @option opts [String] :company_name Company Name
|
20
|
+
# @option opts [String] :phone_number Phone Number
|
21
|
+
# @option opts [String] :address_line_1 Address Line 1
|
22
|
+
# @option opts [String] :city City
|
23
|
+
# @option opts [String] :state State
|
24
|
+
# @option opts [String] :postal_code Zip or Postal Code
|
25
|
+
# @option opts [String] :country Country
|
26
|
+
def initialize(name, opts = {})
|
27
|
+
self.name = name
|
28
|
+
self.opts = opts
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns an XML representation of company_name
|
32
|
+
#
|
33
|
+
# @return [Ox::Element] XML representation of company_name
|
34
|
+
def company_name
|
35
|
+
element_with_value('CompanyName', opts[:company_name][0..34])
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns an XML representation of phone_number
|
39
|
+
#
|
40
|
+
# @return [Ox::Element] XML representation of phone_number
|
41
|
+
def phone_number
|
42
|
+
element_with_value('PhoneNumber', opts[:phone_number][0..14])
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns an XML representation of AttentionName for which we use company
|
46
|
+
# name
|
47
|
+
#
|
48
|
+
# @return [Ox::Element] XML representation of company_name part
|
49
|
+
def attention_name
|
50
|
+
element_with_value('AttentionName', opts[:attention_name][0..34])
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns an XML representation of address
|
54
|
+
#
|
55
|
+
# @return [Ox::Element] An instance of {AddressBuilder} containing the
|
56
|
+
# address
|
57
|
+
def address
|
58
|
+
AddressBuilder.new(opts).to_xml
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns an XML representation of a UPS Organization
|
62
|
+
#
|
63
|
+
# @return [Ox::Element] XML representation of the current object
|
64
|
+
def to_xml
|
65
|
+
Element.new(name).tap do |org|
|
66
|
+
org << company_name
|
67
|
+
org << phone_number
|
68
|
+
org << attention_name
|
69
|
+
org << address
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'ox'
|
2
|
+
|
3
|
+
module UPS
|
4
|
+
module Builders
|
5
|
+
# The {RateBuilder} class builds UPS XML Rate Objects.
|
6
|
+
#
|
7
|
+
# @author Paul Trippett
|
8
|
+
# @since 0.1.0
|
9
|
+
class RateBuilder < BuilderBase
|
10
|
+
include Ox
|
11
|
+
|
12
|
+
# Initializes a new {RateBuilder} object
|
13
|
+
#
|
14
|
+
def initialize
|
15
|
+
super 'RatingServiceSelectionRequest'
|
16
|
+
|
17
|
+
add_request('Rate', 'Shop')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'ox'
|
2
|
+
|
3
|
+
module UPS
|
4
|
+
module Builders
|
5
|
+
# The {ShipAcceptBuilder} class builds UPS XML ShipAccept Objects.
|
6
|
+
#
|
7
|
+
# @author Paul Trippett
|
8
|
+
# @since 0.1.0
|
9
|
+
class ShipAcceptBuilder < BuilderBase
|
10
|
+
include Ox
|
11
|
+
|
12
|
+
# Initializes a new {ShipAcceptBuilder} object
|
13
|
+
#
|
14
|
+
def initialize
|
15
|
+
super 'ShipmentAcceptRequest'
|
16
|
+
|
17
|
+
add_request 'ShipAccept', '1'
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds a ShipmentDigest section to the XML document being built
|
21
|
+
#
|
22
|
+
# @param [String] digest The UPS Shipment Digest returned from the
|
23
|
+
# ShipConfirm request
|
24
|
+
# @return [void]
|
25
|
+
def add_shipment_digest(digest)
|
26
|
+
root << element_with_value('ShipmentDigest', digest)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'ox'
|
2
|
+
|
3
|
+
module UPS
|
4
|
+
module Builders
|
5
|
+
# The {ShipConfirmBuilder} class builds UPS XML ShipConfirm Objects.
|
6
|
+
#
|
7
|
+
# @author Paul Trippett
|
8
|
+
# @since 0.1.0
|
9
|
+
# @attr [String] name The Containing XML Element Name
|
10
|
+
# @attr [Hash] opts The Organization and Address Parts
|
11
|
+
class ShipConfirmBuilder < BuilderBase
|
12
|
+
include Ox
|
13
|
+
|
14
|
+
# Initializes a new {ShipConfirmBuilder} object
|
15
|
+
#
|
16
|
+
def initialize
|
17
|
+
super 'ShipmentConfirmRequest'
|
18
|
+
|
19
|
+
add_request 'ShipConfirm', 'validate'
|
20
|
+
end
|
21
|
+
|
22
|
+
# Adds a LabelSpecification section to the XML document being built
|
23
|
+
# according to user inputs
|
24
|
+
#
|
25
|
+
# @return [void]
|
26
|
+
def add_label_specification(format, size)
|
27
|
+
root << Element.new('LabelSpecification').tap do |label_spec|
|
28
|
+
label_spec << label_print_method(format)
|
29
|
+
label_spec << label_image_format(format)
|
30
|
+
label_spec << label_stock_size(size)
|
31
|
+
label_spec << http_user_agent if gif?(format)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Adds a Service section to the XML document being built
|
36
|
+
#
|
37
|
+
# @param [String] service_code The Service code for the choosen Shipping
|
38
|
+
# method
|
39
|
+
# @param [optional, String] service_description A description for the
|
40
|
+
# choosen Shipping Method
|
41
|
+
# @return [void]
|
42
|
+
def add_service(service_code, service_description = '')
|
43
|
+
shipment_root << code_description('Service',
|
44
|
+
service_code,
|
45
|
+
service_description)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Adds Description to XML document being built
|
49
|
+
#
|
50
|
+
# @param [String] description The description for goods being sent
|
51
|
+
#
|
52
|
+
# @return [void]
|
53
|
+
def add_description(description)
|
54
|
+
shipment_root << element_with_value('Description', description)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Adds ReferenceNumber to the XML document being built
|
58
|
+
#
|
59
|
+
# @param [Hash] opts A Hash of data to build the requested section
|
60
|
+
# @option opts [String] :code Code
|
61
|
+
# @option opts [String] :value Value
|
62
|
+
#
|
63
|
+
# @return [void]
|
64
|
+
def add_reference_number(opts = {})
|
65
|
+
shipment_root << reference_number(opts[:code], opts[:value])
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def gif?(string)
|
71
|
+
string.downcase == 'gif'
|
72
|
+
end
|
73
|
+
|
74
|
+
def http_user_agent
|
75
|
+
element_with_value('HTTPUserAgent', version_string)
|
76
|
+
end
|
77
|
+
|
78
|
+
def version_string
|
79
|
+
"RubyUPS/#{UPS::Version::STRING}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def label_print_method(format)
|
83
|
+
code_description 'LabelPrintMethod', "#{format}", "#{format} file"
|
84
|
+
end
|
85
|
+
|
86
|
+
def label_image_format(format)
|
87
|
+
code_description 'LabelImageFormat', "#{format}", "#{format}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def label_stock_size(size)
|
91
|
+
multi_valued('LabelStockSize',
|
92
|
+
'Height' => size[:height].to_s,
|
93
|
+
'Width' => size[:width].to_s)
|
94
|
+
end
|
95
|
+
|
96
|
+
def reference_number(code, value)
|
97
|
+
multi_valued('ReferenceNumber',
|
98
|
+
'Code' => code.to_s,
|
99
|
+
'Value' => value.to_s)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'ox'
|
2
|
+
|
3
|
+
module UPS
|
4
|
+
module Builders
|
5
|
+
# The {ShipperBuilder} class builds UPS XML Organization Objects.
|
6
|
+
#
|
7
|
+
# @author Paul Trippett
|
8
|
+
# @since 0.1.0
|
9
|
+
# @attr [String] name The Containing XML Element Name
|
10
|
+
# @attr [Hash] opts The Shipper and Address Parts
|
11
|
+
class ShipperBuilder < BuilderBase
|
12
|
+
include Ox
|
13
|
+
|
14
|
+
attr_accessor :name, :opts
|
15
|
+
|
16
|
+
# Initializes a new {ShipperBuilder} object
|
17
|
+
#
|
18
|
+
# @param [Hash] opts The Shipper and Address Parts
|
19
|
+
# @option opts [String] :company_name Company Name
|
20
|
+
# @option opts [String] :phone_number Phone Number
|
21
|
+
# @option opts [String] :address_line_1 Address Line 1
|
22
|
+
# @option opts [String] :city City
|
23
|
+
# @option opts [String] :state State
|
24
|
+
# @option opts [String] :postal_code Zip or Postal Code
|
25
|
+
# @option opts [String] :country Country
|
26
|
+
def initialize(opts = {})
|
27
|
+
self.name = name
|
28
|
+
self.opts = opts
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns an XML representation of shipper_name
|
32
|
+
#
|
33
|
+
# @return [Ox::Element] XML representation of shipper_name
|
34
|
+
def shipper_name
|
35
|
+
element_with_value('Name', opts[:company_name])
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns an XML representation of company_name
|
39
|
+
#
|
40
|
+
# @return [Ox::Element] XML representation of company_name
|
41
|
+
def company_name
|
42
|
+
element_with_value('CompanyName', opts[:company_name])
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns an XML representation of company_name
|
46
|
+
#
|
47
|
+
# @return [Ox::Element] XML representation of phone_number
|
48
|
+
def phone_number
|
49
|
+
element_with_value('PhoneNumber', opts[:phone_number])
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns an XML representation of company_name
|
53
|
+
#
|
54
|
+
# @return [Ox::Element] XML representation of shipper_number
|
55
|
+
def shipper_number
|
56
|
+
element_with_value('ShipperNumber', opts[:shipper_number] || '')
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns an XML representation of the associated Address
|
60
|
+
#
|
61
|
+
# @return [Ox::Element] XML object of the associated Address
|
62
|
+
def address
|
63
|
+
AddressBuilder.new(opts).to_xml
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns an XML representation of attention_name
|
67
|
+
#
|
68
|
+
# @return [Ox::Element] XML representation of attention_name
|
69
|
+
def attention_name
|
70
|
+
element_with_value('AttentionName', opts[:attention_name] || '')
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns an XML representation of the current object
|
74
|
+
#
|
75
|
+
# @return [Ox::Element] XML representation of the current object
|
76
|
+
def to_xml
|
77
|
+
Element.new('Shipper').tap do |org|
|
78
|
+
org << shipper_name
|
79
|
+
org << attention_name
|
80
|
+
org << company_name
|
81
|
+
org << phone_number
|
82
|
+
org << shipper_number
|
83
|
+
org << address
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'excon'
|
3
|
+
require 'digest/md5'
|
4
|
+
require 'ox'
|
5
|
+
|
6
|
+
module UPS
|
7
|
+
# The {Connection} class acts as the main entry point to performing rate and
|
8
|
+
# ship operations against the UPS API.
|
9
|
+
#
|
10
|
+
# @author Paul Trippett
|
11
|
+
# @abstract
|
12
|
+
# @since 0.1.0
|
13
|
+
# @attr [String] url The base url to use either TEST_URL or LIVE_URL
|
14
|
+
class Connection
|
15
|
+
attr_accessor :url
|
16
|
+
|
17
|
+
TEST_URL = 'https://wwwcie.ups.com'
|
18
|
+
LIVE_URL = 'https://onlinetools.ups.com'
|
19
|
+
|
20
|
+
RATE_PATH = '/ups.app/xml/Rate'
|
21
|
+
SHIP_CONFIRM_PATH = '/ups.app/xml/ShipConfirm'
|
22
|
+
SHIP_ACCEPT_PATH = '/ups.app/xml/ShipAccept'
|
23
|
+
ADDRESS_PATH = '/ups.app/xml/XAV'
|
24
|
+
|
25
|
+
DEFAULT_PARAMS = {
|
26
|
+
test_mode: false
|
27
|
+
}
|
28
|
+
|
29
|
+
# Initializes a new {Connection} object
|
30
|
+
#
|
31
|
+
# @param [Hash] params The initialization options
|
32
|
+
# @option params [Boolean] :test_mode If TEST_URL should be used for
|
33
|
+
# requests to the UPS URL
|
34
|
+
def initialize(params = {})
|
35
|
+
params = DEFAULT_PARAMS.merge(params)
|
36
|
+
self.url = (params[:test_mode]) ? TEST_URL : LIVE_URL
|
37
|
+
end
|
38
|
+
|
39
|
+
# Makes a request to fetch Rates for a shipment.
|
40
|
+
#
|
41
|
+
# A pre-configured {Builders::RateBuilder} object can be passed as the first
|
42
|
+
# option or a block yielded to configure a new {Builders::RateBuilder}
|
43
|
+
# object.
|
44
|
+
#
|
45
|
+
# @param [Builders::RateBuilder] rate_builder A pre-configured
|
46
|
+
# {Builders::RateBuilder} object to use
|
47
|
+
# @yield [rate_builder] A RateBuilder object for configuring
|
48
|
+
# the shipment information sent
|
49
|
+
def rates(rate_builder = nil)
|
50
|
+
if rate_builder.nil? && block_given?
|
51
|
+
rate_builder = UPS::Builders::RateBuilder.new
|
52
|
+
yield rate_builder
|
53
|
+
end
|
54
|
+
|
55
|
+
response = get_response_stream RATE_PATH, rate_builder.to_xml
|
56
|
+
UPS::Parsers::RatesParser.new.tap do |parser|
|
57
|
+
Ox.sax_parse(parser, response)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Makes a request to ship a package
|
62
|
+
#
|
63
|
+
# A pre-configured {Builders::ShipConfirmBuilder} object can be passed as
|
64
|
+
# the first option or a block yielded to configure a new
|
65
|
+
# {Builders::ShipConfirmBuilder} object.
|
66
|
+
#
|
67
|
+
# @param [Builders::ShipConfirmBuilder] confirm_builder A pre-configured
|
68
|
+
# {Builders::ShipConfirmBuilder} object to use
|
69
|
+
# @yield [ship_confirm_builder] A ShipConfirmBuilder object for configuring
|
70
|
+
# the shipment information sent
|
71
|
+
def ship(confirm_builder = nil)
|
72
|
+
if confirm_builder.nil? && block_given?
|
73
|
+
confirm_builder = Builders::ShipConfirmBuilder.new
|
74
|
+
yield confirm_builder
|
75
|
+
end
|
76
|
+
|
77
|
+
confirm_response = make_confirm_request(confirm_builder)
|
78
|
+
return confirm_response unless confirm_response.success?
|
79
|
+
|
80
|
+
accept_builder = build_accept_request_from_confirm(confirm_builder,
|
81
|
+
confirm_response)
|
82
|
+
make_accept_request accept_builder
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def build_url(path)
|
88
|
+
"#{url}#{path}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_response_stream(path, body)
|
92
|
+
response = Excon.post(build_url(path), body: body)
|
93
|
+
StringIO.new(response.body)
|
94
|
+
end
|
95
|
+
|
96
|
+
def make_confirm_request(confirm_builder)
|
97
|
+
make_ship_request confirm_builder,
|
98
|
+
SHIP_CONFIRM_PATH,
|
99
|
+
Parsers::ShipConfirmParser.new
|
100
|
+
end
|
101
|
+
|
102
|
+
def make_accept_request(accept_builder)
|
103
|
+
make_ship_request accept_builder,
|
104
|
+
SHIP_ACCEPT_PATH,
|
105
|
+
Parsers::ShipAcceptParser.new
|
106
|
+
end
|
107
|
+
|
108
|
+
def make_ship_request(builder, path, ship_parser)
|
109
|
+
response = get_response_stream path, builder.to_xml
|
110
|
+
ship_parser.tap do |parser|
|
111
|
+
Ox.sax_parse(parser, response)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_accept_request_from_confirm(confirm_builder, confirm_response)
|
116
|
+
UPS::Builders::ShipAcceptBuilder.new.tap do |builder|
|
117
|
+
builder.add_access_request confirm_builder.license_number,
|
118
|
+
confirm_builder.user_id,
|
119
|
+
confirm_builder.password
|
120
|
+
builder.add_shipment_digest confirm_response.shipment_digest
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
data/lib/ups/data.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'levenshtein'
|
2
|
+
|
3
|
+
module UPS
|
4
|
+
module Data
|
5
|
+
class << self
|
6
|
+
EMPTY_STATE_MESSAGE = 'Invalid Address State [:state]'
|
7
|
+
|
8
|
+
# Normalizes Irish states as per UPS requirements
|
9
|
+
#
|
10
|
+
# @param [String] string The Irish State to normalize
|
11
|
+
# @return [String] The normalized Irish state name
|
12
|
+
def ie_state_normalizer(string)
|
13
|
+
string.tap do |target|
|
14
|
+
IE_COUNTY_PREFIXES.each do |prefix|
|
15
|
+
target.gsub!(/^#{Regexp.escape(prefix.downcase)} /i, '')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the closest matching Irish state name. Uses Levenshtein
|
21
|
+
# distance to correct any possible spelling errors.
|
22
|
+
#
|
23
|
+
# @param [String] match_string The Irish State to match
|
24
|
+
# @raise [InvalidAttributeError] If the passed match_String is nil or
|
25
|
+
# empty
|
26
|
+
# @return [String] The closest matching irish state with the specified
|
27
|
+
# name
|
28
|
+
def ie_state_matcher(match_string)
|
29
|
+
fail Exceptions::InvalidAttributeError, EMPTY_STATE_MESSAGE if
|
30
|
+
match_string.nil? || match_string.empty?
|
31
|
+
|
32
|
+
normalized_string = ie_state_normalizer string_normalizer match_string
|
33
|
+
counties_with_distances = IE_COUNTIES.map do |county|
|
34
|
+
[county, Levenshtein.distance(county.downcase, normalized_string)]
|
35
|
+
end
|
36
|
+
counties_with_distances_hash = Hash[*counties_with_distances.flatten]
|
37
|
+
counties_with_distances_hash.min_by { |_k, v| v }[0]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Removes extra characters from a string
|
41
|
+
#
|
42
|
+
# @param [String] string The string to normalize and remove special
|
43
|
+
# characters
|
44
|
+
# @return [String] The normalized string
|
45
|
+
def string_normalizer(string)
|
46
|
+
string.downcase.gsub(/[^0-9a-z ]/i, '')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|