type_balancer 0.1.3 → 0.2.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.
data/examples/quality.rb CHANGED
@@ -23,6 +23,8 @@ class QualityChecker
23
23
  check_available_positions_edge_cases
24
24
  check_balance_method_robust
25
25
  check_real_world_feed
26
+ check_custom_type_field
27
+ check_strategy_system
26
28
 
27
29
  print_summary
28
30
  exit(@issues.empty? ? 0 : 1)
@@ -328,6 +330,97 @@ class QualityChecker
328
330
  puts " Balanced items with custom order: #{ordered_result.map { |i| i[:type] }.inspect}"
329
331
  end
330
332
 
333
+ def check_custom_type_field
334
+ @examples_run += 1
335
+ puts "\nCustom Type Field Example:"
336
+ data = [
337
+ { category: 'A', payload: 1 },
338
+ { category: 'B', payload: 2 },
339
+ { category: 'C', payload: 3 },
340
+ { category: 'A', payload: 4 }
341
+ ]
342
+ balanced = TypeBalancer.balance(data, type_field: :category)
343
+ found = balanced.map { |i| i[:category] }.uniq.sort
344
+ expected = %w[A B C]
345
+ if found == expected
346
+ @examples_passed += 1
347
+ puts "#{GREEN}Custom field respected: #{found.inspect}#{RESET}"
348
+ else
349
+ record_issue("Expected #{expected.inspect}, got #{found.inspect}")
350
+ puts "#{RED}Custom field test failed: #{found.inspect}#{RESET}"
351
+ end
352
+ print_section_table('custom_type_field', 1, found == expected ? 1 : 0)
353
+ end
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
+
331
424
  def print_summary
332
425
  puts "\n#{'-' * 50}"
333
426
  puts 'Quality Check Summary:'
@@ -3,6 +3,7 @@
3
3
  require_relative 'ratio_calculator'
4
4
  require_relative 'batch_processing'
5
5
  require_relative 'position_calculator'
6
+ require_relative 'type_extractor_registry'
6
7
 
7
8
  module TypeBalancer
8
9
  # Handles balancing of items across batches based on type ratios
@@ -10,9 +11,11 @@ module TypeBalancer
10
11
  # Initialize a new Balancer instance
11
12
  #
12
13
  # @param types [Array<String>, nil] Optional types
14
+ # @param type_field [Symbol] Field to use for type extraction (default: :type)
13
15
  # @param type_order [Array<String>, nil] Optional order of types
14
- def initialize(types = nil, type_order: nil)
16
+ def initialize(types = nil, type_field: :type, type_order: nil)
15
17
  @types = Array(types) if types
18
+ @type_field = type_field
16
19
  @type_order = type_order
17
20
  validate_types! if @types
18
21
  end
@@ -23,7 +26,18 @@ module TypeBalancer
23
26
  # @return [Array] Balanced items
24
27
  def call(collection)
25
28
  validate_collection!(collection)
26
- items_by_type = group_items_by_type(collection)
29
+ extractor = TypeExtractorRegistry.get(@type_field)
30
+
31
+ begin
32
+ items_by_type = extractor.group_by_type(collection)
33
+ rescue TypeBalancer::Error => e
34
+ raise TypeBalancer::Error, "Cannot access type field '#{@type_field}': #{e.message}"
35
+ end
36
+
37
+ # Remove nil types and validate
38
+ items_by_type.delete(nil)
39
+ raise TypeBalancer::Error, "Cannot access type field '#{@type_field}'" if items_by_type.empty?
40
+
27
41
  validate_types_in_collection!(items_by_type)
28
42
 
29
43
  target_counts = calculate_target_counts(items_by_type)
@@ -70,24 +84,7 @@ module TypeBalancer
70
84
  raise TypeBalancer::Error, "Invalid type(s): #{invalid_types.join(', ')}" if invalid_types.any?
71
85
  end
72
86
 
73
- def group_items_by_type(collection)
74
- collection.group_by do |item|
75
- extract_type(item)
76
- end
77
- end
78
-
79
- def extract_type(item)
80
- return item[:type] || item['type'] || raise(TypeBalancer::Error, 'Cannot access type field') if item.is_a?(Hash)
81
-
82
- begin
83
- item.type
84
- rescue NoMethodError
85
- raise TypeBalancer::Error, 'Cannot access type field'
86
- end
87
- end
88
-
89
87
  def calculate_target_counts(items_by_type)
90
- items_by_type.values.sum(&:size)
91
88
  items_by_type.transform_values(&:size)
92
89
  end
