type_balancer 0.1.4 → 0.2.1

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/docs/balance.md CHANGED
@@ -1,25 +1,124 @@
1
1
  # Detailed Documentation: `TypeBalancer.balance`
2
2
 
3
- `TypeBalancer.balance` is the main method for distributing items of different types across a sequence, ensuring optimal spacing and respecting type ratios. It is highly configurable and supports custom type fields and type orderings.
3
+ `TypeBalancer.balance` is the main method for distributing items of different types across a sequence, ensuring optimal spacing and respecting type ratios. It is highly configurable and supports custom type fields, type orderings, and different balancing strategies.
4
4
 
5
5
  ## Method Signature
6
6
 
7
7
  ```ruby
8
- TypeBalancer.balance(items, type_field: :type, type_order: nil)
8
+ TypeBalancer.balance(items, type_field: :type, type_order: nil, strategy: nil, **strategy_options)
9
9
  ```
10
10
 
11
11
  ### Arguments
12
12
  - `items` (Array<Hash>): The collection of items to balance. Each item should have a type field (default: `:type`).
13
13
  - `type_field` (Symbol/String, optional): The key to use for extracting the type from each item. Default is `:type`.
14
14
  - `type_order` (Array<String>, optional): An array specifying the desired order of types in the output. If omitted, the gem determines the order automatically.
15
+ - `strategy` (Symbol, optional): The balancing strategy to use. Default is `:sliding_window`.
16
+ - `strategy_options` (Hash, optional): Additional options specific to the chosen strategy.
15
17
 
16
- ## Return Value
17
- - Returns a new array of items, balanced by type and spaced as evenly as possible.
18
- - The output array will have the same length as the input.
18
+ ## Available Strategies
19
+
20
+ ### 1. Sliding Window Strategy (default)
21
+ The sliding window strategy is a sophisticated approach that balances items by examining fixed-size windows of items sequentially. For each window, it:
22
+ 1. Calculates the target ratio of each type based on the overall collection
23
+ 2. Ensures minimum representation of each type when possible
24
+ 3. Distributes remaining slots to maintain target ratios
25
+ 4. Handles transitions between windows to maintain smooth distribution
26
+
27
+ **Technical Details:**
28
+ - Default window size: 10 items
29
+ - Minimum representation: Each type gets at least one slot in a window if ratio > 0
30
+ - Ratio preservation: Maintains approximate global ratios while ensuring local diversity
31
+ - Adaptive sizing: Window size automatically adjusts near the end of the collection
32
+
33
+ **Configuration Options:**
34
+ ```ruby
35
+ TypeBalancer.balance(items,
36
+ strategy: :sliding_window,
37
+ window_size: 25, # Size of the sliding window
38
+ type_field: :type, # Field containing type information
39
+ type_order: %w[...] # Optional: preferred type order
40
+ )
41
+ ```
42
+
43
+ **When to Use:**
44
+ 1. **Content Feed Optimization**
45
+ - Perfect for social media feeds, blog lists, or any paginated content
46
+ - Ensures users see a diverse mix regardless of where they stop scrolling
47
+ ```ruby
48
+ TypeBalancer.balance(posts,
49
+ strategy: :sliding_window,
50
+ window_size: 10
51
+ )
52
+ ```
53
+
54
+ 2. **E-commerce Category Display**
55
+ - Balances product types in search results or category pages
56
+ - Maintains category ratios while ensuring variety
57
+ ```ruby
58
+ TypeBalancer.balance(products,
59
+ strategy: :sliding_window,
60
+ window_size: 15,
61
+ type_field: :category
62
+ )
63
+ ```
64
+
65
+ 3. **News Feed Management**
66
+ - Mixes different news categories while maintaining importance
67
+ - Larger windows allow for some natural clustering
68
+ ```ruby
69
+ TypeBalancer.balance(articles,
70
+ strategy: :sliding_window,
71
+ window_size: 25,
72
+ type_order: %w[breaking featured regular]
73
+ )
74
+ ```
75
+
76
+ **Window Size Guidelines:**
77
+ - **Small (5-10 items)**
78
+ - Strictest local balance
79
+ - Best for: Short lists, critical diversity needs
80
+ - Example: Featured content sections
81
+
82
+ - **Medium (15-25 items)**
83
+ - Balanced local/global distribution
84
+ - Best for: Standard content feeds
85
+ - Example: Blog post listings
86
+
87
+ - **Large (30+ items)**
88
+ - More gradual transitions
89
+ - Best for: Long-form content, natural grouping
90
+ - Example: Search results with category clustering
91
+
92
+ **Implementation Notes:**
93
+ - The strategy maintains a queue for each type
94
+ - Window calculations consider both used and available items
95
+ - Edge cases (end of collection, single type) are handled gracefully
96
+ - Performance scales linearly with collection size
97
+
98
+ **Example with Analysis:**
99
+ ```ruby
100
+ # Balance a feed with analytics
101
+ items = [
102
+ { type: 'video', id: 1 },
103
+ { type: 'article', id: 2 },
104
+ # ... more items
105
+ ]
106
+
107
+ balanced = TypeBalancer.balance(items,
108
+ strategy: :sliding_window,
109
+ window_size: 15,
110
+ type_field: :type
111
+ )
112
+
113
+ # Analyze distribution in first window
114
+ first_window = balanced.first(15)
115
+ distribution = first_window.group_by { |i| i[:type] }
116
+ .transform_values(&:count)
117
+ ```
19
118
 
