offer_accept_matcher 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|