type_balancer 0.1.2 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ffc72598d68e02ede3d667d51d4eb3bafd48597cddbb36e124cdb95a8e6f54c
4
- data.tar.gz: fd4d236432a8490e20271c66235ecd4bcd114cb6d5ceb213dbbc7e9346f7037e
3
+ metadata.gz: f73ec86fddfda3cd22f19b0081824a8a488e1a47359976e32eb6b999074b92a3
4
+ data.tar.gz: f25f85b6caeeeb3a5f8012ae295b96ae02413e4b4342aba946438bbcbb9eb063
5
5
  SHA512:
6
- metadata.gz: ea6b99ba44cb6e209a122a18a16a88cc8a8ccb6c5f8a1ceba1a443344489e1744cc3d105886d27df4e601a2149d0bbf0a72cabc9ddd5f766a3fada1d88abc64e
7
- data.tar.gz: eac933d9f6b1e77f4ec4e83794aabd09dabd47f4a83fd10e148e40680c0cff411dd33c8eaf343823b830206c7b16536c954372788e78bcd65cecbbac5d09d227
6
+ metadata.gz: c8895eea255feccab33cd33ba70d9bfb486a4ff9108ba1a828d1be72b501db1635cb35d5aab574298923533d1080c29d7b45a50bb8d1547ffdacfd882dc4d491
7
+ data.tar.gz: '09f755338981a1523cae4eddc3468b17d7693b9183e7039b7e81f1d24bd6beab29133d9d9a797487ea3745195f3f4e577311c89258b41fa237bd63cc3fb63cd2'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- type_balancer (0.1.2)
4
+ type_balancer (0.1.3)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -20,7 +20,7 @@ TypeBalancer is a sophisticated Ruby gem designed to solve the challenge of dist
20
20
 
21
21
  The gem uses advanced distribution algorithms to ensure that items are not only balanced by type but also maintain optimal spacing, preventing clusters of similar content while respecting specified ratios.
22
22
 
23
- [View Examples & Quality Tests](docs/quality.md) | [View Benchmark Results](docs/benchmarks/README.md)
23
+ [View Documentation](docs/README.md) | [View Benchmark Results](docs/benchmarks/README.md)
24
24
 
25
25
  ## Installation
26
26
 