20
119
  ## Usage Examples
21
120
 
22
- ### 1. Basic Balancing
121
+ ### 1. Basic Balancing (Default Strategy)
23
122
  ```ruby
24
123
  items = [
25
124
  { type: 'video', title: 'Video 1' },
@@ -33,11 +132,14 @@ balanced = TypeBalancer.balance(items)
33
132
  # => [ { type: 'article', ... }, { type: 'image', ... }, { type: 'video', ... }, ... ]
34
133
  ```
35
134
 
36
- ### 2. Custom Type Order
135
+ ### 2. Custom Strategy Options
37
136
  ```ruby
38
- # Prioritize images, then videos, then articles
39
- balanced = TypeBalancer.balance(items, type_order: %w[image video article])
40
- # => [ { type: 'image', ... }, { type: 'video', ... }, { type: 'article', ... }, ... ]
137
+ # Large window size for more gradual transitions
138
+ balanced = TypeBalancer.balance(items,
139
+ strategy: :sliding_window,
140
+ window_size: 50,
141
+ type_order: %w[image video article]
142
+ )
41
143
  ```
42
144
 
43
145
  ### 3. Custom Type Field
@@ -64,6 +166,7 @@ If a type in `type_order` is not present in the input, it is simply ignored in t
64
166
  - The `type_order` argument must be an array of strings matching the type values in your items.
65
167
  - If you use a custom `type_field`, ensure all items have that field.
66
168
  - The method does not mutate the input array.
169
+ - Strategy options are specific to each strategy and are ignored by other strategies.
67
170
 
68
171
  ## See Also
69
172
  - [README.md](../README.md) for general usage
data/docs/quality.md CHANGED
@@ -53,15 +53,23 @@ The script tests several key aspects of the TypeBalancer gem:
53
53
  - Shows spacing calculations between positions
54
54
  - Verifies edge cases (single item, no items, all items)
55
55
 
56
- ### 2. Robust Balance Method Tests
56
+ ### 2. Strategy System
57
+ - Tests the default sliding window strategy
58
+ - Verifies behavior with different window sizes
59
+ - Checks strategy options and customization
60
+ - Ensures backward compatibility with existing code
61
+
62
+ ### 3. Robust Balance Method Tests
57
63
  - Loads scenarios from a YAML file (`examples/balance_test_data.yml`)
