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.
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: