weighted_list_rank 0.6.0 → 0.7.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/Gemfile.lock +9 -9
- data/README.md +49 -0
- data/lib/weighted_list_rank/context.rb +32 -22
- data/lib/weighted_list_rank/strategies/exponential.rb +107 -49
- data/lib/weighted_list_rank/strategy.rb +17 -0
- data/lib/weighted_list_rank/version.rb +1 -1
- data/weighted_list_rank.gemspec +42 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e8758848cc7ed9090ee9348afeb2efd6fe7d27506b00a2dd997ffce53959615
|
|
4
|
+
data.tar.gz: ff68f02846e841ac26fb3863d50b1309543c2d1e8368024ff01e7de44041cb23
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf99a1e72f8dc738541b1002ef1ddb9d590a198ac957aa59e2ee570b4dcf82d9702f78291459ad2d9f98250482a7b421809e3a0aa557f445454b0545c914e43d
|
|
7
|
+
data.tar.gz: d9f13250eac45ebe3466c8020a16a5f290fa693174d574d64e35f17010cf0624993c0d575d53d319b7b21510a9a8e022af625b4bdedf39f9c7b64d4952c77e7c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [0.7.0] - 2026-08-23
|
|
2
|
+
### Performance
|
|
3
|
+
- Ranking a list is now linear in the number of items instead of quadratic. `Exponential#calculate_score` recomputed the ranked-item count and the total exponential factor for every item, even though both are constant across a list. They are now computed once per list.
|
|
4
|
+
- Measured on synthetic data shaped like a real catalogue: 700 lists / ~19k entries went from 226ms to 13ms (17x). The gain grows with the size of your longest list — a single 1,600-item list went from 217ms to 1.4ms (158x).
|
|
5
|
+
- Ranking output is unchanged. Scores, `score_details`, and ordering are bit-for-bit identical to 0.6.0, verified across four data profiles and a 3,000-case edge-case matrix.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `Strategy#calculate_scores(list)`, which returns an array of scores positionally matching `list.items`. This is now the entry point `RankingContext` uses. The default implementation delegates to `calculate_score` for each item, so existing custom strategies keep working unchanged. Strategies with per-list invariants should override it — see "Writing a Custom Strategy" in the README.
|
|
9
|
+
- `warn_on_invalid_position` option on the `Exponential` strategy, defaulting to `false`.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Items whose position exceeds the number of items in their list are still clamped to the last position, but this is no longer reported by default. Previously every such item printed a warning to `stdout`, which was noisy inside an application. Enable `warn_on_invalid_position: true` to get a single `stderr` warning per affected list.
|
|
13
|
+
- Updated all development dependencies to their latest versions.
|
|
14
|
+
|
|
1
15
|
## [0.6.0] - 2025-06-29
|
|
2
16
|
- Added `list_count_penalties` feature to `RankingContext` to allow penalizing items based on the number of lists they appear on. This is useful for de-emphasizing items that are not widely represented across your data.
|
|
3
17
|
- The feature accepts a hash mapping list counts to penalty percentages (e.g., `{1 => 0.50, 2 => 0.25}` to penalize items on 1 list by 50% and items on 2 lists by 25%).
|
data/Gemfile.lock
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
weighted_list_rank (0.
|
|
4
|
+
weighted_list_rank (0.7.0)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
8
8
|
specs:
|
|
9
9
|
ast (2.4.3)
|
|
10
10
|
json (2.12.2)
|
|
11
|
-
language_server-protocol (3.17.0.
|
|
11
|
+
language_server-protocol (3.17.0.6)
|
|
12
12
|
lint_roller (1.1.0)
|
|
13
|
-
minitest (5.
|
|
13
|
+
minitest (5.27.0)
|
|
14
14
|
parallel (1.27.0)
|
|
15
|
-
parser (3.3.
|
|
15
|
+
parser (3.3.12.0)
|
|
16
16
|
ast (~> 2.4.1)
|
|
17
17
|
racc
|
|
18
18
|
prism (1.4.0)
|
|
19
19
|
racc (1.8.1)
|
|
20
20
|
rainbow (3.1.1)
|
|
21
|
-
rake (13.
|
|
21
|
+
rake (13.4.2)
|
|
22
22
|
regexp_parser (2.10.0)
|
|
23
23
|
rubocop (1.75.8)
|
|
24
24
|
json (~> 2.3)
|
|
@@ -34,7 +34,7 @@ GEM
|
|
|
34
34
|
rubocop-ast (1.45.1)
|
|
35
35
|
parser (>= 3.3.7.2)
|
|
36
36
|
prism (~> 1.4)
|
|
37
|
-
rubocop-minitest (0.
|
|
37
|
+
rubocop-minitest (0.40.0)
|
|
38
38
|
lint_roller (~> 1.1)
|
|
39
39
|
rubocop (>= 1.75.0, < 2.0)
|
|
40
40
|
rubocop-ast (>= 1.38.0, < 2.0)
|
|
@@ -58,9 +58,9 @@ GEM
|
|
|
58
58
|
standard-performance (1.8.0)
|
|
59
59
|
lint_roller (~> 1.1)
|
|
60
60
|
rubocop-performance (~> 1.25.0)
|
|
61
|
-
unicode-display_width (3.
|
|
62
|
-
unicode-emoji (~> 4.
|
|
63
|
-
unicode-emoji (4.0
|
|
61
|
+
unicode-display_width (3.2.0)
|
|
62
|
+
unicode-emoji (~> 4.1)
|
|
63
|
+
unicode-emoji (4.2.0)
|
|
64
64
|
|
|
65
65
|
PLATFORMS
|
|
66
66
|
arm64-darwin-24
|
data/README.md
CHANGED
|
@@ -229,6 +229,55 @@ ranked_items = ranking_context.rank([list1, list2, list3])
|
|
|
229
229
|
- This feature is fully backwards compatible: if you do not provide `list_count_penalties`, no penalties are applied.
|
|
230
230
|
- This penalty stacks with individual item penalties (e.g., `score_penalty` on an item).
|
|
231
231
|
|
|
232
|
+
### Handling Invalid Positions
|
|
233
|
+
If a list contains an item whose `position` is greater than the number of items in that list, the position is clamped to the last position. This is silent by default, so the gem stays quiet inside an application.
|
|
234
|
+
|
|
235
|
+
While investigating your data you can opt into a warning. It is written to `stderr`, and at most one is emitted per affected list no matter how many items are involved:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
strategy = WeightedListRank::Strategies::Exponential.new(warn_on_invalid_position: true)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
WeightedListRank: list 42 has 3 item(s) with a position greater than the number of items in the list (25); clamping to 25.
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Writing a Custom Strategy
|
|
246
|
+
A strategy needs to implement `calculate_score(list, item)`:
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
class MyStrategy < WeightedListRank::Strategy
|
|
250
|
+
def calculate_score(list, item)
|
|
251
|
+
list.weight * (1.0 / item.position)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`RankingContext` scores a list through `calculate_scores(list)`, which returns an array of scores positionally matching `list.items`. The default implementation calls `calculate_score` once per item, so implementing only the single-item method is enough to get a working strategy.
|
|
257
|
+
|
|
258
|
+
If your scoring depends on values that are constant across the list — a total, a count, a normalizing factor — override `calculate_scores` and compute them once. Recomputing per-list values inside `calculate_score` makes ranking quadratic in list length, which is what the built-in `Exponential` strategy used to do:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
class MyStrategy < WeightedListRank::Strategy
|
|
262
|
+
def calculate_scores(list)
|
|
263
|
+
items = list.items
|
|
264
|
+
total = items.sum { |i| i.position || 0 } # computed once for the whole list
|
|
265
|
+
|
|
266
|
+
items.map { |item| score_for(list, item, total) }
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def calculate_score(list, item)
|
|
270
|
+
score_for(list, item, list.items.sum { |i| i.position || 0 })
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
private
|
|
274
|
+
|
|
275
|
+
def score_for(list, item, total)
|
|
276
|
+
list.weight * (item.position.to_f / total)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
232
281
|
## Development
|
|
233
282
|
|
|
234
283
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
@@ -19,40 +19,50 @@ module WeightedListRank
|
|
|
19
19
|
# @return [Array<Hash>] a sorted array of item scores, with each item's details including ID, score details, and total score.
|
|
20
20
|
def rank(lists)
|
|
21
21
|
items = {}
|
|
22
|
+
|
|
23
|
+
# Strategies subclassing Strategy always have this; the fallback covers
|
|
24
|
+
# duck-typed strategies that only define #calculate_score.
|
|
25
|
+
batch_capable = strategy.respond_to?(:calculate_scores)
|
|
26
|
+
|
|
22
27
|
lists.each do |list|
|
|
23
|
-
list.items
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
list_items = list.items
|
|
29
|
+
scores = if batch_capable
|
|
30
|
+
strategy.calculate_scores(list)
|
|
31
|
+
else
|
|
32
|
+
list_items.map { |item| strategy.calculate_score(list, item) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
list_id = list.id
|
|
36
|
+
weight = list.weight
|
|
37
|
+
|
|
38
|
+
list_items.each_with_index do |item, index|
|
|
39
|
+
score = scores[index]
|
|
40
|
+
details = (items[item.id] ||= {list_details: [], total_score: 0})
|
|
41
|
+
details[:list_details] << {list_id: list_id, score: score, weight: weight, score_penalty: item.score_penalty}
|
|
42
|
+
details[:total_score] += score
|
|
33
43
|
end
|
|
34
44
|
end
|
|
35
45
|
|
|
46
|
+
apply_penalties = !list_count_penalties.empty?
|
|
47
|
+
|
|
36
48
|
# Convert hash to a formatted array
|
|
37
49
|
formatted_items = items.map do |id, details|
|
|
50
|
+
list_details = details[:list_details]
|
|
51
|
+
total_score = details[:total_score]
|
|
52
|
+
|
|
53
|
+
# Apply list count penalties if configured
|
|
54
|
+
if apply_penalties && (penalty = list_count_penalties[list_details.length])
|
|
55
|
+
total_score *= (1 - penalty)
|
|
56
|
+
end
|
|
57
|
+
|
|
38
58
|
{
|
|
39
59
|
id: id,
|
|
40
60
|
# Sort the score_details array by score in descending order before including it
|
|
41
|
-
score_details:
|
|
42
|
-
total_score:
|
|
61
|
+
score_details: list_details.sort_by { |detail| -detail[:score] },
|
|
62
|
+
total_score: total_score
|
|
43
63
|
}
|
|
44
64
|
end
|
|
45
65
|
|
|
46
|
-
# Apply list count penalties if configured
|
|
47
|
-
if !list_count_penalties.empty?
|
|
48
|
-
formatted_items.each do |item|
|
|
49
|
-
list_count = item[:score_details].length
|
|
50
|
-
if (penalty = list_count_penalties[list_count])
|
|
51
|
-
item[:total_score] *= (1 - penalty)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
66
|
# Sort the array by total_score in descending order
|
|
57
67
|
formatted_items.sort_by { |item| -item[:total_score] }
|
|
58
68
|
end
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module WeightedListRank
|
|
2
2
|
module Strategies
|
|
3
3
|
class Exponential < WeightedListRank::Strategy
|
|
4
|
-
attr_reader :exponent, :bonus_pool_percentage, :average_list_length, :include_unranked_items
|
|
4
|
+
attr_reader :exponent, :bonus_pool_percentage, :average_list_length, :include_unranked_items,
|
|
5
|
+
:warn_on_invalid_position
|
|
5
6
|
|
|
6
7
|
# Initializes the Exponential strategy with optional parameters for exponent,
|
|
7
8
|
# bonus pool percentage, average list length, and whether to include unranked items in the bonus pool.
|
|
@@ -13,87 +14,144 @@ module WeightedListRank
|
|
|
13
14
|
# defaults to nil.
|
|
14
15
|
# @param include_unranked_items [Boolean] whether to include unranked items in the bonus pool calculation,
|
|
15
16
|
# defaults to false for backward compatibility.
|
|
16
|
-
|
|
17
|
+
# @param warn_on_invalid_position [Boolean] whether to emit a warning when a list contains items whose
|
|
18
|
+
# position exceeds the number of items in the list. Such positions are always clamped to the last position;
|
|
19
|
+
# this only controls whether that is reported. Defaults to false, so scoring is silent.
|
|
20
|
+
def initialize(exponent: 1.5, bonus_pool_percentage: 1.0, average_list_length: nil,
|
|
21
|
+
include_unranked_items: false, warn_on_invalid_position: false)
|
|
17
22
|
@exponent = exponent
|
|
18
23
|
@bonus_pool_percentage = bonus_pool_percentage
|
|
19
24
|
@average_list_length = average_list_length
|
|
20
25
|
@include_unranked_items = include_unranked_items
|
|
26
|
+
@warn_on_invalid_position = warn_on_invalid_position
|
|
21
27
|
end
|
|
22
28
|
|
|
23
|
-
# Calculates the
|
|
29
|
+
# Calculates the scores for every item in a list in one pass.
|
|
30
|
+
#
|
|
31
|
+
# The number of ranked items, the adjusted bonus pool, and the total
|
|
32
|
+
# exponential factor are all constant across the list, so they are
|
|
33
|
+
# computed once here instead of once per item. Scoring a list is
|
|
34
|
+
# therefore linear in the number of items.
|
|
35
|
+
#
|
|
36
|
+
# @param list [WeightedListRank::List] the list whose items should be scored.
|
|
37
|
+
# @return [Array<Float>] scores positionally matching +list.items+.
|
|
38
|
+
def calculate_scores(list)
|
|
39
|
+
items = list.items
|
|
40
|
+
total_items = items.count
|
|
41
|
+
return [] if total_items.zero?
|
|
42
|
+
|
|
43
|
+
num_ranked_items = count_ranked(items)
|
|
44
|
+
adjusted_bonus_pool = adjusted_bonus_pool_for(list, total_items)
|
|
45
|
+
|
|
46
|
+
# Only lists containing ranked items need this, and it is the most
|
|
47
|
+
# expensive value to produce, so it is computed on first use.
|
|
48
|
+
total_exponential_factor = nil
|
|
49
|
+
invalid_positions = 0
|
|
50
|
+
|
|
51
|
+
scores = items.map do |item|
|
|
52
|
+
position = item.position
|
|
53
|
+
|
|
54
|
+
if position.nil?
|
|
55
|
+
score = list.weight
|
|
56
|
+
if num_ranked_items.zero? && include_unranked_items
|
|
57
|
+
score += adjusted_bonus_pool / total_items
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
if position > total_items
|
|
61
|
+
invalid_positions += 1
|
|
62
|
+
position = total_items
|
|
63
|
+
end
|
|
64
|
+
total_exponential_factor ||= exponential_factor_sum(total_items)
|
|
65
|
+
exponential_factor = (total_items + 1 - position)**exponent
|
|
66
|
+
score = list.weight + (exponential_factor / total_exponential_factor) * adjusted_bonus_pool
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
floor(apply_penalty(score, item.score_penalty))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
report_invalid_positions(list, invalid_positions, total_items)
|
|
73
|
+
scores
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Calculates the score of a single item within a list based on its rank position, the total number of items,
|
|
24
77
|
# and the list's weight, using an exponential formula. The bonus pool for score adjustments is determined
|
|
25
78
|
# by the specified bonus pool percentage of the list's total weight, adjusted by the average list length.
|
|
26
79
|
#
|
|
27
80
|
# If +include_unranked_items+ is true, unranked items will also receive a portion of the bonus pool.
|
|
28
81
|
# Ranked items will receive an exponential bonus, while unranked items will split the remaining bonus pool evenly.
|
|
29
82
|
#
|
|
83
|
+
# Scoring a whole list is cheaper through {#calculate_scores}, which shares the per-list work across items.
|
|
84
|
+
#
|
|
30
85
|
# @param list [WeightedListRank::List] the list containing the item being scored.
|
|
31
86
|
# @param item [WeightedListRank::Item] the item for which to calculate the score.
|
|
32
87
|
#
|
|
33
88
|
# @return [Float] the calculated score for the item, adjusted by the list's weight, the specified exponent,
|
|
34
89
|
# and the bonus pool percentage.
|
|
35
90
|
def calculate_score(list, item)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
total_items = list.items.count
|
|
41
|
-
|
|
42
|
-
# Separate ranked and unranked items
|
|
43
|
-
ranked_items = list.items.select { |i| i.position }
|
|
44
|
-
num_ranked_items = ranked_items.count
|
|
91
|
+
items = list.items
|
|
92
|
+
total_items = items.count
|
|
93
|
+
num_ranked_items = count_ranked(items)
|
|
94
|
+
position = item.position
|
|
45
95
|
|
|
46
|
-
if
|
|
47
|
-
# If there are ranked items, unranked items get no bonus, only the list's weight
|
|
96
|
+
if position.nil?
|
|
48
97
|
score = list.weight
|
|
98
|
+
if num_ranked_items.zero? && include_unranked_items
|
|
99
|
+
score += adjusted_bonus_pool_for(list, total_items) / total_items
|
|
100
|
+
end
|
|
49
101
|
else
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# Adjust the bonus pool based on the average list length
|
|
54
|
-
adjusted_bonus_pool = if average_list_length && average_list_length > 0
|
|
55
|
-
total_bonus_pool * (total_items / average_list_length.to_f)
|
|
56
|
-
else
|
|
57
|
-
total_bonus_pool
|
|
102
|
+
if position > total_items
|
|
103
|
+
report_invalid_positions(list, 1, total_items)
|
|
104
|
+
position = total_items
|
|
58
105
|
end
|
|
106
|
+
exponential_factor = (total_items + 1 - position)**exponent
|
|
107
|
+
score = list.weight +
|
|
108
|
+
(exponential_factor / exponential_factor_sum(total_items)) * adjusted_bonus_pool_for(list, total_items)
|
|
109
|
+
end
|
|
59
110
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if include_unranked_items && num_ranked_items == 0
|
|
63
|
-
unranked_bonus = adjusted_bonus_pool / total_items
|
|
64
|
-
score += unranked_bonus
|
|
65
|
-
end
|
|
66
|
-
else
|
|
67
|
-
# Check if the item's position is higher than the total number of items
|
|
68
|
-
if item.position > total_items
|
|
69
|
-
puts "Warning: Item position (#{item.position}) is higher than the total number of items (#{total_items}) in the list. Using total items as position."
|
|
70
|
-
item_position = total_items
|
|
71
|
-
else
|
|
72
|
-
item_position = item.position
|
|
73
|
-
end
|
|
111
|
+
floor(apply_penalty(score, item.score_penalty))
|
|
112
|
+
end
|
|
74
113
|
|
|
75
|
-
|
|
76
|
-
exponential_factor = (total_items + 1 - item_position)**exponent
|
|
77
|
-
total_exponential_factor = (1..total_items).sum { |pos| (total_items + 1 - pos)**exponent }
|
|
114
|
+
private
|
|
78
115
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
116
|
+
def count_ranked(items)
|
|
117
|
+
count = 0
|
|
118
|
+
items.each { |item| count += 1 if item.position }
|
|
119
|
+
count
|
|
120
|
+
end
|
|
84
121
|
|
|
85
|
-
|
|
86
|
-
|
|
122
|
+
def adjusted_bonus_pool_for(list, total_items)
|
|
123
|
+
total_bonus_pool = list.weight * bonus_pool_percentage
|
|
87
124
|
|
|
88
|
-
|
|
89
|
-
|
|
125
|
+
if average_list_length && average_list_length > 0
|
|
126
|
+
total_bonus_pool * (total_items / average_list_length.to_f)
|
|
127
|
+
else
|
|
128
|
+
total_bonus_pool
|
|
129
|
+
end
|
|
90
130
|
end
|
|
91
131
|
|
|
92
|
-
|
|
132
|
+
# The sum of every item's exponential factor, used to normalize each
|
|
133
|
+
# item's share of the bonus pool. Constant for a given list length.
|
|
134
|
+
def exponential_factor_sum(total_items)
|
|
135
|
+
(1..total_items).sum { |pos| (total_items + 1 - pos)**exponent }
|
|
136
|
+
end
|
|
93
137
|
|
|
94
138
|
def apply_penalty(score, penalty)
|
|
95
139
|
penalty ? score * (1 - penalty) : score
|
|
96
140
|
end
|
|
141
|
+
|
|
142
|
+
# Ensure the score is not less than 1.
|
|
143
|
+
def floor(score)
|
|
144
|
+
(score < 1) ? 1 : score
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Emits at most one warning per list, and only when explicitly enabled.
|
|
148
|
+
def report_invalid_positions(list, count, total_items)
|
|
149
|
+
return unless warn_on_invalid_position
|
|
150
|
+
return if count.zero?
|
|
151
|
+
|
|
152
|
+
warn "WeightedListRank: list #{list.id} has #{count} item(s) with a position greater than the " \
|
|
153
|
+
"number of items in the list (#{total_items}); clamping to #{total_items}."
|
|
154
|
+
end
|
|
97
155
|
end
|
|
98
156
|
end
|
|
99
157
|
end
|
|
@@ -7,5 +7,22 @@ module WeightedListRank
|
|
|
7
7
|
def calculate_score(list, item)
|
|
8
8
|
raise NotImplementedError
|
|
9
9
|
end
|
|
10
|
+
|
|
11
|
+
# Calculates the scores for every item in a list, returned in the same order
|
|
12
|
+
# as +list.items+.
|
|
13
|
+
#
|
|
14
|
+
# This is the entry point {RankingContext} uses. The default implementation
|
|
15
|
+
# simply delegates to {#calculate_score} for each item, so strategies that
|
|
16
|
+
# implement only the single-item API continue to work unchanged.
|
|
17
|
+
#
|
|
18
|
+
# Strategies whose scoring depends on values that are constant across the
|
|
19
|
+
# list (a total, a count, a normalizing factor) should override this and
|
|
20
|
+
# compute those values once, rather than recomputing them for every item.
|
|
21
|
+
#
|
|
22
|
+
# @param list [List] the list whose items should be scored.
|
|
23
|
+
# @return [Array<Numeric>] scores positionally matching +list.items+.
|
|
24
|
+
def calculate_scores(list)
|
|
25
|
+
list.items.map { |item| calculate_score(list, item) }
|
|
26
|
+
end
|
|
10
27
|
end
|
|
11
28
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/weighted_list_rank/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "weighted_list_rank"
|
|
7
|
+
spec.version = WeightedListRank::VERSION
|
|
8
|
+
spec.authors = ["Shane Sherman"]
|
|
9
|
+
spec.email = ["shane.sherman@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "generate ranks of items from weighted lists"
|
|
12
|
+
spec.description = "generate ranks of items from weighted lists"
|
|
13
|
+
spec.homepage = "https://github.com/ssherman/weighted_list_rank"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 2.6.0"
|
|
16
|
+
|
|
17
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
|
18
|
+
|
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/ssherman/weighted_list_rank"
|
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/ssherman/weighted_list_rank/CHANGELOG.md"
|
|
22
|
+
|
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
27
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
spec.bindir = "exe"
|
|
31
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
32
|
+
spec.require_paths = ["lib"]
|
|
33
|
+
|
|
34
|
+
# Uncomment to register a new dependency of your gem
|
|
35
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
|
36
|
+
spec.add_development_dependency "rubocop", "~> 1.0"
|
|
37
|
+
spec.add_development_dependency "standard", ">= 1.35.1"
|
|
38
|
+
spec.add_development_dependency "rubocop-minitest", "~> 0.3"
|
|
39
|
+
spec.add_development_dependency "rubocop-rake", "~> 0.6"
|
|
40
|
+
# For more information and examples about making a new gem, check out our
|
|
41
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
42
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: weighted_list_rank
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shane Sherman
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rubocop
|
|
@@ -88,6 +88,7 @@ files:
|
|
|
88
88
|
- lib/weighted_list_rank/strategy.rb
|
|
89
89
|
- lib/weighted_list_rank/version.rb
|
|
90
90
|
- sig/weighted_list_rank.rbs
|
|
91
|
+
- weighted_list_rank.gemspec
|
|
91
92
|
homepage: https://github.com/ssherman/weighted_list_rank
|
|
92
93
|
licenses:
|
|
93
94
|
- MIT
|
|
@@ -109,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
109
110
|
- !ruby/object:Gem::Version
|
|
110
111
|
version: '0'
|
|
111
112
|
requirements: []
|
|
112
|
-
rubygems_version:
|
|
113
|
+
rubygems_version: 4.0.16
|
|
113
114
|
specification_version: 4
|
|
114
115
|
summary: generate ranks of items from weighted lists
|
|
115
116
|
test_files: []
|