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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +57 -3
- data/docs/README.md +8 -0
- data/docs/balance.md +71 -0
- data/docs/calculate_positions.md +87 -0
- data/docs/quality.md +55 -21
- data/examples/balance_test_data.yml +66 -0
- data/examples/quality.rb +149 -77
- data/lib/type_balancer/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f73ec86fddfda3cd22f19b0081824a8a488e1a47359976e32eb6b999074b92a3
|
4
|
+
data.tar.gz: f25f85b6caeeeb3a5f8012ae295b96ae02413e4b4342aba946438bbcbb9eb063
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c8895eea255feccab33cd33ba70d9bfb486a4ff9108ba1a828d1be72b501db1635cb35d5aab574298923533d1080c29d7b45a50bb8d1547ffdacfd882dc4d491
|
7
|
+
data.tar.gz: '09f755338981a1523cae4eddc3468b17d7693b9183e7039b7e81f1d24bd6beab29133d9d9a797487ea3745195f3f4e577311c89258b41fa237bd63cc3fb63cd2'
|
data/Gemfile.lock
CHANGED
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
|
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
|
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
|
-
- [
|
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.
|
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
|
-
|
48
|
-
-
|
49
|
-
-
|
50
|
-
-
|
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 "\
|
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
|
-
|
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 "\
|
338
|
+
puts "\n#{GREEN}All quality checks passed! ✓#{RESET}"
|
280
339
|
else
|
281
|
-
puts "\
|
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
|
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.
|
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:
|
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.
|
83
|
+
rubygems_version: 3.6.2
|
80
84
|
specification_version: 4
|
81
85
|
summary: Balances types in collections
|
82
86
|
test_files: []
|