@@ -56,6 +56,60 @@ items = [
56
56
  balanced_items = TypeBalancer.balance(items, type_field: :type)
57
57
  ```
58
58
 
59
+ ## Balancing Collections with `TypeBalancer.balance`
60
+
61
+ The primary method for balancing collections is `TypeBalancer.balance`. This method takes an array of items and distributes them by type, ensuring optimal spacing and respecting type ratios.
62
+
63
+ **Basic Example:**
64
+
65
+ ```ruby
66
+ items = [
67
+ { type: 'video', title: 'Video 1' },
68
+ { type: 'image', title: 'Image 1' },
69
+ { type: 'article', title: 'Article 1' },
70
+ # ...
71
+ ]
72
+ balanced = TypeBalancer.balance(items, type_field: :type)
73
+ # => [ { type: 'article', ... }, { type: 'image', ... }, { type: 'video', ... }, ... ]
74
+ ```
75
+
76
+ **Custom Type Order:**
77
+
78
+ You can specify a custom order for types using the `type_order` argument. This controls the priority of types in the balanced output.
79
+
80
+ ```ruby
81
+ # Prioritize images, then videos, then articles
82
+ balanced = TypeBalancer.balance(items, type_field: :type, type_order: %w[image video article])
83
+ # => [ { type: 'image', ... }, { type: 'video', ... }, { type: 'article', ... }, ... ]
84
+ ```
85
+
86
+ - `type_field`: The key to use for type extraction (default: `:type`).
87
+ - `type_order`: An array of type names (as strings) specifying the desired order.
88
+
89
+ For more advanced usage and options, see [Detailed Balance Method Documentation](docs/balance.md).
90
+
91
+ ## Calculating Positions Directly
92
+
93
+ In addition to balancing collections, you can use `TypeBalancer.calculate_positions` to determine optimal positions for a given type or subset of items within a sequence. This is useful for advanced scenarios where you need fine-grained control over item placement.
94
+
95
+ **Basic Example:**
96
+
97
+ ```ruby
98
+ # Calculate positions for 3 items in a sequence of 10 slots
99
+ positions = TypeBalancer.calculate_positions(total_count: 10, ratio: 0.3)
100
+ # => [0, 5, 9]
101
+ ```
102
+
103
+ **With Available Items:**
104
+
105
+ ```ruby
106
+ # Restrict placement to specific slots
107
+ positions = TypeBalancer.calculate_positions(total_count: 10, ratio: 0.5, available_items: [0, 1, 2])
108
+ # => [0, 1, 2]
109
+ ```
110
+
111
+ For more advanced usage and options, see [Detailed Position Calculation Documentation](docs/calculate_positions.md).
112
+
59
113
  ## Performance Characteristics
60
114
 
61
115
  TypeBalancer is designed to handle collections of varying sizes efficiently. Here are the current performance metrics:
@@ -91,7 +145,7 @@ After checking out the repo, run `bin/setup` to install dependencies. Then:
91
145
  3. Run `rake rubocop` to check code style
92
146
  4. Run `bin/console` for an interactive prompt
93
147
 
94
- For more information about the quality script and its uses, see our [quality script documentation](docs/quality.md).
148
+ For more information about the gem, its features, and quality checks, see our [documentation](docs/README.md).
95
149
 
96
150
  ## Contributing
97
151
 
@@ -140,7 +194,7 @@ We welcome contributions to TypeBalancer! Here's how you can help:
140
194
  - Quality script should pass without new issues
141
195
 
142
196
  For more detailed information about our development process and tools:
143
- - [Quality Script Documentation](docs/quality.md)
197
+ - [Documentation](docs/README.md)
144
198
  - [Benchmark Documentation](docs/benchmarks/README.md)
145
199
 
146
200
  ## License
data/docs/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # Documentation Index
2
+
3
+ Welcome to the TypeBalancer documentation! Below you'll find links to all major documentation resources for the gem.
4
+
5
+ - [Quality Script](quality.md): Integration and example script for verifying gem functionality, with instructions for running locally or from other projects.
6
+ - [Balance Method Details](balance.md): In-depth documentation for the `TypeBalancer.balance` method, including arguments, usage, and edge cases.
7
+ - [Calculate Positions Method Details](calculate_positions.md): Detailed documentation for `TypeBalancer.calculate_positions`, with examples and caveats.
8
+ - [Benchmarks](benchmarks/README.md): Performance metrics and benchmarking methodology for the gem.
data/docs/balance.md ADDED
@@ -0,0 +1,71 @@
1
+ # Detailed Documentation: `TypeBalancer.balance`
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.
4
+
5
+ ## Method Signature
6
+
7
+ ```ruby
8
+ TypeBalancer.balance(items, type_field: :type, type_order: nil)
9
+ ```
10
+
11
+ ### Arguments
12
+ - `items` (Array<Hash>): The collection of items to balance. Each item should have a type field (default: `:type`).
13
+ - `type_field` (Symbol/String, optional): The key to use for extracting the type from each item. Default is `:type`.
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
+
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.
19
+
20
+ ## Usage Examples
21
+
22
+ ### 1. Basic Balancing
23
+ ```ruby
24
+ items = [
25
+ { type: 'video', title: 'Video 1' },
26
+ { type: 'image', title: 'Image 1' },
27
+ { type: 'article', title: 'Article 1' },
28
+ { type: 'article', title: 'Article 2' },
29
+ { type: 'image', title: 'Image 2' },
30
+ { type: 'video', title: 'Video 2' }
31
+ ]
32
+ balanced = TypeBalancer.balance(items)
33
+ # => [ { type: 'article', ... }, { type: 'image', ... }, { type: 'video', ... }, ... ]
34
+ ```
35
+
36
+ ### 2. Custom Type Order
37
+ ```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', ... }, ... ]
41
+ ```
42
+
43
+ ### 3. Custom Type Field
44
+ ```ruby
45
+ items = [
46
+ { category: 'video', title: 'Video 1' },
47
+ { category: 'image', title: 'Image 1' },
48
+ { category: 'article', title: 'Article 1' }
49
+ ]
50
+ balanced = TypeBalancer.balance(items, type_field: :category)
51
+ # => [ { category: 'article', ... }, { category: 'image', ... }, { category: 'video', ... } ]
52
+ ```
53
+
54
+ ### 4. Handling Missing Types
55
+ If a type in `type_order` is not present in the input, it is simply ignored in the output order.
56
+
57
+ ### 5. Edge Cases
58
+ - **Empty Input:** Raises an exception (`Collection cannot be empty`).
59
+ - **Single Type:** All items are returned in their original order.
60
+ - **Missing Type Field:** If an item is missing the type field, it is ignored or may cause an error depending on context.
61
+
62
+ ## Notes and Caveats
63
+ - The method is deterministic: the same input will always produce the same output.
64
+ - The `type_order` argument must be an array of strings matching the type values in your items.
65
+ - If you use a custom `type_field`, ensure all items have that field.
66
+ - The method does not mutate the input array.
67
+
68
+ ## See Also
69
+ - [README.md](../README.md) for general usage
70
+ - [Quality Script Documentation](quality.md) for integration tests and examples
71
+ - [Detailed Position Calculation Documentation](calculate_positions.md)
@@ -0,0 +1,87 @@
1
+ # Detailed Documentation: `TypeBalancer.calculate_positions`
2
+
3
+ `TypeBalancer.calculate_positions` is a utility method for determining the optimal positions for a given number of items (or a ratio of items) within a sequence of slots. This is useful for advanced scenarios where you want to control the distribution of a specific type or subset of items.
4
+
5
+ ## Method Signature
6
+
7
+ ```ruby
8
+ TypeBalancer.calculate_positions(total_count:, ratio:, available_items: nil)
9
+ ```
10
+
11
+ ### Arguments
12
+ - `total_count` (Integer): The total number of slots in the sequence (e.g., the length of your feed or array).
13
+ - `ratio` (Float): The desired ratio of items to place (e.g., `0.3` for 30%).
14
+ - `available_items` (Array<Integer>, optional): An array of slot indices where placement is allowed. If omitted, all slots are considered available.
15
+
16
+ ## Return Value
17
+ - Returns an array of integer indices representing the optimal positions for the items.
18
+ - The array will have a length close to `total_count * ratio`, rounded as appropriate.
19
+ - If `available_items` is provided, only those slots will be used.
20
+
21
+ ## Usage Examples
22
+
23
+ ### 1. Even Distribution
24
+ ```ruby
25
+ # Place 3 items evenly in 10 slots
26
+ TypeBalancer.calculate_positions(total_count: 10, ratio: 0.3)
27
+ # => [0, 5, 9]
28
+ ```
29
+
30
+ ### 2. Restricting to Available Slots
31
+ ```ruby
32
+ # Only use slots 0, 1, and 2
33
+ TypeBalancer.calculate_positions(total_count: 10, ratio: 0.5, available_items: [0, 1, 2])
34
+ # => [0, 1, 2]
35
+ ```
36
+
37
+ ### 3. Edge Cases
38
+ ```ruby
39
+ # Single item
40
+ TypeBalancer.calculate_positions(total_count: 1, ratio: 1.0)
41
+ # => [0]
42
+
43
+ # No items
44
+ TypeBalancer.calculate_positions(total_count: 100, ratio: 0.0)
45
+ # => []
46
+
47
+ # All items
48
+ TypeBalancer.calculate_positions(total_count: 5, ratio: 1.0)
49
+ # => [0, 1, 2, 3, 4]
50
+ ```
51
+
52
+ ### 4. Precision with Small Ratios
53
+ ```ruby
54
+ # Two positions in three slots
55
+ TypeBalancer.calculate_positions(total_count: 3, ratio: 0.67)
56
+ # => [0, 1]
57
+
58
+ # Single position in three slots
59
+ TypeBalancer.calculate_positions(total_count: 3, ratio: 0.34)
60
+ # => [0]
61
+ ```
62
+
63
+ ### 5. Available Items Edge Cases
64
+ ```ruby
65
+ # Single target with multiple available positions
66
+ TypeBalancer.calculate_positions(total_count: 5, ratio: 0.2, available_items: [1, 2, 3])
67
+ # => [1]
68
+
69
+ # Two targets with multiple available positions
70
+ TypeBalancer.calculate_positions(total_count: 10, ratio: 0.2, available_items: [1, 3, 5])
71
+ # => [1, 5]
72
+
73
+ # Exact match of available positions
74
+ TypeBalancer.calculate_positions(total_count: 10, ratio: 0.3, available_items: [2, 4, 6])
75
+ # => [2, 4, 6]
76
+ ```
77
+
78
+ ## Notes and Caveats
79
+ - If `ratio` is 0 or `total_count` is 0, returns an empty array.
80
+ - If `ratio` is 1.0, returns all available slots.
81
+ - If `available_items` is provided and its length is less than the target count, all available items are returned.
82
+ - For very small or very large ratios, the method ensures at least one or all slots are used, respectively.
83
+ - The method is deterministic and will always return the same result for the same input.
84
+
85
+ ## See Also
86
+ - [README.md](../README.md) for general usage
87
+ - [Quality Script Documentation](quality.md) for integration tests and examples
data/docs/quality.md CHANGED
@@ -8,12 +8,42 @@ The TypeBalancer gem includes a comprehensive quality check script located at `/
8
8
 
9
9
  ## Running the Script
10
10
 
11
- To run the quality script:
11
+ To run the quality script from the gem repository:
12
12
 
13
13
  ```bash
14
14
  bundle exec ruby examples/quality.rb
15
15
  ```
16
16
 
17
+ ### Running from Other Projects
18
+
19
+ You can run the quality script from any project that includes the TypeBalancer gem. This is useful for verifying the gem's behavior in your app or as part of CI for downstream projects.
20
+
21
+ **Example:**
22
+
23
+ ```bash
24
+ bundle exec ruby /path/to/gems/type_balancer/examples/quality.rb
25
+ ```
26
+
27
+ **How do I find the path to the gem?**
28
+
29
+ If you are not sure where the gem is installed, you can use Bundler to locate it:
30
+
31
+ ```bash
32
+ bundle show type_balancer
33
+ ```
34
+
35
+ This will print the path to the gem directory. The quality script is located in the `examples` subdirectory of that path. For example:
36
+
37
+ ```bash
38
+ $ bundle show type_balancer
39
+ /Users/yourname/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/type_balancer-0.1.3
40
+ $ bundle exec ruby /Users/yourname/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/type_balancer-0.1.3/examples/quality.rb
41
+ ```
42
+
43
+ - Ensure the gem is installed and available in your bundle.
44
+ - The script expects the test data file at `examples/balance_test_data.yml` (relative to the gem root).
45
+ - Output will be color-coded if your terminal supports ANSI colors.
46
+
17
47
  ## What it Tests
18
48
 
19
49
  The script tests several key aspects of the TypeBalancer gem:
@@ -23,32 +53,23 @@ The script tests several key aspects of the TypeBalancer gem:
23
53
  - Shows spacing calculations between positions
24
54
  - Verifies edge cases (single item, no items, all items)
25
55
 
26
- ### 2. Content Feed Example
56
+ ### 2. Robust Balance Method Tests
57
+ - Loads scenarios from a YAML file (`examples/balance_test_data.yml`)
58
+ - Tests `TypeBalancer.balance` with and without the `type_order` argument
59
+ - Checks type counts, custom order, and exception handling for empty input
60
+ - Prints a color-coded summary table for pass/fail counts
61
+
62
+ ### 3. Content Feed Example
27
63
  - Shows a real-world example of content type distribution
28
64
  - Verifies position allocation for different content types (video, image, article)
29
65
  - Checks distribution statistics and ratios
30
66
 
31
- ### 3. Balancer API
32
- - Tests the main TypeBalancer.balance method
33
- - Verifies batch creation and size limits
34
- - Demonstrates custom type ordering
35
-
36
- ### 4. Type Extraction
37
- - Tests type extraction from both hash and object items
38
- - Verifies support for different type field access methods
39
-
40
- ### 5. Error Handling
41
- - Validates handling of empty collections
42
- - Tests response to invalid type fields
43
- - Verifies batch size validation
44
-
45
67
  ## Output Format
46
68
 
47
- The script provides detailed output showing:
48
- - Results of each test case
49
- - Distribution statistics
50
- - Any issues found during testing
51
- - A summary of all examples run and passed
69
+ - Each section prints a color-coded summary table of passing and failing tests
70
+ - Failures and exceptions are highlighted in red; passes in green
71
+ - The final summary shows the total number of examples run and passed
72
+ - The script exits with status 0 if all tests pass, or 1 if any fail (CI-friendly)
52
73
 
53
74
  ## Using as a Development Tool
54
75
 
@@ -58,6 +79,19 @@ The quality script is particularly useful when:
58
79
  3. Verifying changes haven't broken core functionality
59
80
  4. Understanding how different features work together
60
81
 
82
+ ## Customizing/Extending the Script
83
+
84
+ - You can add new scenarios to `examples/balance_test_data.yml` to test additional cases or edge conditions.
85
+ - You may copy or extend the script for your own integration tests.
86
+ - The script can be adapted to accept a custom YAML path if needed (see comments in the script).
87
+
88
+ ## Troubleshooting
89
+
90
+ - **Color output not working:** Ensure your terminal supports ANSI colors.
91
+ - **File not found:** Make sure `examples/balance_test_data.yml` exists and is accessible from the script's location.
92
+ - **Gem not found:** Ensure the TypeBalancer gem is installed and available in your bundle.
93
+ - **Path issues:** Use an absolute or correct relative path to the script when running from another project.
94
+
61
95
  ## Extending the Script
62
96
 
63
97
  When adding new features to TypeBalancer, consider:
@@ -0,0 +1,66 @@
1
+ # Test data for TypeBalancer.balance integration tests
2
+
3
+ - name: Even distribution
4
+ items:
5
+ - { type: video, id: 1 }
6
+ - { type: video, id: 2 }
7
+ - { type: image, id: 3 }
8
+ - { type: image, id: 4 }
9
+ - { type: article, id: 5 }
10
+ - { type: article, id: 6 }
11
+ - { type: article, id: 7 }
12
+ - { type: article, id: 8 }
13
+ expected_type_counts:
14
+ video: 2
15
+ image: 2
16
+ article: 4
17
+
18
+ - name: Uneven distribution
19
+ items:
20
+ - { type: video, id: 1 }
21
+ - { type: image, id: 2 }
22
+ - { type: article, id: 3 }
23
+ - { type: article, id: 4 }
24
+ - { type: article, id: 5 }
25
+ - { type: article, id: 6 }
26
+ - { type: article, id: 7 }
27
+ - { type: article, id: 8 }
28
+ expected_type_counts:
29
+ video: 1
30
+ image: 1
31
+ article: 6
32
+
33
+ - name: Missing type (no images)
34
+ items:
35
+ - { type: video, id: 1 }
36
+ - { type: video, id: 2 }
37
+ - { type: article, id: 3 }
38
+ - { type: article, id: 4 }
39
+ - { type: article, id: 5 }
40
+ expected_type_counts:
41
+ video: 2
42
+ article: 3
43
+
44
+ - name: Custom type order
45
+ items:
46
+ - { type: video, id: 1 }
47
+ - { type: image, id: 2 }
48
+ - { type: article, id: 3 }
49
+ - { type: article, id: 4 }
50
+ - { type: image, id: 5 }
51
+ - { type: video, id: 6 }
52
+ - { type: article, id: 7 }
53
+ type_order: [image, video, article]
54
+ expected_first_type: image
55
+
56
+ - name: Edge case - empty
57
+ items: []
58
+ expected_type_counts: {}
59
+
60
+ - name: Edge case - single type
61
+ items:
62
+ - { type: article, id: 1 }
63
+ - { type: article, id: 2 }
64
+ - { type: article, id: 3 }
65
+ expected_type_counts:
66
+ article: 3
data/examples/quality.rb CHANGED
@@ -1,8 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'type_balancer'
4
+ require 'yaml'
4
5
 
5
6
  class QualityChecker
7
+ GREEN = "\e[32m"
8
+ RED = "\e[31m"
9
+ YELLOW = "\e[33m"
10
+ RESET = "\e[0m"
11
+
6
12
  def initialize
7
13
  @issues = []
8
14
  @examples_run = 0
@@ -15,6 +21,7 @@ class QualityChecker
15
21
  check_edge_cases
16
22
  check_position_precision
17
23
  check_available_positions_edge_cases
24
+ check_balance_method_robust
18
25
  check_real_world_feed
19
26
 
20
27
  print_summary
@@ -29,11 +36,15 @@ class QualityChecker
29
36
 
30
37
  def check_basic_distribution
31
38
  @examples_run += 1
39
+ @section_examples_run = 0 if !defined?(@section_examples_run)
40
+ @section_examples_passed = 0 if !defined?(@section_examples_passed)
41
+ @section_examples_run += 1
32
42
  puts "\nBasic Distribution Example:"
33
43
  positions = TypeBalancer.calculate_positions(total_count: 10, ratio: 0.3)
34
44
 
35
45
  if positions == [0, 5, 9]
36
46
  @examples_passed += 1
47
+ @section_examples_passed += 1
37
48
  else
38
49
  record_issue("Basic distribution positions #{positions.inspect} don't match expected [0, 5, 9]")
39
50
  end
@@ -49,6 +60,7 @@ class QualityChecker
49
60
 
50
61
  def check_available_items
51
62
  @examples_run += 1
63
+ @section_examples_run += 1
52
64
  puts "\nAvailable Items Example:"
53
65
  positions = TypeBalancer.calculate_positions(
54
66
  total_count: 10,
@@ -58,6 +70,7 @@ class QualityChecker
58
70
 
59
71
  if positions == [0, 1, 2]
60
72
  @examples_passed += 1
73
+ @section_examples_passed += 1
61
74
  else
62
75
  record_issue("Available items test returned #{positions.inspect} instead of expected [0, 1, 2]")
63
76
  end
@@ -70,33 +83,40 @@ class QualityChecker
70
83
 
71
84
  # Single item
72
85
  @examples_run += 1
86
+ @section_examples_run += 1
73
87
  single = TypeBalancer.calculate_positions(total_count: 1, ratio: 1.0)
74
88
  puts "Single item: #{single.inspect}"
75
89
  if single == [0]
76
90
  @examples_passed += 1
91
+ @section_examples_passed += 1
77
92
  else
78
93
  record_issue("Single item case returned #{single.inspect} instead of [0]")
79
94
  end
80
95
 
81
96
  # No items
82
97
  @examples_run += 1
98
+ @section_examples_run += 1
83
99
  none = TypeBalancer.calculate_positions(total_count: 100, ratio: 0.0)
84
100
  puts "No items needed: #{none.inspect}"
85
101
  if none == []
86
102
  @examples_passed += 1
103
+ @section_examples_passed += 1
87
104
  else
88
105
  record_issue("Zero ratio case returned #{none.inspect} instead of []")
89
106
  end
90
107
 
91
108
  # All items
92
109
  @examples_run += 1
110
+ @section_examples_run += 1
93
111
  all = TypeBalancer.calculate_positions(total_count: 5, ratio: 1.0)
94
112
  puts "All items needed: #{all.inspect}"
95
113
  if all == [0, 1, 2, 3, 4]
96
114
  @examples_passed += 1
115
+ @section_examples_passed += 1
97
116
  else
98
117
  record_issue("Full ratio case returned #{all.inspect} instead of [0, 1, 2, 3, 4]")
99
118
  end
119
+ print_section_table('calculate_positions')
100
120
  end
101
121
 
102
122
  def check_position_precision
@@ -104,23 +124,28 @@ class QualityChecker
104
124
 
105
125
  # Two positions in three slots
106
126
  @examples_run += 1
127
+ @section_examples_run += 1
107
128
  positions = TypeBalancer.calculate_positions(total_count: 3, ratio: 0.67)
108
129
  puts "Two positions in three slots: #{positions.inspect}"
109
130
  if positions == [0, 1]
110
131
  @examples_passed += 1
132
+ @section_examples_passed += 1
111
133
  else
112
134
  record_issue("Two in three case returned #{positions.inspect} instead of [0, 1]")
113
135
  end
114
136
 
115
137
  # Single position in three slots
116
138
  @examples_run += 1
139
+ @section_examples_run += 1
117
140
  positions = TypeBalancer.calculate_positions(total_count: 3, ratio: 0.34)
118
141
  puts "Single position in three slots: #{positions.inspect}"
119
142
  if positions == [0]
120
143
  @examples_passed += 1
144
+ @section_examples_passed += 1
121
145
  else
122
146
  record_issue("One in three case returned #{positions.inspect} instead of [0]")
123
147
  end
148
+ print_section_table('calculate_positions')
124
149
  end
125
150
 
126
151
  def check_available_positions_edge_cases
@@ -128,6 +153,7 @@ class QualityChecker
128
153
 
129
154
  # Single target with multiple available positions
130
155
  @examples_run += 1
156
+ @section_examples_run += 1
131
157
  positions = TypeBalancer.calculate_positions(
132
158
  total_count: 5,
133
159
  ratio: 0.2,
@@ -136,12 +162,14 @@ class QualityChecker
136
162
  puts "Single target with multiple available: #{positions.inspect}"
137
163
  if positions == [1]
138
164
  @examples_passed += 1
165
+ @section_examples_passed += 1
139
166
  else
140
167
  record_issue("Single target with multiple available returned #{positions.inspect} instead of [1]")
141
168
  end
142
169
 
143
170
  # Two targets with multiple available positions
144
171
  @examples_run += 1
172
+ @section_examples_run += 1
145
173
  positions = TypeBalancer.calculate_positions(
146
174
  total_count: 10,
147
175
  ratio: 0.2,
@@ -150,12 +178,14 @@ class QualityChecker
150
178
  puts "Two targets with multiple available: #{positions.inspect}"
151
179
  if positions == [1, 5]
152
180
  @examples_passed += 1
181
+ @section_examples_passed += 1
153
182
  else
154
183
  record_issue("Two targets with multiple available returned #{positions.inspect} instead of [1, 5]")
155
184
  end
156
185
 
157
186
  # Exact match of available positions
158
187
  @examples_run += 1
188
+ @section_examples_run += 1
159
189
  positions = TypeBalancer.calculate_positions(
160
190
  total_count: 10,
161
191
  ratio: 0.3,
@@ -164,17 +194,111 @@ class QualityChecker
164
194
  puts "Exact match of available positions: #{positions.inspect}"
165
195
  if positions == [2, 4, 6]
166
196
  @examples_passed += 1
197
+ @section_examples_passed += 1
167
198
  else
168
199
  record_issue("Exact match case returned #{positions.inspect} instead of [2, 4, 6]")
169
200
  end
201
+ print_section_table('calculate_positions')
202
+ end
203
+
204
+ def check_balance_method_robust
205
+ puts "\n#{YELLOW}Robust Balance Method Tests:#{RESET}"
206
+ scenarios = YAML.load_file(File.expand_path('../balance_test_data.yml', __FILE__))
207
+ section_run = 0
208
+ section_passed = 0
209
+ scenarios.each do |scenario|
210
+ @examples_run += 1
211
+ section_run += 1
212
+ # Deep symbolize keys for all items in the scenario
213
+ items = (scenario['items'] || []).map { |item| deep_symbolize_keys(item) }
214
+ type_order = scenario['type_order']
215
+ expected_type_counts = scenario['expected_type_counts'] || {}
216
+ expected_first_type = scenario['expected_first_type']
217
+
218
+ # Test with and without type_order
219
+ [nil, type_order].uniq.each do |order|
220
+ label = order ? "with type_order #{order}" : "default order"
221
+ begin
222
+ # Special handling for the empty input case
223
+ if scenario['name'] =~ /empty/i
224
+ begin
225
+ if order
226
+ TypeBalancer.balance(items, type_field: :type, type_order: order)
227
+ else
228
+ TypeBalancer.balance(items, type_field: :type)
229
+ end
230
+ # If no exception, this is a failure
231
+ record_issue("#{scenario['name']} (#{label}): Expected exception for empty input, but none was raised")
232
+ puts "#{RED}#{scenario['name']} (#{label}): Expected exception for empty input, but none was raised#{RESET}"
233
+ rescue => e
234
+ if e.message =~ /Collection cannot be empty/
235
+ @examples_passed += 1
236
+ section_passed += 1
237
+ puts "#{GREEN}#{scenario['name']} (#{label}): Correctly raised exception for empty input#{RESET}"
238
+ else
239
+ record_issue("#{scenario['name']} (#{label}): Unexpected exception: #{e}")
240
+ puts "#{RED}#{scenario['name']} (#{label}): Unexpected exception: #{e}#{RESET}"
241
+ end
242
+ end
243
+ next
244
+ end
245
+ # Normal test logic for other cases
246
+ result = if order
247
+ TypeBalancer.balance(items, type_field: :type, type_order: order)
248
+ else
249
+ TypeBalancer.balance(items, type_field: :type)
250
+ end
251
+ rescue => e
252
+ record_issue("#{scenario['name']} (#{label}): Exception raised: #{e}")
253
+ puts "#{RED}#{scenario['name']} (#{label}): Exception raised: #{e}#{RESET}"
254
+ next
255
+ end
256
+ # Check type counts
257
+ type_counts = result.group_by { |i| i[:type] }.transform_values(&:size)
258
+ if expected_type_counts && !expected_type_counts.empty?
259
+ if type_counts != expected_type_counts
260
+ record_issue("#{scenario['name']} (#{label}): Type counts #{type_counts.inspect} do not match expected #{expected_type_counts.inspect}")
261
+ puts "#{RED}#{scenario['name']} (#{label}): Type counts #{type_counts.inspect} do not match expected #{expected_type_counts.inspect}#{RESET}"
262
+ else
263
+ @examples_passed += 1
264
+ section_passed += 1
265
+ puts "#{GREEN}#{scenario['name']} (#{label}): Passed#{RESET}"
266
+ end
267
+ end
268
+ # Check first type for custom order
269
+ if expected_first_type && order
270
+ if result.first && result.first[:type] != expected_first_type
271
+ record_issue("#{scenario['name']} (#{label}): First type #{result.first[:type]} does not match expected #{expected_first_type}")
272
+ puts "#{RED}#{scenario['name']} (#{label}): First type #{result.first[:type]} does not match expected #{expected_first_type}#{RESET}"
273
+ else
274
+ @examples_passed += 1
275
+ section_passed += 1
276
+ puts "#{GREEN}#{scenario['name']} (#{label}): Custom order respected#{RESET}"
277
+ end
278
+ end
279
+ puts " Result: #{result.map { |i| i[:type] }.inspect}"
280
+ puts " Type counts: #{type_counts.inspect}"
281
+ end
282
+ end
283
+ print_section_table('balance_method', section_run, section_passed)
284
+ end
285
+
286
+ # Helper to deeply symbolize keys in a hash
287
+ def deep_symbolize_keys(obj)
288
+ case obj
289
+ when Hash
290
+ obj.each_with_object({}) { |(k, v), h| h[k.to_sym] = deep_symbolize_keys(v) }
291
+ when Array
292
+ obj.map { |v| deep_symbolize_keys(v) }
293
+ else
294
+ obj
295
+ end
170
296
  end
171
297
 
172
298
  def check_real_world_feed
173
299
  @examples_run += 1
174
- puts "\nReal World Example - Content Feed:"
300
+ puts "\n#{YELLOW}Real World Example - Content Feed:#{RESET}"
175
301
  feed_size = 20
176
-
177
- # Create test items
178
302
  items = [
179
303
  { type: 'video', id: 1 },
180
304
  { type: 'video', id: 2 },
@@ -186,87 +310,22 @@ class QualityChecker
186
310
  { type: 'article', id: 8 },
187
311
  { type: 'article', id: 9 }
188
312
  ]
189
-
190
- # Track allocated positions
191
- allocated_positions = []
192
- content_positions = {}
193
-
194
- # Calculate positions for each type
195
- content_positions[:video] = TypeBalancer.calculate_positions(
196
- total_count: feed_size,
197
- ratio: 0.3,
198
- available_items: (0..7).to_a - allocated_positions
199
- )
200
- allocated_positions += content_positions[:video]
201
-
202
- content_positions[:image] = TypeBalancer.calculate_positions(
203
- total_count: feed_size,
204
- ratio: 0.4,
205
- available_items: (0..14).to_a - allocated_positions
206
- )
207
- allocated_positions += content_positions[:image]
208
-
209
- content_positions[:article] = TypeBalancer.calculate_positions(
210
- total_count: feed_size,
211
- ratio: 0.3,
212
- available_items: (0..19).to_a - allocated_positions
213
- )
214
-
215
- puts "\nContent Type Positions:"
216
- content_positions.each do |type, pos|
217
- puts "#{type}: #{pos.inspect}"
218
- end
219
-
220
- # Check for overlaps
221
- all_positions = content_positions.values.compact.flatten
222
- if all_positions == all_positions.uniq
223
- puts "\nSuccess: No overlapping positions!"
224
- @examples_passed += 1
225
- else
226
- overlaps = all_positions.group_by { |e| e }.select { |_, v| v.size > 1 }.keys
227
- record_issue("Found overlapping positions at indices: #{overlaps.inspect}")
228
- puts "\nWarning: Some positions overlap!"
229
- end
230
-
231
- # Verify distribution
232
- puts "\nDistribution Stats:"
233
- expected_counts = { video: 6, image: 8, article: 6 }
234
- content_positions.each do |type, positions|
235
- count = positions&.length || 0
236
- percentage = (count.to_f / feed_size * 100).round(1)
237
- puts "#{type}: #{count} items (#{percentage}% of feed)"
238
-
239
- if count != expected_counts[type]
240
- record_issue("#{type} count #{count} doesn't match expected #{expected_counts[type]}")
241
- end
242
- end
243
-
244
313
  # Test with custom type order
245
314
  ordered_result = TypeBalancer.balance(
246
315
  items,
247
316
  type_field: :type,
248
317
  type_order: %w[article image video]
249
318
  )
250
-
251
- # Verify type order is respected
252
319
  if ordered_result.first[:type] == 'article'
253
320
  @examples_passed += 1
321
+ puts "#{GREEN}Custom type order respected in real world feed#{RESET}"
322
+ print_section_table('real_world_feed', 1, 1)
254
323
  else
255
- record_issue("Custom type order not respected")
256
- end
257
-
258
- # Test position calculation
259
- positions = TypeBalancer::Distributor.calculate_target_positions(
260
- total_count: 10,
261
- ratio: 0.3
262
- )
263
- if positions.is_a?(Array) && positions.all? { |p| p.is_a?(Integer) }
264
- @examples_passed += 1
265
- else
266
- record_issue("Position calculation failed")
324
+ record_issue("Custom type order not respected in real world feed")
325
+ puts "#{RED}Custom type order not respected in real world feed#{RESET}"
326
+ print_section_table('real_world_feed', 1, 0)
267
327
  end
268
-
269
- puts "\nBalanced items with custom order:"
328
+ puts " Balanced items with custom order: #{ordered_result.map { |i| i[:type] }.inspect}"
270
329
  end
271
330
 
272
331
  def print_summary
@@ -276,15 +335,28 @@ class QualityChecker
276
335
  puts "Expectations Passed: #{@examples_passed}"
277
336
 
278
337
  if @issues.empty?
279
- puts "\nAll quality checks passed! "
338
+ puts "\n#{GREEN}All quality checks passed! ✓#{RESET}"
280
339
  else
281
- puts "\nQuality check failed with #{@issues.size} issues:"
340
+ puts "\n#{RED}Quality check failed with #{@issues.size} issues:#{RESET}"
282
341
  @issues.each_with_index do |issue, index|
283
- puts "#{index + 1}. #{issue}"
342
+ puts "#{RED}#{index + 1}. #{issue}#{RESET}"
284
343
  end
285
344
  end
286
345
  puts "#{'-' * 50}"
287
346
  end
347
+
348
+ # Print a summary table for a section
349
+ def print_section_table(section, run = @section_examples_run, passed = @section_examples_passed)
350
+ failed = run - passed
351
+ puts "\nSection: #{section}"
352
+ puts "-----------------------------"
353
+ puts " #{GREEN}Passing: #{passed}#{RESET}"
354
+ puts " #{RED}Failing: #{failed}#{RESET}"
355
+ puts "-----------------------------\n"
356
+ # Reset section counters for next section
357
+ @section_examples_run = 0
358
+ @section_examples_passed = 0
359
+ end
288
360
  end
289
361
 
290
362
  QualityChecker.new.run
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TypeBalancer
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.3'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: type_balancer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl Smith
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2025-04-28 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Balances types in collections by ensuring each type appears a similar
13
13
  number of times
@@ -37,8 +37,12 @@ files:
37
37
  - benchmark_results/ruby3.3.7_yjit.txt
38
38
  - benchmark_results/ruby3.4.2.txt
39
39
  - benchmark_results/ruby3.4.2_yjit.txt
40
+ - docs/README.md
41
+ - docs/balance.md
40
42
  - docs/benchmarks/README.md
43
+ - docs/calculate_positions.md
41
44
  - docs/quality.md
45
+ - examples/balance_test_data.yml
42
46
  - examples/quality.rb
43
47
  - lib/type_balancer.rb
44
48
  - lib/type_balancer/alternating_filler.rb
@@ -76,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
80
  - !ruby/object:Gem::Version
77
81
  version: '0'
78
82
  requirements: []
79
- rubygems_version: 3.6.7
83
+ rubygems_version: 3.6.2
80
84
  specification_version: 4
81
85
  summary: Balances types in collections
82
86
  test_files: []