93
90
 
@@ -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,41 @@ 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
+ def initialize(items, type_field: :type, types: nil, strategy: nil, **strategy_options)
109
118
  raise ArgumentError, 'Items cannot be nil' if items.nil?
110
119
  raise ArgumentError, 'Type field cannot be nil' if type_field.nil?
111
120
 
112
121
  @items = items
113
122
  @type_field = type_field
114
- @types = types || extract_types
123
+ @types = types
124
+ @strategy_name = strategy
125
+ @strategy_options = strategy_options
115
126
  end
116
127
 
117
128
  def call
118
129
  return [] if @items.empty?
119
130
 
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)
131
+ # Create strategy instance
132
+ strategy = StrategyFactory.create(
133
+ @strategy_name,
134
+ items: @items,
135
+ type_field: @type_field,
136
+ types: @types || extract_types,
137
+ **@strategy_options
138
+ )
139
+
140
+ # Balance items using strategy
141
+ strategy.balance
129
142
  end
130
143
 
131
144
  private
132
145
 
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
146
  def extract_types
141
147
  types = @items.map { |item| item[@type_field].to_s }.uniq
142
148
  DEFAULT_TYPE_ORDER.select { |type| types.include?(type) } + (types - DEFAULT_TYPE_ORDER)
143
149
  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
175
- 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
215
- end
216
- end
217
150
  end
218
151
  end
152
+
153
+ # Register default strategy
154
+ TypeBalancer::StrategyFactory.register(:sliding_window, TypeBalancer::Strategies::SlidingWindowStrategy)
@@ -6,29 +6,82 @@ module TypeBalancer
6
6
  # Validate inputs
7
7
  return [] if total_count <= 0 || ratio <= 0 || ratio > 1
8
8
 
9
- # Calculate target count and round down for specific ratios
10
- target_count = if ratio <= 0.34
11
- 1 # For ratios <= 0.34, always use 1 position
12
- elsif ratio <= 0.67
13
- 2 # For ratios <= 0.67, always use 2 positions
14
- else
15
- (total_count * ratio).ceil
16
- end
9
+ # Calculate base target count
10
+ target_count = (total_count * ratio).ceil
11
+
12
+ # Special case for 3 slots
13
+ if total_count == 3
14
+ target_count = if ratio <= 0.34
15
+ 1
16
+ elsif ratio <= 0.67
17
+ 2
18
+ else
19
+ 3
20
+ end
21
+ end
17
22
 
18
23
  return [] if target_count.zero?
19
24
  return (0...total_count).to_a if target_count >= total_count
20
25
 
21
- # Special case for 3 slots
22
- if total_count == 3
26
+ if available_positions
27
+ # Filter out invalid positions and sort them
28
+ valid_positions = available_positions.select { |pos| pos >= 0 && pos < total_count }.sort
29
+ return [] if valid_positions.empty?
30
+
31
+ # For single target position, use first available
32
+ return [valid_positions.first] if target_count == 1
33
+
34
+ # For two positions
35
+ if target_count == 2
36
+ # Special case for three slots
37
+ if total_count == 3
38
+ return [valid_positions[0], valid_positions[1]] if valid_positions.size >= 2
39
+
40
+ return [valid_positions.first, valid_positions.first + 1]
41
+ end
42
+
43
+ # Special case for invalid positions that go beyond total_count
44
+ if available_positions.any? { |pos| pos >= total_count }
45
+ valid_positions = available_positions.select { |pos| pos >= 0 }.sort
46
+ return [valid_positions.first, valid_positions.last]
47
+ end
48
+
49
+ # Otherwise use first and last
50
+ return [valid_positions.first, valid_positions.last]
51
+ end
52
+
53
+ # If we have fewer or equal positions than needed, use all available up to target_count
54
+ return valid_positions if valid_positions.size <= target_count
55
+
56
+ # For more positions, take the first N positions where N is target_count
57
+ return valid_positions.first(target_count) if target_count <= 3
58
+
59
+ # For larger target counts, distribute evenly
60
+ target_positions = []
61
+ step = (valid_positions.size - 1).fdiv(target_count - 1)
62
+ (0...target_count).each do |i|
63
+ index = (i * step).round
64
+ target_positions << valid_positions[index]
65
+ end
66
+ target_positions
67
+ else
68
+ # Handle single target position
23
69
  return [0] if target_count == 1
24
- return [0, 1] if target_count == 2
25
- end
26
70
 
27
- TypeBalancer::PositionCalculator.calculate_positions(
28
- total_count: total_count,
29
- ratio: ratio,
30
- available_items: available_positions
31
- )
71
+ # For two positions
72
+ if target_count == 2
73
+ # Special case for three slots
74
+ return [0, 1] if total_count == 3
75
+
76
+ # Otherwise use first and last
77
+ return [0, total_count - 1]
78
+ end
79
+
80
+ # Calculate evenly spaced positions for multiple targets
81
+ (0...target_count).map do |i|
82
+ ((total_count - 1) * i.fdiv(target_count - 1)).round
83
+ end
84
+ end
32
85
  end
33
86
  end
34
87
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeBalancer
4
+ module Strategies
5
+ # Base class for all balancing strategies
6
+ class BaseStrategy
7
+ def initialize(items:, type_field:, types: nil)
8
+ @items = items
9
+ @type_field = type_field
10
+ @types = types
11
+ end
12
+
13
+ # Interface method that all strategies must implement
14
+ def balance
15
+ raise NotImplementedError, 'Strategies must implement #balance'
16
+ end
17
+
18
+ protected
19
+
20
+ def validate_items!
21
+ @items.each do |item|
22
+ raise ArgumentError, 'All items must have a type field' unless item.key?(@type_field)
23
+ raise ArgumentError, 'Type values cannot be empty' if item[@type_field].to_s.strip.empty?
24
+ end
25
+ end
26
+
27
+ def extract_types
28
+ types = @items.map { |item| item[@type_field].to_s }.uniq
29
+ DEFAULT_TYPE_ORDER.select { |type| types.include?(type) } + (types - DEFAULT_TYPE_ORDER)
30
+ end
31
+
32
+ def group_items_by_type
33
+ # First, create a hash to store items by type while preserving order
34
+ type_queues = {}
35
+ @types.each { |type| type_queues[type] = [] }
36
+
37
+ # Add items to their respective queues in order
38
+ @items.each do |item|
39
+ type = item[@type_field].to_s
40
+ type_queues[type] << item if type_queues.key?(type)
41
+ end
42
+
43
+ type_queues
44
+ end
45
+
46
+ DEFAULT_TYPE_ORDER = %w[video image strip article].freeze
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_strategy'
4
+
5
+ module TypeBalancer
6
+ module Strategies
7
+ # Implements a sliding window approach to balance items
8
+ class SlidingWindowStrategy < BaseStrategy
9
+ def initialize(items:, type_field:, types: nil, window_size: 10)
10
+ super(items: items, type_field: type_field, types: types)
11
+ @window_size = window_size
12
+ @types = types || extract_types
13
+ end
14
+
15
+ def balance
16
+ return [] if @items.empty?
17
+
18
+ validate_items!
19
+ return @items.dup if group_items_by_type.size == 1
20
+
21
+ type_queues = group_items_by_type
22
+ type_ratios = calculate_type_ratios(type_queues)
23
+
24
+ process_windows(type_queues, type_ratios)
25
+ end
26
+
27
+ private
28
+
29
+ def calculate_type_ratios(type_queues)
30
+ total_items = @items.size.to_f
31
+ type_queues.transform_values { |list| list.size / total_items }
32
+ end
33
+
34
+ def process_windows(type_queues, type_ratios)
35
+ result = []
36
+ used_items = Set.new
37
+
38
+ until result.size == @items.size
39
+ size = next_window_size(result)
40
+ window = balance_window(type_queues, type_ratios, size, used_items)
41
+
42
+ if window.empty?
43
+ append_remaining(result, used_items)
44
+ else
45
+ window.each do |item|
46
+ next if used_items.include?(item)
47
+
48
+ result << item
49
+ used_items.add(item)
50
+ end
51
+ end
52
+ end
53
+
54
+ result
55
+ end
56
+
57
+ def next_window_size(result)
58
+ (@items.size - result.size).clamp(1, @window_size)
59
+ end
60
+
61
+ def append_remaining(result, used_items)
62
+ @items.each do |item|
63
+ next if used_items.include?(item)
64
+
65
+ result << item
66
+ used_items.add(item)
67
+ end
68
+ end
69
+
70
+ def balance_window(type_queues, type_ratios, window_size, used_items)
71
+ window_items = []
72
+ target_counts = calculate_window_targets(type_ratios, window_size)
73
+ current_counts = Hash.new(0)
74
+
75
+ while window_items.size < window_size
76
+ type_to_add = find_next_type(type_ratios, current_counts, target_counts, type_queues, used_items)
77
+ break unless type_to_add
78
+
79
+ next_item = type_queues[type_to_add].find { |i| !used_items.include?(i) }
80
+ break unless next_item
81
+
82
+ window_items << next_item
83
+ current_counts[type_to_add] += 1
84
+ end
85
+
86
+ window_items
87
+ end
88
+
89
+ def calculate_window_targets(type_ratios, window_size)
90
+ targets = type_ratios.transform_values { |ratio| (window_size * ratio).floor }
91
+ ensure_minimum_representation(targets, type_ratios)
92
+ scale_down_if_needed(targets, window_size)
93
+ distribute_remaining_slots(targets, type_ratios, window_size)
94
+ targets
95
+ end
96
+
97
+ def find_next_type(type_ratios, current_counts, target_counts, type_queues, used_items)
98
+ current_ratios = compute_current_ratios(current_counts, type_ratios)
99
+ eligible = eligible_types(type_ratios, current_counts, target_counts, type_queues, used_items)
100
+ eligible.min_by { |t| (current_ratios[t] || 0) - type_ratios[t] }
101
+ end
102
+
103
+ def ensure_minimum_representation(targets, type_ratios)
104
+ type_ratios.each_key do |t|
105
+ targets[t] = 1 if type_ratios[t].positive? && targets[t] < 1
106
+ end
107
+ end
108
+
109
+ def scale_down_if_needed(targets, window_size)
110
+ total = targets.values.sum
111
+ return unless total > window_size
112
+
113
+ factor = window_size.to_f / total
114
+ targets.transform_values! { |count| (count * factor).floor }
115
+ end
116
+
117
+ def distribute_remaining_slots(targets, type_ratios, window_size)
118
+ remaining = window_size - targets.values.sum
119
+ return unless remaining.positive?
120
+
121
+ sorted = type_ratios.sort_by { |_t, r| -r }.map(&:first)
122
+ remaining.times { |i| targets[sorted[i % sorted.size]] += 1 }
123
+ end
124
+
125
+ def compute_current_ratios(current_counts, type_ratios)
126
+ total = current_counts.values.sum.to_f
127
+ return type_ratios.dup if total.zero?
128
+
129
+ current_counts.transform_values { |c| c / total }
130
+ end
131
+
132
+ def eligible_types(type_ratios, current_counts, target_counts, type_queues, used_items)
133
+ type_ratios.keys.select do |t|
134
+ type_queues[t].any? { |i| !used_items.include?(i) } &&
135
+ current_counts[t] < target_counts[t]
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeBalancer
4
+ # Module containing all balancing strategies
5
+ module Strategies
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeBalancer
4
+ # Factory for creating and managing balancing strategies
5
+ class StrategyFactory
6
+ class << self
7
+ def create(strategy_name = nil, **)
8
+ strategy_name ||= default_strategy
9
+ strategy_class = find_strategy(strategy_name)
10
+
11
+ raise ArgumentError, "Unknown strategy: #{strategy_name}" unless strategy_class
12
+
13
+ strategy_class.new(**)
14
+ end
15
+
16
+ def register(name, strategy_class)
17
+ strategies[name.to_sym] = strategy_class
18
+ end
19
+
20
+ def default_strategy=(name)
21
+ raise ArgumentError, "Unknown strategy: #{name}" unless strategies.key?(name.to_sym)
22
+
23
+ @default_strategy = name.to_sym
24
+ end
25
+
26
+ def default_strategy
27
+ @default_strategy ||= :sliding_window
28
+ end
29
+
30
+ private
31
+
32
+ def strategies
33
+ @strategies ||= {}
34
+ end
35
+
36
+ def find_strategy(name)
37
+ strategies[name.to_sym]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -20,10 +20,15 @@ module TypeBalancer
20
20
  if item.respond_to?(@type_field)
21
21
  item.send(@type_field)
22
22
  elsif item.respond_to?(:[])
23
- item[@type_field] || item[@type_field.to_s]
23
+ value = item[@type_field] || item[@type_field.to_s]
24
+ raise TypeBalancer::Error, "Cannot access type field '#{@type_field}' on item #{item.inspect}" if value.nil?
25
+
26
+ value
24
27
  else
25
- raise Error, "Cannot access type field '#{@type_field}' on item #{item}"
28
+ raise TypeBalancer::Error, "Cannot access type field '#{@type_field}' on item #{item.inspect}"
26
29
  end
30
+ rescue NoMethodError, TypeError
31
+ raise TypeBalancer::Error, "Cannot access type field '#{@type_field}' on item #{item.inspect}"
27
32
  end
28
33
  end
29
34
  end