ups-ruby 0.8.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|