offer_accept_matcher 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.md +33 -0
  2. data/lib/offer_accept_matcher.rb +181 -0
  3. metadata +47 -0
@@ -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: