offer_accept_matcher 0.0.2
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.
- data/README.md +33 -0
- data/lib/offer_accept_matcher.rb +181 -0
- metadata +47 -0
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
Offer and accept matcher.
|
2
|
+
|
3
|
+
Author: Philipp Kempgen, [http://kempgen.net](http://kempgen.net)
|
4
|
+
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
require 'offer_accept_matcher'
|
9
|
+
|
10
|
+
offers = [
|
11
|
+
{ :main => 'en-UK' , 'q' => 1.0 },
|
12
|
+
{ :main => 'de-DE' , 'q' => 0.8 },
|
13
|
+
]
|
14
|
+
|
15
|
+
accept_str = "de-DE;q=1.0, de;q=0.9, en-US;q=0.6, en-UK;q=0.6, en;q=0.5, *;q=0.1"
|
16
|
+
|
17
|
+
winning_offer = OfferAcceptMatcher.winning_offer(
|
18
|
+
weighted_offers =
|
19
|
+
OfferAcceptMatcher.compare( accept_str, offers,
|
20
|
+
& OfferAcceptMatcher.iso_lang_tag_comparator
|
21
|
+
))
|
22
|
+
|
23
|
+
puts "Weighted offers:"
|
24
|
+
weighted_offers.each { |offer|
|
25
|
+
puts offer.inspect
|
26
|
+
}
|
27
|
+
#=> [0.9508, {:main=>"de-DE", "q"=>0.8}]
|
28
|
+
#=> [0.6010, {:main=>"en-UK", "q"=>1.0}]
|
29
|
+
|
30
|
+
puts "Winning offer:"
|
31
|
+
puts winning_offer[:main].inspect
|
32
|
+
#=> "de-DE"
|
33
|
+
|
@@ -0,0 +1,181 @@
|
|
1
|
+
module OfferAcceptMatcher
|
2
|
+
|
3
|
+
Q_KEY = 'q'.freeze
|
4
|
+
|
5
|
+
# Parses a comma-separated (",") list of preferences, similar
|
6
|
+
# to the Accept, Accept-Charset, Accept-Language and other headers
|
7
|
+
# in HTTP.
|
8
|
+
# Preferences can be weighted with "q=", see
|
9
|
+
# http://tools.ietf.org/html/rfc2616
|
10
|
+
#
|
11
|
+
# e.g. "de-DE;q=1, de;q=0.9, en-US;q=0.6, en-UK;q=0.6, en;q=0.5, *;q=0.1"
|
12
|
+
#
|
13
|
+
# This isn't a fully valid implementation of a parser for
|
14
|
+
# parameters as the are allowed in HTTP headers, as "," (comma)
|
15
|
+
# isn't allowed even in quoted string values.
|
16
|
+
#
|
17
|
+
# We parse parameters but we don't interpret the main "thing", be
|
18
|
+
# it a media type or a character encoding name or an ISO language
|
19
|
+
# tag or what have you. It may not include a "," or ";", that's
|
20
|
+
# all.
|
21
|
+
#
|
22
|
+
def self.parse_acceptables_list( str )
|
23
|
+
|
24
|
+
acceptables = []
|
25
|
+
accept_specs = str.to_s.split(',')
|
26
|
+
accept_specs.each { |accept_spec|
|
27
|
+
accept_spec_parts = accept_spec.split(';').each( & :'strip!' )
|
28
|
+
main = accept_spec_parts.shift || ''
|
29
|
+
next if main.empty?
|
30
|
+
acceptable = { :main => main, :params => {} }
|
31
|
+
accept_spec_parts.each { |param_spec|
|
32
|
+
param, value = param_spec.split('=',2)
|
33
|
+
next if param.to_s.empty?
|
34
|
+
param.strip!
|
35
|
+
if value
|
36
|
+
# ";param=value"
|
37
|
+
value.strip!
|
38
|
+
if value.start_with?('"') && value.end_with?('"')
|
39
|
+
value = value[ 1, value.length - 2 ]
|
40
|
+
value.gsub!( /\\"/, '"' )
|
41
|
+
end
|
42
|
+
# ";param", e.g. "no-cache"
|
43
|
+
end
|
44
|
+
acceptable[:params][ param.freeze ] = value
|
45
|
+
}
|
46
|
+
acceptables << acceptable
|
47
|
+
}
|
48
|
+
if acceptables.length == 0
|
49
|
+
acceptables << { :main => '*' , :params => {} }
|
50
|
+
acceptables << { :main => '*/*' , :params => {} }
|
51
|
+
end
|
52
|
+
return acceptables
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.interpret_q_values!( acceptables )
|
57
|
+
acceptables.each { |acceptable|
|
58
|
+
acceptable[:params][Q_KEY] = (acceptable[:params][Q_KEY] || 1.0).to_f
|
59
|
+
}
|
60
|
+
acceptables
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.compare( acceptables, offers, &comparator )
|
64
|
+
acceptables = parse_acceptables_list( acceptables ) unless acceptables.kind_of?(::Array)
|
65
|
+
parse_qvals!( acceptables )
|
66
|
+
l = convert_to_lambda( &comparator )
|
67
|
+
weighted_offers = offers.map { |offer|
|
68
|
+
o = offer.dup
|
69
|
+
offer_rating = acceptables.map { |acceptable|
|
70
|
+
a = acceptable.dup
|
71
|
+
#puts " #{offer.inspect} #{acceptable.inspect}"
|
72
|
+
rating = l.call( o, a )
|
73
|
+
if ! rating.respond_to?(:to_f)
|
74
|
+
rating = 0.0
|
75
|
+
else
|
76
|
+
acceptable_q = (acceptable [Q_KEY] || 1.0).to_f
|
77
|
+
offer_q = (offer [Q_KEY] || 1.0).to_f
|
78
|
+
offer_q = 0.5 if ! offer_q.between?( -0.0001, 1.0001 )
|
79
|
+
|
80
|
+
rating = rating.to_f * acceptable_q * (offer_q / 4 + (1.0 - 1.0 / 4))
|
81
|
+
rating = 0.0 if offer_q == 0.0
|
82
|
+
rating+= (offer_q / 1000)
|
83
|
+
end
|
84
|
+
#puts " #{offer.inspect} #{acceptable.inspect} => #{rating}"
|
85
|
+
rating
|
86
|
+
}.max || 1.0
|
87
|
+
[ offer_rating, offer ]
|
88
|
+
}
|
89
|
+
#weighted_offers.sort_by{ |x| x[0] }.reverse.each { |o|
|
90
|
+
# puts " #{o.inspect}"
|
91
|
+
#}
|
92
|
+
|
93
|
+
#winning_offer_rating, winning_offer = weighted_offers.max_by{ |x| x[0] }
|
94
|
+
#return winning_offer
|
95
|
+
weighted_offers.sort_by!{ |x| x[0] }.reverse!
|
96
|
+
return weighted_offers
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.winning_offer( sorted_weighted_offers )
|
100
|
+
#winning_offer = (unsorted_weighted_offers || []).max_by{ |x| x[0] }
|
101
|
+
winning_offer = (sorted_weighted_offers || []).first
|
102
|
+
return (winning_offer || [])[1] # can be nil
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.simplistic_comparator
|
106
|
+
return lambda { |offer, acceptable|
|
107
|
+
o_main = offer [:main].to_s.downcase
|
108
|
+
a_main = acceptable [:main].to_s.downcase
|
109
|
+
return 1.00 if o_main == a_main
|
110
|
+
return 0.90 if a_main == '*'
|
111
|
+
return 0.90 if a_main == '*/*'
|
112
|
+
return 0.40 if o_main.start_with?( a_main )
|
113
|
+
return 0.30 if a_main.start_with?( o_main )
|
114
|
+
return 0.01
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.charset_comparator
|
119
|
+
return lambda { |offer, acceptable|
|
120
|
+
o_main = offer [:main].to_s.downcase
|
121
|
+
a_main = acceptable [:main].to_s.downcase
|
122
|
+
return 1.00 if o_main == a_main
|
123
|
+
return 0.90 if a_main == '*'
|
124
|
+
return 0.01
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
# http://tools.ietf.org/html/rfc2616#section-14.4
|
129
|
+
# http://tools.ietf.org/html/rfc2616#section-3.10
|
130
|
+
#
|
131
|
+
def self.iso_lang_tag_comparator
|
132
|
+
return lambda { |offer, acceptable|
|
133
|
+
#return ->( offer, acceptable ) { # stabby lamdas! :-)
|
134
|
+
o_lang = offer [:main].to_s.downcase.gsub('_','-')
|
135
|
+
a_lang = acceptable [:main].to_s.downcase.gsub('_','-')
|
136
|
+
return 1.00 if o_lang == a_lang
|
137
|
+
return 0.99 if a_lang == '*'
|
138
|
+
o_lang_parts = o_lang.split('-')
|
139
|
+
a_lang_parts = a_lang.split('-')
|
140
|
+
common_len = [ o_lang_parts, a_lang_parts ].map(& :length).min
|
141
|
+
common_len = [ common_len, 20 ].min # max. size
|
142
|
+
while common_len > 0
|
143
|
+
if o_lang_parts.first( common_len ) == a_lang_parts.first( common_len )
|
144
|
+
return 0.6 +
|
145
|
+
(0.005 * common_len) +
|
146
|
+
(o_lang_parts.length > a_lang_parts.length ? 0.002 : -0.002)
|
147
|
+
end
|
148
|
+
common_len -= 1
|
149
|
+
end
|
150
|
+
return 0.00
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
#def self.mime_type_comparator
|
155
|
+
#end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def self.parse_qvals!( acceptables )
|
160
|
+
acceptables.each { |acceptable|
|
161
|
+
qf = 1.0
|
162
|
+
qv = acceptable[:params][Q_KEY]
|
163
|
+
if qv
|
164
|
+
qv.match( /\A\s* (?<qs> [01](\.\d{0,3})? )/x ) { |m|
|
165
|
+
qf = m[:qs].to_f
|
166
|
+
}
|
167
|
+
end
|
168
|
+
acceptable[Q_KEY] = qf
|
169
|
+
}
|
170
|
+
return nil
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.convert_to_lambda( &block )
|
174
|
+
return block if block.lambda?
|
175
|
+
obj = ::Object.new
|
176
|
+
obj.define_singleton_method( :_, &block )
|
177
|
+
return obj.method(:_).to_proc
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: offer_accept_matcher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Philipp Kempgen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-17 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Matches offers and "acceptables".
|
15
|
+
email:
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/offer_accept_matcher.rb
|
21
|
+
- README.md
|
22
|
+
homepage: https://github.com/philipp-kempgen/offer-accept-matcher
|
23
|
+
licenses: []
|
24
|
+
post_install_message:
|
25
|
+
rdoc_options: []
|
26
|
+
require_paths:
|
27
|
+
- lib
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
requirements: []
|
41
|
+
rubyforge_project:
|
42
|
+
rubygems_version: 1.8.25
|
43
|
+
signing_key:
|
44
|
+
specification_version: 3
|
45
|
+
summary: Offer and accept matcher.
|
46
|
+
test_files: []
|
47
|
+
has_rdoc:
|