58
64
  - Tests `TypeBalancer.balance` with and without the `type_order` argument
65
+ - Tests different strategy configurations
59
66
  - Checks type counts, custom order, and exception handling for empty input
60
67
  - Prints a color-coded summary table for pass/fail counts
61
68
 
62
- ### 3. Content Feed Example
69
+ ### 4. Content Feed Example
63
70
  - Shows a real-world example of content type distribution
64
71
  - Verifies position allocation for different content types (video, image, article)
72
+ - Tests strategy behavior with real-world data
65
73
  - Checks distribution statistics and ratios
66
74
 
67
75
  ## Output Format
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # rubocop:disable Metrics/ClassLength
5
+ # rubocop:disable Metrics/MethodLength
6
+ # rubocop:disable Metrics/AbcSize
7
+
8
+ require_relative '../lib/type_balancer'
9
+ require 'yaml'
10
+ require 'json'
11
+
12
+ class LargeScaleBalanceTest
13
+ GREEN = "\e[32m"
14
+ RED = "\e[31m"
15
+ YELLOW = "\e[33m"
16
+ RESET = "\e[0m"
17
+
18
+ def initialize
19
+ @total_records = 500
20
+ @type_distribution = {
21
+ 'type_a' => 250, # 50%
22
+ 'type_b' => 175, # 35%
23
+ 'type_c' => 75 # 15%
24
+ }
25
+ @window_sizes = [10, 25, 50, 100]
26
+ @failures = []
27
+ @tests_run = 0
28
+ @tests_passed = 0
29
+ end
30
+
31
+ def run
32
+ puts "\n#{YELLOW}Running Large Scale Balance Test#{RESET}"
33
+ puts "Total Records: #{@total_records}"
34
+ puts 'Distribution:'
35
+ @type_distribution.each do |type, count|
36
+ puts " #{type}: #{count} (#{(count.to_f / @total_records * 100).round(1)}%)"
37
+ end
38
+
39
+ test_data = generate_test_data
40
+
41
+ # Test default strategy
42
+ puts "\n#{YELLOW}Testing Default Strategy (Sliding Window)#{RESET}"
43
+ run_balance_test(test_data)
44
+
45
+ # Test with different window sizes
46
+ @window_sizes.each do |size|
47
+ puts "\n#{YELLOW}Testing Sliding Window Strategy with Window Size #{size}#{RESET}"
48
+ run_balance_test(test_data, strategy: :sliding_window, window_size: size)
49
+ end
50
+
51
+ # Test with custom type order
52
+ puts "\n#{YELLOW}Testing with Custom Type Order#{RESET}"
53
+ run_balance_test(
54
+ test_data,
55
+ strategy: :sliding_window,
56
+ types: %w[type_c type_b type_a],
57
+ window_size: 25
58
+ )
59
+
60
+ print_summary
61
+ @failures.empty?
62
+ end
63
+
64
+ private
65
+
66
+ def record_failure(message)
67
+ @failures << message
68
+ end
69
+
70
+ def generate_test_data
71
+ items = []
72
+ @type_distribution.each do |type, count|
73
+ count.times do |i|
74
+ items << { type: type, id: "#{type}_#{i + 1}" }
75
+ end
76
+ end
77
+ items.shuffle
78
+ end
79
+
80
+ def run_balance_test(test_data, **options)
81
+ @tests_run += 1
82
+ puts "\nRunning balance test..."
83
+
84
+ begin
85
+ result = TypeBalancer.balance(test_data, type_field: :type, **options)
86
+ analyze_result(result, options)
87
+ @tests_passed += 1
88
+ rescue StandardError => e
89
+ record_failure("Balance test failed: #{e.message}")
90
+ puts "#{RED}Balance test failed: #{e.message}#{RESET}"
91
+ end
92
+ end
93
+
94
+ def analyze_result(result, options)
95
+ window_size = options[:window_size] || 10
96
+ puts "\nAnalyzing windows of size #{window_size}:"
97
+
98
+ result.each_slice(window_size).with_index(1) do |window, index|
99
+ analyze_window(window, index)
100
+ end
101
+
102
+ analyze_overall_distribution(result)
103
+ end
104
+
105
+ def analyze_window(window, window_number)
106
+ puts "\nAnalyzing window #{window_number} (#{window.size} items):"
107
+ type_counts = window.group_by { |item| item[:type] }.transform_values(&:size)
108
+ total_in_window = window.size.to_f
109
+
110
+ type_counts.each do |type, count|
111
+ percentage = (count / total_in_window * 100).round(1)
112
+ target_percentage = (@type_distribution[type].to_f / @total_records * 100).round(1)
113
+ diff = (percentage - target_percentage).abs.round(1)
114
+
115
+ message = "#{type}: #{count} (#{percentage}%), "
116
+ message += if diff <= 15
117
+ "#{GREEN}acceptable deviation#{RESET}"
118
+ else
119
+ "#{RED}high deviation#{RESET}"
120
+ end
121
+
122
+ puts message
123
+ end
124
+ end
125
+
126
+ def analyze_overall_distribution(result)
127
+ puts "\nOverall Distribution:"
128
+ type_counts = result.group_by { |item| item[:type] }.transform_values(&:size)
129
+ total = result.size.to_f
130
+
131
+ type_counts.each do |type, count|
132
+ percentage = (count / total * 100).round(1)
133
+ target_percentage = (@type_distribution[type].to_f / @total_records * 100).round(1)
134
+ diff = (percentage - target_percentage).abs.round(1)
135
+
136
+ message = "#{type}: #{count} (#{percentage}% vs target #{target_percentage}%), "
137
+ message += if diff <= 5
138
+ "#{GREEN}good distribution#{RESET}"
139
+ else
140
+ "#{RED}distribution needs improvement#{RESET}"
141
+ end
142
+
143
+ puts message
144
+ end
145
+ end
146
+
147
+ def print_summary
148
+ puts "\n#{YELLOW}Test Summary:#{RESET}"
149
+ puts "Tests Run: #{@tests_run}"
150
+ puts "Tests Passed: #{@tests_passed}"
151
+ puts "Failures: #{@failures.size}"
152
+
153
+ if @failures.any?
154
+ puts "\n#{RED}Failures:#{RESET}"
155
+ @failures.each { |failure| puts "- #{failure}" }
156
+ else
157
+ puts "\n#{GREEN}All tests passed!#{RESET}"
158
+ end
159
+ end
160
+ end
161
+
162
+ # Run the test
163
+ test = LargeScaleBalanceTest.new
164
+ exit(test.run ? 0 : 1)
165
+
166
+ # rubocop:enable Metrics/ClassLength
167
+ # rubocop:enable Metrics/MethodLength
168
+ # rubocop:enable Metrics/AbcSize
data/examples/quality.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'type_balancer'
3
+ require_relative '../lib/type_balancer'
4
4
  require 'yaml'
5
5
 
6
6
  class QualityChecker
@@ -24,6 +24,7 @@ class QualityChecker
24
24
  check_balance_method_robust
25
25
  check_real_world_feed
26
26
  check_custom_type_field
27
+ check_strategy_system
27
28
 
28
29
  print_summary
29
30
  exit(@issues.empty? ? 0 : 1)
@@ -351,21 +352,93 @@ class QualityChecker
351
352
  print_section_table('custom_type_field', 1, found == expected ? 1 : 0)
352
353
  end
353
354
 
355
+ def check_strategy_system
356
+ puts "\n#{YELLOW}Strategy System Tests:#{RESET}"
357
+ @section_examples_run = 0
358
+ @section_examples_passed = 0
359
+
360
+ # Test default strategy
361
+ @examples_run += 1
362
+ @section_examples_run += 1
363
+ items = [
364
+ { type: 'video', id: 1 },
365
+ { type: 'image', id: 2 },
366
+ { type: 'video', id: 3 }
367
+ ]
368
+ result = TypeBalancer.balance(items, type_field: :type)
369
+ if result.size == items.size && result.map { |i| i[:id] }.sort == [1, 2, 3]
370
+ @examples_passed += 1
371
+ @section_examples_passed += 1
372
+ puts "#{GREEN}Default strategy test passed#{RESET}"
373
+ else
374
+ record_issue("Default strategy test failed: unexpected result #{result.inspect}")
375
+ puts "#{RED}Default strategy test failed#{RESET}"
376
+ end
377
+
378
+ # Test sliding window strategy with custom window size
379
+ @examples_run += 1
380
+ @section_examples_run += 1
381
+ items = [
382
+ { type: 'video', id: 1 },
383
+ { type: 'image', id: 2 },
384
+ { type: 'video', id: 3 },
385
+ { type: 'image', id: 4 }
386
+ ]
387
+ result = TypeBalancer.balance(items, type_field: :type, strategy: :sliding_window, window_size: 2)
388
+ if result.size == items.size &&
389
+ result[0..1].map { |i| i[:type] }.uniq.size == 2 # First window has both types
390
+ @examples_passed += 1
391
+ @section_examples_passed += 1
392
+ puts "#{GREEN}Sliding window strategy with custom window size test passed#{RESET}"
393
+ else
394
+ record_issue("Sliding window strategy test failed: unexpected result #{result.inspect}")
395
+ puts "#{RED}Sliding window strategy with custom window size test failed#{RESET}"
396
+ end
397
+
398
+ # Test strategy with custom type order
399
+ @examples_run += 1
400
+ @section_examples_run += 1
401
+ items = [
402
+ { type: 'video', id: 1 },
403
+ { type: 'image', id: 2 },
404
+ { type: 'article', id: 3 }
405
+ ]
406
+ result = TypeBalancer.balance(
407
+ items,
408
+ type_field: :type,
409
+ strategy: :sliding_window,
410
+ types: %w[image video article]
411
+ )
412
+ if result.size == items.size && result.first[:type] == 'image'
413
+ @examples_passed += 1
414
+ @section_examples_passed += 1
415
+ puts "#{GREEN}Strategy with custom type order test passed#{RESET}"
416
+ else
417
+ record_issue("Strategy with custom type order test failed: unexpected result #{result.inspect}")
418
+ puts "#{RED}Strategy with custom type order test failed#{RESET}"
419
+ end
420
+
421
+ print_section_table('strategy_system')
422
+ end
423
+
354
424
  def print_summary
355
- puts "\n#{'-' * 50}"
356
- puts 'Quality Check Summary:'
425
+ puts "\n#{'-' * 80}"
426
+ puts "#{YELLOW}QUALITY CHECK SUMMARY#{RESET}"
427
+ puts "#{'-' * 80}"
357
428
  puts "Examples Run: #{@examples_run}"
358
- puts "Expectations Passed: #{@examples_passed}"
429
+ puts "Examples Passed: #{@examples_passed}"
430
+ puts "Examples Failed: #{@examples_run - @examples_passed}"
431
+ puts "#{'-' * 80}"
359
432
 
360
433
  if @issues.empty?
361
- puts "\n#{GREEN}All quality checks passed! ✓#{RESET}"
434
+ puts "\n#{GREEN} FINAL STATUS: ALL QUALITY CHECKS PASSED!#{RESET}"
362
435
  else
363
- puts "\n#{RED}Quality check failed with #{@issues.size} issues:#{RESET}"
436
+ puts "\n#{RED} FINAL STATUS: QUALITY CHECKS FAILED WITH #{@issues.size} ISSUES:#{RESET}"
364
437
  @issues.each_with_index do |issue, index|
365
438
  puts "#{RED}#{index + 1}. #{issue}#{RESET}"
366
439
  end
367
440
  end
368
- puts "#{'-' * 50}"
441
+ puts "#{'-' * 80}"
369
442
  end
370
443
 
371
444
  # Print a summary table for a section
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'strategy_factory'
4
+ require_relative 'strategies/base_strategy'
5
+ require_relative 'strategies/sliding_window_strategy'
6
+
3
7
  module TypeBalancer
4
8
  # Handles calculation of positions for balanced item distribution
5
9
  class PositionCalculator
@@ -80,6 +84,7 @@ module TypeBalancer
80
84
 
81
85
  def calculate_evenly_spaced_positions(total_count, target_count, ratio)
82
86
  return [0] if target_count == 1
87
+ return handle_two_positions_in_three_slots if target_count == 2 && total_count == 3
83
88
  return handle_two_thirds_case(total_count) if two_thirds_ratio?(ratio, total_count)
84
89
 
85
90
  (0...target_count).map do |i|
@@ -95,6 +100,10 @@ module TypeBalancer
95
100
  [0, 1]
96
101
  end
97
102
 
103
+ def handle_two_positions_in_three_slots
104
+ [0, 1]
105
+ end
106
+
98
107
  def valid_inputs?(total_count, ratio)
99
108
  total_count >= 0 && ratio >= 0 && ratio <= 1.0
100
109
  end
@@ -105,114 +114,52 @@ module TypeBalancer
105
114
  class Calculator
106
115
  DEFAULT_TYPE_ORDER = %w[video image strip article].freeze
107
116
 
108
- def initialize(items, type_field: :type, types: nil)
117
+ # rubocop:disable Metrics/ParameterLists
118
+ def initialize(items, type_field: :type, types: nil, type_order: nil, strategy: nil, **strategy_options)
109
119
  raise ArgumentError, 'Items cannot be nil' if items.nil?
110
120
  raise ArgumentError, 'Type field cannot be nil' if type_field.nil?
111
121
 
112
122
  @items = items
113
123
  @type_field = type_field
114
- @types = types || extract_types
124
+ @types = types
125
+ @type_order = type_order
126
+ @strategy_name = strategy
127
+ @strategy_options = strategy_options
115
128
  end
129
+ # rubocop:enable Metrics/ParameterLists
116
130
 
117
131
  def call
118
132
  return [] if @items.empty?
119
133
 
120
- validate_items!
121
-
122
- items_by_type = @types.map { |type| @items.select { |item| item[@type_field].to_s == type } }
123
-
124
- # Calculate target positions for each type
125
- target_positions = calculate_target_positions(items_by_type)
126
-
127
- # Place items at their target positions
128
- place_items_at_positions(items_by_type, target_positions)
134
+ # Create strategy instance with all options
135
+ strategy = StrategyFactory.create(
136
+ @strategy_name,
137
+ items: @items,
138
+ type_field: @type_field,
139
+ types: @types || extract_types,
140
+ type_order: @type_order,
141
+ **@strategy_options
142
+ )
143
+
144
+ # Balance items using strategy
145
+ strategy.balance
129
146
  end
130
147
 
131
148
  private
132
149
 
133
- def validate_items!
134
- @items.each do |item|
135
- raise ArgumentError, 'All items must have a type field' unless item.key?(@type_field)
136
- raise ArgumentError, 'Type values cannot be empty' if item[@type_field].to_s.strip.empty?
137
- end
138
- end
139
-
140
150
  def extract_types
141
151
  types = @items.map { |item| item[@type_field].to_s }.uniq
142
- DEFAULT_TYPE_ORDER.select { |type| types.include?(type) } + (types - DEFAULT_TYPE_ORDER)
143
- end
144
-
145
- def calculate_target_positions(items_by_type)
146
- total_count = @items.size
147
- available_positions = (0...total_count).to_a
148
-
149
- items_by_type.map.with_index do |_items, index|
150
- ratio = calculate_ratio(items_by_type.size, index)
151
- target_count = (total_count * ratio).round
152
-
153
- # Calculate positions based on ratio and total count
154
- if target_count == 1
155
- [index]
156
- else
157
- # For better distribution, calculate positions based on available slots
158
- step = available_positions.size.fdiv(target_count)
159
- positions = (0...target_count).map do |i|
160
- pos_index = (i * step).round
161
- available_positions[pos_index]
162
- end
163
-
164
- # Remove used positions from available ones
165
- positions.each { |pos| available_positions.delete(pos) }
166
- positions
167
- end
168
- end
169
- end
170
-
171
- def calculate_ratio(type_count, index)
172
- case type_count
173
- when 1 then 1.0
174
- when 2 then index.zero? ? 0.6 : 0.4
152
+ if @type_order
153
+ # First include ordered types that exist in the items
154
+ ordered = @type_order & types
155
+ # Then append any remaining types that weren't in the order
156
+ ordered + (types - @type_order)
175
157
  else
176
- # For 3+ types: first type gets 0.4, rest split remaining 0.6 evenly
177
- remaining = (0.6 / (type_count - 1).to_f).round(6)
178
- index.zero? ? 0.4 : remaining
179
- end
180
- end
181
-
182
- def place_items_at_positions(items_by_type, target_positions)
183
- result = Array.new(@items.size)
184
- used_items = place_items_at_target_positions(items_by_type, target_positions, result)
185
- fill_empty_slots(result, used_items)
186
- result.compact
187
- end
188
-
189
- def place_items_at_target_positions(items_by_type, target_positions, result)
190
- used_items = []
191
- items_by_type.each_with_index do |items, type_index|
192
- positions = target_positions[type_index] || []
193
- place_type_items(items, positions, result, used_items)
194
- end
195
- used_items
196
- end
197
-
198
- def place_type_items(items, positions, result, used_items)
199
- items.take(positions.size).each_with_index do |item, item_index|
200
- pos = positions[item_index]
201
- next unless pos && result[pos].nil?
202
-
203
- result[pos] = item
204
- used_items << item
205
- end
206
- end
207
-
208
- def fill_empty_slots(result, used_items)
209
- remaining_items = @items - used_items
210
- empty_slots = result.each_index.select { |i| result[i].nil? }
211
- empty_slots.zip(remaining_items).each do |slot, item|
212
- break unless item
213
-
214
- result[slot] = item
158
+ DEFAULT_TYPE_ORDER.select { |type| types.include?(type) } + (types - DEFAULT_TYPE_ORDER)
215
159
  end
216
160
  end
217
161
  end
218
162
  end
163
+
164
+ # Register default strategy
165
+ TypeBalancer::StrategyFactory.register(:sliding_window, TypeBalancer::Strategies::SlidingWindowStrategy)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeBalancer
4
+ # Configuration class to handle all balancing options
5
+ class Configuration
6
+ attr_accessor :type_field, :type_order, :strategy, :window_size, :batch_size, :types
7
+ attr_reader :strategy_options
8
+
9
+ def initialize(options = {})
10
+ @type_field = options.fetch(:type_field, :type)
11
+ @type_order = options[:type_order]
12
+ @strategy = options[:strategy]
13
+ @window_size = options[:window_size]
14
+ @batch_size = options[:batch_size]
15
+ @types = options[:types]
16
+ @strategy_options = extract_strategy_options(options)
17
+ end
18
+
19
+ def merge_window_size
20
+ return strategy_options unless window_size
21
+
22
+ strategy_options.merge(window_size: window_size)
23
+ end
24
+
25
+ private
26
+
27
+ def extract_strategy_options(options)
28
+ options.reject { |key, _| %i[type_field type_order strategy window_size types].include?(key) }
29
+ end
30
+ end
31
+ end