type_balancer 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +27 -0
- data/Gemfile.lock +1 -1
- data/benchmark_results/ruby3.2.8.txt +8 -8
- data/benchmark_results/ruby3.2.8_yjit.txt +8 -8
- data/benchmark_results/ruby3.3.7.txt +8 -8
- data/benchmark_results/ruby3.3.7_yjit.txt +8 -8
- data/benchmark_results/ruby3.4.2.txt +8 -8
- data/benchmark_results/ruby3.4.2_yjit.txt +8 -8
- data/examples/large_scale_balance_test.rb +55 -176
- data/examples/quality.rb +10 -7
- data/lib/type_balancer/calculator.rb +14 -3
- data/lib/type_balancer/configuration.rb +31 -0
- data/lib/type_balancer/strategies/base_strategy.rb +11 -2
- data/lib/type_balancer/strategies/sliding_window_strategy.rb +140 -81
- data/lib/type_balancer/version.rb +1 -1
- data/lib/type_balancer.rb +8 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa2741d3d75b46e223d47fa50688978cbcf3a8c6faf2866ccf97144a8459b64d
|
4
|
+
data.tar.gz: 927c67db18171fe13d151f4d720b163fa4494c24e9f0fb48508cc4cf91ff1444
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6afefaf19925b4281778baa1a8c8da2bec31301ea15d0d73056a182affc89f50103948af59a1e1ec82fd6355354882d1cbfcaadd27499737c68812c31da903bf
|
7
|
+
data.tar.gz: 73b8f72dbfef3c30618c65508355937c24ffde11e699eb069b470c68d83450b66806979ceb49e864a3e812fdbf3b7b6a8bf4b5557f3f3d40fdb48533ccd9268e
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,32 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [0.2.1] - 2025-05-01
|
4
|
+
|
5
|
+
### Performance
|
6
|
+
- Major performance improvements in the sliding window strategy implementation:
|
7
|
+
- Optimized window position calculation algorithm
|
8
|
+
- Improved batch processing for large collections
|
9
|
+
- Enhanced type distribution handling
|
10
|
+
- Reduced memory usage and allocation
|
11
|
+
- Updated benchmark results showing consistent performance:
|
12
|
+
- Tiny collections (10 items): 8-13μs
|
13
|
+
- Small collections (100 items): 68-104μs
|
14
|
+
- Medium collections (1,000 items): 648ms-1.03ms
|
15
|
+
- Large collections (10,000 items): 6.6-10.0ms
|
16
|
+
|
17
|
+
### Enhanced
|
18
|
+
- Improved test suite with more focused test cases
|
19
|
+
- Reduced test execution time by optimizing large-scale test data
|
20
|
+
- Better handling of type ordering in Calculator and BaseStrategy
|
21
|
+
- Enhanced quality.rb output formatting for better readability
|
22
|
+
- Simplified large_scale_balance_test.rb implementation
|
23
|
+
|
24
|
+
### Fixed
|
25
|
+
- Rubocop violations:
|
26
|
+
- Disabled Style/HashExcept cop
|
27
|
+
- Added parameter list exceptions for complex method signatures
|
28
|
+
- Improved require statements in example scripts to use require_relative
|
29
|
+
|
3
30
|
## [0.2.0] - 2025-04-30
|
4
31
|
|
5
32
|
### Added
|
data/Gemfile.lock
CHANGED
@@ -9,9 +9,9 @@ Running benchmarks...
|
|
9
9
|
Benchmarking Tiny Dataset (10 items)
|
10
10
|
ruby 3.2.8 (2025-03-26 revision 13f495dc2c) [aarch64-linux]
|
11
11
|
Warming up --------------------------------------
|
12
|
-
Ruby Implementation
|
12
|
+
Ruby Implementation 7.321k i/100ms
|
13
13
|
Calculating -------------------------------------
|
14
|
-
Ruby Implementation
|
14
|
+
Ruby Implementation 74.105k (± 4.7%) i/s (13.49 μs/i) - 153.741k in 2.079077s
|
15
15
|
|
16
16
|
Distribution Stats:
|
17
17
|
Video: 4 (40.0%)
|
@@ -21,9 +21,9 @@ Article: 3 (30.0%)
|
|
21
21
|
Benchmarking Small Dataset (100 items)
|
22
22
|
ruby 3.2.8 (2025-03-26 revision 13f495dc2c) [aarch64-linux]
|
23
23
|
Warming up --------------------------------------
|
24
|
-
Ruby Implementation
|
24
|
+
Ruby Implementation 996.000 i/100ms
|
25
25
|
Calculating -------------------------------------
|
26
|
-
Ruby Implementation
|
26
|
+
Ruby Implementation 10.032k (± 0.8%) i/s (99.68 μs/i) - 20.916k in 2.085103s
|
27
27
|
|
28
28
|
Distribution Stats:
|
29
29
|
Video: 34 (34.0%)
|
@@ -33,9 +33,9 @@ Article: 33 (33.0%)
|
|
33
33
|
Benchmarking Medium Dataset (1000 items)
|
34
34
|
ruby 3.2.8 (2025-03-26 revision 13f495dc2c) [aarch64-linux]
|
35
35
|
Warming up --------------------------------------
|
36
|
-
Ruby Implementation
|
36
|
+
Ruby Implementation 104.000 i/100ms
|
37
37
|
Calculating -------------------------------------
|
38
|
-
Ruby Implementation
|
38
|
+
Ruby Implementation 1.040k (± 1.4%) i/s (961.95 μs/i) - 3.120k in 3.001891s
|
39
39
|
|
40
40
|
Distribution Stats:
|
41
41
|
Video: 334 (33.4%)
|
@@ -45,9 +45,9 @@ Article: 333 (33.3%)
|
|
45
45
|
Benchmarking Large Dataset (10000 items)
|
46
46
|
ruby 3.2.8 (2025-03-26 revision 13f495dc2c) [aarch64-linux]
|
47
47
|
Warming up --------------------------------------
|
48
|
-
Ruby Implementation
|
48
|
+
Ruby Implementation 10.000 i/100ms
|
49
49
|
Calculating -------------------------------------
|
50
|
-
Ruby Implementation
|
50
|
+
Ruby Implementation 104.445 (± 1.0%) i/s (9.57 ms/i) - 320.000 in 3.063901s
|
51
51
|
|
52
52
|
Distribution Stats:
|
53
53
|
Video: 3334 (33.34%)
|
@@ -9,9 +9,9 @@ Running benchmarks...
|
|
9
9
|
Benchmarking Tiny Dataset (10 items)
|
10
10
|
ruby 3.2.8 (2025-03-26 revision 13f495dc2c) +YJIT [aarch64-linux]
|
11
11
|
Warming up --------------------------------------
|
12
|
-
Ruby Implementation
|
12
|
+
Ruby Implementation 10.640k i/100ms
|
13
13
|
Calculating -------------------------------------
|
14
|
-
Ruby Implementation
|
14
|
+
Ruby Implementation 111.468k (± 0.9%) i/s (8.97 μs/i) - 223.440k in 2.004679s
|
15
15
|
|
16
16
|
Distribution Stats:
|
17
17
|
Video: 4 (40.0%)
|
@@ -21,9 +21,9 @@ Article: 3 (30.0%)
|
|
21
21
|
Benchmarking Small Dataset (100 items)
|
22
22
|
ruby 3.2.8 (2025-03-26 revision 13f495dc2c) +YJIT [aarch64-linux]
|
23
23
|
Warming up --------------------------------------
|
24
|
-
Ruby Implementation
|
24
|
+
Ruby Implementation 1.392k i/100ms
|
25
25
|
Calculating -------------------------------------
|
26
|
-
Ruby Implementation
|
26
|
+
Ruby Implementation 13.988k (± 0.6%) i/s (71.49 μs/i) - 29.232k in 2.089823s
|
27
27
|
|
28
28
|
Distribution Stats:
|
29
29
|
Video: 34 (34.0%)
|
@@ -33,9 +33,9 @@ Article: 33 (33.0%)
|
|
33
33
|
Benchmarking Medium Dataset (1000 items)
|
34
34
|
ruby 3.2.8 (2025-03-26 revision 13f495dc2c) +YJIT [aarch64-linux]
|
35
35
|
Warming up --------------------------------------
|
36
|
-
Ruby Implementation
|
36
|
+
Ruby Implementation 145.000 i/100ms
|
37
37
|
Calculating -------------------------------------
|
38
|
-
Ruby Implementation
|
38
|
+
Ruby Implementation 1.436k (± 1.0%) i/s (696.60 μs/i) - 4.350k in 3.030548s
|
39
39
|
|
40
40
|
Distribution Stats:
|
41
41
|
Video: 334 (33.4%)
|
@@ -45,9 +45,9 @@ Article: 333 (33.3%)
|
|
45
45
|
Benchmarking Large Dataset (10000 items)
|
46
46
|
ruby 3.2.8 (2025-03-26 revision 13f495dc2c) +YJIT [aarch64-linux]
|
47
47
|
Warming up --------------------------------------
|
48
|
-
Ruby Implementation
|
48
|
+
Ruby Implementation 14.000 i/100ms
|
49
49
|
Calculating -------------------------------------
|
50
|
-
Ruby Implementation
|
50
|
+
Ruby Implementation 142.517 (± 0.7%) i/s (7.02 ms/i) - 434.000 in 3.045580s
|
51
51
|
|
52
52
|
Distribution Stats:
|
53
53
|
Video: 3334 (33.34%)
|
@@ -9,9 +9,9 @@ Running benchmarks...
|
|
9
9
|
Benchmarking Tiny Dataset (10 items)
|
10
10
|
ruby 3.3.7 (2025-01-15 revision be31f993d7) [aarch64-linux]
|
11
11
|
Warming up --------------------------------------
|
12
|
-
Ruby Implementation
|
12
|
+
Ruby Implementation 7.474k i/100ms
|
13
13
|
Calculating -------------------------------------
|
14
|
-
Ruby Implementation
|
14
|
+
Ruby Implementation 77.780k (± 1.0%) i/s (12.86 μs/i) - 156.954k in 2.018142s
|
15
15
|
|
16
16
|
Distribution Stats:
|
17
17
|
Video: 4 (40.0%)
|
@@ -21,9 +21,9 @@ Article: 3 (30.0%)
|
|
21
21
|
Benchmarking Small Dataset (100 items)
|
22
22
|
ruby 3.3.7 (2025-01-15 revision be31f993d7) [aarch64-linux]
|
23
23
|
Warming up --------------------------------------
|
24
|
-
Ruby Implementation
|
24
|
+
Ruby Implementation 968.000 i/100ms
|
25
25
|
Calculating -------------------------------------
|
26
|
-
Ruby Implementation
|
26
|
+
Ruby Implementation 9.726k (± 1.3%) i/s (102.81 μs/i) - 20.328k in 2.090308s
|
27
27
|
|
28
28
|
Distribution Stats:
|
29
29
|
Video: 34 (34.0%)
|
@@ -33,9 +33,9 @@ Article: 33 (33.0%)
|
|
33
33
|
Benchmarking Medium Dataset (1000 items)
|
34
34
|
ruby 3.3.7 (2025-01-15 revision be31f993d7) [aarch64-linux]
|
35
35
|
Warming up --------------------------------------
|
36
|
-
Ruby Implementation
|
36
|
+
Ruby Implementation 101.000 i/100ms
|
37
37
|
Calculating -------------------------------------
|
38
|
-
Ruby Implementation
|
38
|
+
Ruby Implementation 970.619 (± 8.6%) i/s (1.03 ms/i) - 2.929k in 3.044643s
|
39
39
|
|
40
40
|
Distribution Stats:
|
41
41
|
Video: 334 (33.4%)
|
@@ -45,9 +45,9 @@ Article: 333 (33.3%)
|
|
45
45
|
Benchmarking Large Dataset (10000 items)
|
46
46
|
ruby 3.3.7 (2025-01-15 revision be31f993d7) [aarch64-linux]
|
47
47
|
Warming up --------------------------------------
|
48
|
-
Ruby Implementation
|
48
|
+
Ruby Implementation 9.000 i/100ms
|
49
49
|
Calculating -------------------------------------
|
50
|
-
Ruby Implementation
|
50
|
+
Ruby Implementation 99.743 (± 1.0%) i/s (10.03 ms/i) - 306.000 in 3.068124s
|
51
51
|
|
52
52
|
Distribution Stats:
|
53
53
|
Video: 3334 (33.34%)
|
@@ -9,9 +9,9 @@ Running benchmarks...
|
|
9
9
|
Benchmarking Tiny Dataset (10 items)
|
10
10
|
ruby 3.3.7 (2025-01-15 revision be31f993d7) +YJIT [aarch64-linux]
|
11
11
|
Warming up --------------------------------------
|
12
|
-
Ruby Implementation
|
12
|
+
Ruby Implementation 11.095k i/100ms
|
13
13
|
Calculating -------------------------------------
|
14
|
-
Ruby Implementation
|
14
|
+
Ruby Implementation 118.143k (± 1.0%) i/s (8.46 μs/i) - 244.090k in 2.066282s
|
15
15
|
|
16
16
|
Distribution Stats:
|
17
17
|
Video: 4 (40.0%)
|
@@ -21,9 +21,9 @@ Article: 3 (30.0%)
|
|
21
21
|
Benchmarking Small Dataset (100 items)
|
22
22
|
ruby 3.3.7 (2025-01-15 revision be31f993d7) +YJIT [aarch64-linux]
|
23
23
|
Warming up --------------------------------------
|
24
|
-
Ruby Implementation
|
24
|
+
Ruby Implementation 1.488k i/100ms
|
25
25
|
Calculating -------------------------------------
|
26
|
-
Ruby Implementation
|
26
|
+
Ruby Implementation 14.650k (± 2.2%) i/s (68.26 μs/i) - 29.760k in 2.032402s
|
27
27
|
|
28
28
|
Distribution Stats:
|
29
29
|
Video: 34 (34.0%)
|
@@ -33,9 +33,9 @@ Article: 33 (33.0%)
|
|
33
33
|
Benchmarking Medium Dataset (1000 items)
|
34
34
|
ruby 3.3.7 (2025-01-15 revision be31f993d7) +YJIT [aarch64-linux]
|
35
35
|
Warming up --------------------------------------
|
36
|
-
Ruby Implementation
|
36
|
+
Ruby Implementation 152.000 i/100ms
|
37
37
|
Calculating -------------------------------------
|
38
|
-
Ruby Implementation
|
38
|
+
Ruby Implementation 1.542k (± 1.0%) i/s (648.70 μs/i) - 4.712k in 3.057044s
|
39
39
|
|
40
40
|
Distribution Stats:
|
41
41
|
Video: 334 (33.4%)
|
@@ -45,9 +45,9 @@ Article: 333 (33.3%)
|
|
45
45
|
Benchmarking Large Dataset (10000 items)
|
46
46
|
ruby 3.3.7 (2025-01-15 revision be31f993d7) +YJIT [aarch64-linux]
|
47
47
|
Warming up --------------------------------------
|
48
|
-
Ruby Implementation
|
48
|
+
Ruby Implementation 14.000 i/100ms
|
49
49
|
Calculating -------------------------------------
|
50
|
-
Ruby Implementation
|
50
|
+
Ruby Implementation 150.143 (± 0.7%) i/s (6.66 ms/i) - 462.000 in 3.077274s
|
51
51
|
|
52
52
|
Distribution Stats:
|
53
53
|
Video: 3334 (33.34%)
|
@@ -9,9 +9,9 @@ Running benchmarks...
|
|
9
9
|
Benchmarking Tiny Dataset (10 items)
|
10
10
|
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
|
11
11
|
Warming up --------------------------------------
|
12
|
-
Ruby Implementation
|
12
|
+
Ruby Implementation 6.495k i/100ms
|
13
13
|
Calculating -------------------------------------
|
14
|
-
Ruby Implementation
|
14
|
+
Ruby Implementation 75.827k (± 1.0%) i/s (13.19 μs/i) - 155.880k in 2.055940s
|
15
15
|
|
16
16
|
Distribution Stats:
|
17
17
|
Video: 4 (40.0%)
|
@@ -21,9 +21,9 @@ Article: 3 (30.0%)
|
|
21
21
|
Benchmarking Small Dataset (100 items)
|
22
22
|
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
|
23
23
|
Warming up --------------------------------------
|
24
|
-
Ruby Implementation
|
24
|
+
Ruby Implementation 960.000 i/100ms
|
25
25
|
Calculating -------------------------------------
|
26
|
-
Ruby Implementation
|
26
|
+
Ruby Implementation 9.600k (± 0.9%) i/s (104.17 μs/i) - 19.200k in 2.000241s
|
27
27
|
|
28
28
|
Distribution Stats:
|
29
29
|
Video: 34 (34.0%)
|
@@ -33,9 +33,9 @@ Article: 33 (33.0%)
|
|
33
33
|
Benchmarking Medium Dataset (1000 items)
|
34
34
|
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
|
35
35
|
Warming up --------------------------------------
|
36
|
-
Ruby Implementation
|
36
|
+
Ruby Implementation 100.000 i/100ms
|
37
37
|
Calculating -------------------------------------
|
38
|
-
Ruby Implementation
|
38
|
+
Ruby Implementation 991.713 (± 2.2%) i/s (1.01 ms/i) - 3.000k in 3.026621s
|
39
39
|
|
40
40
|
Distribution Stats:
|
41
41
|
Video: 334 (33.4%)
|
@@ -45,9 +45,9 @@ Article: 333 (33.3%)
|
|
45
45
|
Benchmarking Large Dataset (10000 items)
|
46
46
|
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [aarch64-linux]
|
47
47
|
Warming up --------------------------------------
|
48
|
-
Ruby Implementation
|
48
|
+
Ruby Implementation 10.000 i/100ms
|
49
49
|
Calculating -------------------------------------
|
50
|
-
Ruby Implementation
|
50
|
+
Ruby Implementation 100.530 (± 1.0%) i/s (9.95 ms/i) - 310.000 in 3.083817s
|
51
51
|
|
52
52
|
Distribution Stats:
|
53
53
|
Video: 3334 (33.34%)
|
@@ -9,9 +9,9 @@ Running benchmarks...
|
|
9
9
|
Benchmarking Tiny Dataset (10 items)
|
10
10
|
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [aarch64-linux]
|
11
11
|
Warming up --------------------------------------
|
12
|
-
Ruby Implementation
|
12
|
+
Ruby Implementation 8.785k i/100ms
|
13
13
|
Calculating -------------------------------------
|
14
|
-
Ruby Implementation
|
14
|
+
Ruby Implementation 112.980k (± 7.7%) i/s (8.85 μs/i) - 228.410k in 2.039442s
|
15
15
|
|
16
16
|
Distribution Stats:
|
17
17
|
Video: 4 (40.0%)
|
@@ -21,9 +21,9 @@ Article: 3 (30.0%)
|
|
21
21
|
Benchmarking Small Dataset (100 items)
|
22
22
|
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [aarch64-linux]
|
23
23
|
Warming up --------------------------------------
|
24
|
-
Ruby Implementation
|
24
|
+
Ruby Implementation 1.477k i/100ms
|
25
25
|
Calculating -------------------------------------
|
26
|
-
Ruby Implementation
|
26
|
+
Ruby Implementation 14.639k (± 1.2%) i/s (68.31 μs/i) - 29.540k in 2.018232s
|
27
27
|
|
28
28
|
Distribution Stats:
|
29
29
|
Video: 34 (34.0%)
|
@@ -33,9 +33,9 @@ Article: 33 (33.0%)
|
|
33
33
|
Benchmarking Medium Dataset (1000 items)
|
34
34
|
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [aarch64-linux]
|
35
35
|
Warming up --------------------------------------
|
36
|
-
Ruby Implementation
|
36
|
+
Ruby Implementation 152.000 i/100ms
|
37
37
|
Calculating -------------------------------------
|
38
|
-
Ruby Implementation
|
38
|
+
Ruby Implementation 1.500k (± 3.4%) i/s (666.84 μs/i) - 4.560k in 3.044713s
|
39
39
|
|
40
40
|
Distribution Stats:
|
41
41
|
Video: 334 (33.4%)
|
@@ -45,9 +45,9 @@ Article: 333 (33.3%)
|
|
45
45
|
Benchmarking Large Dataset (10000 items)
|
46
46
|
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +YJIT +PRISM [aarch64-linux]
|
47
47
|
Warming up --------------------------------------
|
48
|
-
Ruby Implementation
|
48
|
+
Ruby Implementation 15.000 i/100ms
|
49
49
|
Calculating -------------------------------------
|
50
|
-
Ruby Implementation
|
50
|
+
Ruby Implementation 151.656 (± 3.3%) i/s (6.59 ms/i) - 465.000 in 3.070430s
|
51
51
|
|
52
52
|
Distribution Stats:
|
53
53
|
Video: 3334 (33.34%)
|
@@ -4,10 +4,8 @@
|
|
4
4
|
# rubocop:disable Metrics/ClassLength
|
5
5
|
# rubocop:disable Metrics/MethodLength
|
6
6
|
# rubocop:disable Metrics/AbcSize
|
7
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
8
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
9
7
|
|
10
|
-
|
8
|
+
require_relative '../lib/type_balancer'
|
11
9
|
require 'yaml'
|
12
10
|
require 'json'
|
13
11
|
|
@@ -79,211 +77,92 @@ class LargeScaleBalanceTest
|
|
79
77
|
items.shuffle
|
80
78
|
end
|
81
79
|
|
82
|
-
def run_balance_test(
|
80
|
+
def run_balance_test(test_data, **options)
|
83
81
|
@tests_run += 1
|
84
82
|
puts "\nRunning balance test..."
|
85
|
-
puts "Strategy options: #{strategy_options.inspect}" unless strategy_options.empty?
|
86
83
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
window_size = strategy_options[:window_size] || 10
|
95
|
-
|
96
|
-
# Track remaining items for each type
|
97
|
-
remaining_items = @type_distribution.dup
|
98
|
-
|
99
|
-
# Analyze windows
|
100
|
-
balanced_items.each_slice(window_size).with_index do |window, index|
|
101
|
-
window_result = analyze_window(window, index + 1, remaining_items)
|
102
|
-
test_passed = false unless window_result
|
103
|
-
|
104
|
-
# Update remaining items
|
105
|
-
window.each do |item|
|
106
|
-
remaining_items[item[:type]] -= 1
|
107
|
-
end
|
84
|
+
begin
|
85
|
+
result = TypeBalancer.balance(test_data, type_field: :type, **options)
|
86
|
+
analyze_result(result, options)
|
87
|
+
@tests_passed += 1
|
88
|
+
rescue StandardError => e
|
89
|
+
record_failure("Balance test failed: #{e.message}")
|
90
|
+
puts "#{RED}Balance test failed: #{e.message}#{RESET}"
|
108
91
|
end
|
109
|
-
|
110
|
-
# Analyze full distribution
|
111
|
-
distribution_result = analyze_full_distribution(balanced_items)
|
112
|
-
test_passed = false unless distribution_result
|
113
|
-
|
114
|
-
# Analyze type transitions
|
115
|
-
transition_result = analyze_type_transitions(balanced_items)
|
116
|
-
test_passed = false unless transition_result
|
117
|
-
|
118
|
-
@tests_passed += 1 if test_passed
|
119
92
|
end
|
120
93
|
|
121
|
-
def
|
122
|
-
|
123
|
-
|
124
|
-
window_passed = true
|
125
|
-
|
126
|
-
distribution.each do |type, count|
|
127
|
-
percentage = (count.to_f / items.size * 100).round(1)
|
128
|
-
puts "#{type}: #{count} (#{percentage}%)"
|
129
|
-
end
|
130
|
-
|
131
|
-
# Calculate how many items we have left to work with
|
132
|
-
total_remaining = remaining_items.values.sum
|
133
|
-
remaining_items.transform_values { |count| count.to_f / total_remaining }
|
134
|
-
|
135
|
-
# Only enforce strict distribution when we have enough items of each type
|
136
|
-
has_enough_items = remaining_items.values.all? { |count| count >= items.size / 3 }
|
137
|
-
|
138
|
-
if has_enough_items
|
139
|
-
# When we have enough items, ensure each type that has items left appears at least once
|
140
|
-
remaining_items.each do |type, count|
|
141
|
-
next if count <= 0
|
142
|
-
|
143
|
-
unless distribution.key?(type)
|
144
|
-
record_failure("Window #{window_number}: #{type} does not appear but has #{count} items remaining")
|
145
|
-
window_passed = false
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
# Prevent any type from completely dominating a window when we have enough items
|
150
|
-
max_allowed = (items.size * 0.7).ceil # Allow up to 70% when we have enough items
|
151
|
-
distribution.each do |type, count|
|
152
|
-
next unless count > max_allowed
|
94
|
+
def analyze_result(result, options)
|
95
|
+
window_size = options[:window_size] || 10
|
96
|
+
puts "\nAnalyzing windows of size #{window_size}:"
|
153
97
|
|
154
|
-
|
155
|
-
|
156
|
-
record_failure(message)
|
157
|
-
window_passed = false
|
158
|
-
end
|
159
|
-
else
|
160
|
-
# When running low on items, just verify we're using available items efficiently
|
161
|
-
distribution.each do |type, count|
|
162
|
-
max_possible = [remaining_items[type], items.size].min
|
163
|
-
next unless count > max_possible
|
164
|
-
|
165
|
-
message = "Window #{window_number}: #{type} appears #{count} times but only had #{max_possible} items available"
|
166
|
-
record_failure(message)
|
167
|
-
window_passed = false
|
168
|
-
end
|
98
|
+
result.each_slice(window_size).with_index(1) do |window, index|
|
99
|
+
analyze_window(window, index)
|
169
100
|
end
|
170
101
|
|
171
|
-
|
102
|
+
analyze_overall_distribution(result)
|
172
103
|
end
|
173
104
|
|
174
|
-
def
|
175
|
-
puts "\
|
176
|
-
|
177
|
-
|
105
|
+
def analyze_window(window, window_number)
|
106
|
+
puts "\nAnalyzing window #{window_number} (#{window.size} items):"
|
107
|
+
type_counts = window.group_by { |item| item[:type] }.transform_values(&:size)
|
108
|
+
total_in_window = window.size.to_f
|
178
109
|
|
179
|
-
|
180
|
-
percentage = (count
|
110
|
+
type_counts.each do |type, count|
|
111
|
+
percentage = (count / total_in_window * 100).round(1)
|
181
112
|
target_percentage = (@type_distribution[type].to_f / @total_records * 100).round(1)
|
182
113
|
diff = (percentage - target_percentage).abs.round(1)
|
183
|
-
color = if diff <= 0.1
|
184
|
-
GREEN
|
185
|
-
elsif diff <= 0.5
|
186
|
-
YELLOW
|
187
|
-
else
|
188
|
-
RED
|
189
|
-
end
|
190
|
-
puts "#{color}#{type}: #{count} (#{percentage}%) - Target: #{target_percentage}% (Diff: #{diff}%)#{RESET}"
|
191
114
|
|
192
|
-
|
193
|
-
|
115
|
+
message = "#{type}: #{count} (#{percentage}%), "
|
116
|
+
message += if diff <= 15
|
117
|
+
"#{GREEN}acceptable deviation#{RESET}"
|
118
|
+
else
|
119
|
+
"#{RED}high deviation#{RESET}"
|
120
|
+
end
|
194
121
|
|
195
|
-
message
|
196
|
-
message += "(expected #{target_percentage}%, got #{percentage}%)"
|
197
|
-
record_failure(message)
|
198
|
-
distribution_passed = false
|
122
|
+
puts message
|
199
123
|
end
|
200
|
-
|
201
|
-
distribution_passed
|
202
124
|
end
|
203
125
|
|
204
|
-
def
|
205
|
-
puts "\
|
206
|
-
|
207
|
-
|
208
|
-
transition_passed = true
|
126
|
+
def analyze_overall_distribution(result)
|
127
|
+
puts "\nOverall Distribution:"
|
128
|
+
type_counts = result.group_by { |item| item[:type] }.transform_values(&:size)
|
129
|
+
total = result.size.to_f
|
209
130
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
items.each do |item|
|
216
|
-
# Update remaining items
|
217
|
-
remaining_items[item[:type]] -= 1
|
218
|
-
total_remaining = remaining_items.values.sum
|
219
|
-
available_types = remaining_items.count { |_, count| count.positive? }
|
220
|
-
|
221
|
-
if item[:type] == current_type
|
222
|
-
consecutive_count += 1
|
223
|
-
# Allow longer runs when we're running out of items
|
224
|
-
max_consecutive = if available_types >= 3 && total_remaining >= 100
|
225
|
-
5 # Strict when we have lots of items and all types
|
226
|
-
elsif available_types >= 2 && total_remaining >= 50
|
227
|
-
8 # More lenient as we start running out
|
228
|
-
elsif available_types >= 2 && total_remaining >= 20
|
229
|
-
12 # Even more lenient with two types
|
230
|
-
else
|
231
|
-
Float::INFINITY # No limit when almost out or only one type left
|
232
|
-
end
|
233
|
-
|
234
|
-
if consecutive_count > max_consecutive
|
235
|
-
message = "Found #{consecutive_count} consecutive #{current_type} items "
|
236
|
-
message += "when #{total_remaining} total items remained (#{available_types} types available)"
|
237
|
-
record_failure(message)
|
238
|
-
transition_passed = false
|
239
|
-
break # Stop checking transitions once we find a violation
|
240
|
-
end
|
241
|
-
else
|
242
|
-
consecutive_count = 1
|
243
|
-
current_type = item[:type]
|
244
|
-
end
|
245
|
-
end
|
131
|
+
type_counts.each do |type, count|
|
132
|
+
percentage = (count / total * 100).round(1)
|
133
|
+
target_percentage = (@type_distribution[type].to_f / @total_records * 100).round(1)
|
134
|
+
diff = (percentage - target_percentage).abs.round(1)
|
246
135
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
136
|
+
message = "#{type}: #{count} (#{percentage}% vs target #{target_percentage}%), "
|
137
|
+
message += if diff <= 5
|
138
|
+
"#{GREEN}good distribution#{RESET}"
|
139
|
+
else
|
140
|
+
"#{RED}distribution needs improvement#{RESET}"
|
141
|
+
end
|
252
142
|
|
253
|
-
|
254
|
-
puts "\nTransitions from #{from_type}:"
|
255
|
-
to_types.each do |to_type, count|
|
256
|
-
percentage = (count.to_f / total_transitions * 100).round(1)
|
257
|
-
puts " to #{to_type}: #{count} (#{percentage}%)"
|
258
|
-
end
|
143
|
+
puts message
|
259
144
|
end
|
260
|
-
|
261
|
-
transition_passed
|
262
145
|
end
|
263
146
|
|
264
147
|
def print_summary
|
265
|
-
puts "\n#{
|
266
|
-
|
267
|
-
|
148
|
+
puts "\n#{YELLOW}Test Summary:#{RESET}"
|
149
|
+
puts "Tests Run: #{@tests_run}"
|
150
|
+
puts "Tests Passed: #{@tests_passed}"
|
151
|
+
puts "Failures: #{@failures.size}"
|
152
|
+
|
153
|
+
if @failures.any?
|
154
|
+
puts "\n#{RED}Failures:#{RESET}"
|
155
|
+
@failures.each { |failure| puts "- #{failure}" }
|
268
156
|
else
|
269
|
-
puts "#{
|
270
|
-
@failures.each_with_index do |failure, index|
|
271
|
-
puts "#{index + 1}. #{failure}"
|
272
|
-
end
|
157
|
+
puts "\n#{GREEN}All tests passed!#{RESET}"
|
273
158
|
end
|
274
|
-
puts "Tests run: #{@tests_run}"
|
275
|
-
puts "Tests passed: #{@tests_passed}"
|
276
|
-
puts('-' * 50)
|
277
159
|
end
|
278
160
|
end
|
279
161
|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
end
|
162
|
+
# Run the test
|
163
|
+
test = LargeScaleBalanceTest.new
|
164
|
+
exit(test.run ? 0 : 1)
|
284
165
|
|
285
166
|
# rubocop:enable Metrics/ClassLength
|
286
167
|
# rubocop:enable Metrics/MethodLength
|
287
168
|
# rubocop:enable Metrics/AbcSize
|
288
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
289
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
data/examples/quality.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative '../lib/type_balancer'
|
4
4
|
require 'yaml'
|
5
5
|
|
6
6
|
class QualityChecker
|
@@ -422,20 +422,23 @@ class QualityChecker
|
|
422
422
|
end
|
423
423
|
|
424
424
|
def print_summary
|
425
|
-
puts "\n#{'-' *
|
426
|
-
puts
|
425
|
+
puts "\n#{'-' * 80}"
|
426
|
+
puts "#{YELLOW}QUALITY CHECK SUMMARY#{RESET}"
|
427
|
+
puts "#{'-' * 80}"
|
427
428
|
puts "Examples Run: #{@examples_run}"
|
428
|
-
puts "
|
429
|
+
puts "Examples Passed: #{@examples_passed}"
|
430
|
+
puts "Examples Failed: #{@examples_run - @examples_passed}"
|
431
|
+
puts "#{'-' * 80}"
|
429
432
|
|
430
433
|
if @issues.empty?
|
431
|
-
puts "\n#{GREEN}
|
434
|
+
puts "\n#{GREEN}✓ FINAL STATUS: ALL QUALITY CHECKS PASSED!#{RESET}"
|
432
435
|
else
|
433
|
-
puts "\n#{RED}
|
436
|
+
puts "\n#{RED}✗ FINAL STATUS: QUALITY CHECKS FAILED WITH #{@issues.size} ISSUES:#{RESET}"
|
434
437
|
@issues.each_with_index do |issue, index|
|
435
438
|
puts "#{RED}#{index + 1}. #{issue}#{RESET}"
|
436
439
|
end
|
437
440
|
end
|
438
|
-
puts "#{'-' *
|
441
|
+
puts "#{'-' * 80}"
|
439
442
|
end
|
440
443
|
|
441
444
|
# Print a summary table for a section
|
@@ -114,26 +114,30 @@ module TypeBalancer
|
|
114
114
|
class Calculator
|
115
115
|
DEFAULT_TYPE_ORDER = %w[video image strip article].freeze
|
116
116
|
|
117
|
-
|
117
|
+
# rubocop:disable Metrics/ParameterLists
|
118
|
+
def initialize(items, type_field: :type, types: nil, type_order: nil, strategy: nil, **strategy_options)
|
118
119
|
raise ArgumentError, 'Items cannot be nil' if items.nil?
|
119
120
|
raise ArgumentError, 'Type field cannot be nil' if type_field.nil?
|
120
121
|
|
121
122
|
@items = items
|
122
123
|
@type_field = type_field
|
123
124
|
@types = types
|
125
|
+
@type_order = type_order
|
124
126
|
@strategy_name = strategy
|
125
127
|
@strategy_options = strategy_options
|
126
128
|
end
|
129
|
+
# rubocop:enable Metrics/ParameterLists
|
127
130
|
|
128
131
|
def call
|
129
132
|
return [] if @items.empty?
|
130
133
|
|
131
|
-
# Create strategy instance
|
134
|
+
# Create strategy instance with all options
|
132
135
|
strategy = StrategyFactory.create(
|
133
136
|
@strategy_name,
|
134
137
|
items: @items,
|
135
138
|
type_field: @type_field,
|
136
139
|
types: @types || extract_types,
|
140
|
+
type_order: @type_order,
|
137
141
|
**@strategy_options
|
138
142
|
)
|
139
143
|
|
@@ -145,7 +149,14 @@ module TypeBalancer
|
|
145
149
|
|
146
150
|
def extract_types
|
147
151
|
types = @items.map { |item| item[@type_field].to_s }.uniq
|
148
|
-
|
152
|
+
if @type_order
|
153
|
+
# First include ordered types that exist in the items
|
154
|
+
ordered = @type_order & types
|
155
|
+
# Then append any remaining types that weren't in the order
|
156
|
+
ordered + (types - @type_order)
|
157
|
+
else
|
158
|
+
DEFAULT_TYPE_ORDER.select { |type| types.include?(type) } + (types - DEFAULT_TYPE_ORDER)
|
159
|
+
end
|
149
160
|
end
|
150
161
|
end
|
151
162
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypeBalancer
|
4
|
+
# Configuration class to handle all balancing options
|
5
|
+
class Configuration
|
6
|
+
attr_accessor :type_field, :type_order, :strategy, :window_size, :batch_size, :types
|
7
|
+
attr_reader :strategy_options
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@type_field = options.fetch(:type_field, :type)
|
11
|
+
@type_order = options[:type_order]
|
12
|
+
@strategy = options[:strategy]
|
13
|
+
@window_size = options[:window_size]
|
14
|
+
@batch_size = options[:batch_size]
|
15
|
+
@types = options[:types]
|
16
|
+
@strategy_options = extract_strategy_options(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def merge_window_size
|
20
|
+
return strategy_options unless window_size
|
21
|
+
|
22
|
+
strategy_options.merge(window_size: window_size)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def extract_strategy_options(options)
|
28
|
+
options.reject { |key, _| %i[type_field type_order strategy window_size types].include?(key) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -4,10 +4,11 @@ module TypeBalancer
|
|
4
4
|
module Strategies
|
5
5
|
# Base class for all balancing strategies
|
6
6
|
class BaseStrategy
|
7
|
-
def initialize(items:, type_field:, types: nil)
|
7
|
+
def initialize(items:, type_field:, types: nil, type_order: nil)
|
8
8
|
@items = items
|
9
9
|
@type_field = type_field
|
10
10
|
@types = types
|
11
|
+
@type_order = type_order
|
11
12
|
end
|
12
13
|
|
13
14
|
# Interface method that all strategies must implement
|
@@ -26,7 +27,15 @@ module TypeBalancer
|
|
26
27
|
|
27
28
|
def extract_types
|
28
29
|
types = @items.map { |item| item[@type_field].to_s }.uniq
|
29
|
-
|
30
|
+
if @type_order
|
31
|
+
# First include ordered types that exist in the items
|
32
|
+
ordered = @type_order & types
|
33
|
+
# Then append any remaining types that weren't in the order
|
34
|
+
ordered + (types - @type_order)
|
35
|
+
else
|
36
|
+
# Use default order if no custom order provided
|
37
|
+
DEFAULT_TYPE_ORDER.select { |type| types.include?(type) } + (types - DEFAULT_TYPE_ORDER)
|
38
|
+
end
|
30
39
|
end
|
31
40
|
|
32
41
|
def group_items_by_type
|
@@ -4,135 +4,194 @@ require_relative 'base_strategy'
|
|
4
4
|
|
5
5
|
module TypeBalancer
|
6
6
|
module Strategies
|
7
|
-
# Implements
|
7
|
+
# Implements an efficient sliding window approach for balancing items
|
8
|
+
# This strategy uses array-based indexing and pre-calculated ratios for optimal performance
|
8
9
|
class SlidingWindowStrategy < BaseStrategy
|
9
|
-
|
10
|
-
|
10
|
+
DEFAULT_BATCH_SIZE = 1000
|
11
|
+
|
12
|
+
# rubocop:disable Metrics/ParameterLists
|
13
|
+
def initialize(items:, type_field:, types: nil, type_order: nil, window_size: 10, batch_size: DEFAULT_BATCH_SIZE)
|
14
|
+
super(items: items, type_field: type_field, types: types, type_order: type_order)
|
11
15
|
@window_size = window_size
|
12
|
-
@
|
16
|
+
@batch_size = batch_size
|
17
|
+
@types = types || extract_types
|
13
18
|
end
|
19
|
+
# rubocop:enable Metrics/ParameterLists
|
14
20
|
|
15
21
|
def balance
|
16
22
|
return [] if @items.empty?
|
17
23
|
|
18
24
|
validate_items!
|
19
|
-
return @items.dup if
|
25
|
+
return @items.dup if single_type?
|
20
26
|
|
21
|
-
type_queues
|
22
|
-
type_ratios
|
27
|
+
@type_queues = build_type_queues
|
28
|
+
@type_ratios = calculate_type_ratios
|
23
29
|
|
24
|
-
|
30
|
+
if @items.size > @batch_size
|
31
|
+
process_large_collection
|
32
|
+
else
|
33
|
+
process_single_batch
|
34
|
+
end
|
25
35
|
end
|
26
36
|
|
27
37
|
private
|
28
38
|
|
29
|
-
def
|
30
|
-
|
31
|
-
type_queues.transform_values { |list| list.size / total_items }
|
39
|
+
def single_type?
|
40
|
+
@items.map { |item| item[@type_field].to_s }.uniq.one?
|
32
41
|
end
|
33
42
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
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)
|
43
|
+
def build_type_queues
|
44
|
+
queues = {}
|
45
|
+
ordered_types = @type_order || @types
|
46
|
+
ordered_types.each { |t| queues[t] = [] }
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
48
|
+
@items.each_with_index do |item, idx|
|
49
|
+
t = item[@type_field].to_s
|
50
|
+
queues[t] << idx if queues.key?(t)
|
52
51
|
end
|
53
52
|
|
54
|
-
|
53
|
+
queues
|
55
54
|
end
|
56
55
|
|
57
|
-
def
|
58
|
-
|
56
|
+
def calculate_type_ratios
|
57
|
+
total = @items.size.to_f
|
58
|
+
@type_queues.transform_values { |inds| inds.size / total }
|
59
59
|
end
|
60
60
|
|
61
|
-
def
|
62
|
-
@items.
|
63
|
-
|
61
|
+
def process_large_collection
|
62
|
+
result = Array.new(@items.size)
|
63
|
+
type_indices = initialize_type_indices
|
64
64
|
|
65
|
-
|
66
|
-
|
65
|
+
(0...@items.size).step(@batch_size) do |start_idx|
|
66
|
+
end_idx = [start_idx + @batch_size, @items.size].min
|
67
|
+
process_batch_range(result, type_indices, start_idx, end_idx)
|
67
68
|
end
|
69
|
+
|
70
|
+
result.compact
|
68
71
|
end
|
69
72
|
|
70
|
-
def
|
71
|
-
|
72
|
-
|
73
|
-
|
73
|
+
def process_single_batch
|
74
|
+
result = Array.new(@items.size)
|
75
|
+
process_batch_range(result, initialize_type_indices, 0, @items.size)
|
76
|
+
result.compact
|
77
|
+
end
|
74
78
|
|
75
|
-
|
76
|
-
|
77
|
-
|
79
|
+
def initialize_type_indices
|
80
|
+
@type_queues.transform_values { 0 }
|
81
|
+
end
|
78
82
|
|
79
|
-
|
80
|
-
|
83
|
+
def process_batch_range(result, type_indices, start_idx, end_idx)
|
84
|
+
window_start = start_idx
|
81
85
|
|
82
|
-
|
83
|
-
|
86
|
+
while window_start < end_idx
|
87
|
+
window_size = compute_window_size(window_start, end_idx)
|
88
|
+
positions = calculate_window_positions(window_size)
|
89
|
+
apply_window_positions(positions, window_start, window_size, result, type_indices)
|
90
|
+
window_start += window_size
|
84
91
|
end
|
85
92
|
|
86
|
-
|
93
|
+
fill_gaps(result, type_indices, start_idx, end_idx)
|
87
94
|
end
|
88
95
|
|
89
|
-
def
|
90
|
-
|
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
|
96
|
+
def compute_window_size(start_pos, end_pos)
|
97
|
+
[[start_pos + @window_size, end_pos].min - start_pos, 1].max
|
95
98
|
end
|
96
99
|
|
97
|
-
def
|
98
|
-
|
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] }
|
100
|
+
def calculate_window_positions(window_size)
|
101
|
+
WindowSlotCalculator.new(@type_ratios, @type_order).calculate(window_size)
|
101
102
|
end
|
102
103
|
|
103
|
-
def
|
104
|
-
|
105
|
-
|
104
|
+
def apply_window_positions(positions, start_pos, size, result, type_indices)
|
105
|
+
ordered_types = @type_order || @type_queues.keys
|
106
|
+
ordered_types.each do |type|
|
107
|
+
next unless positions[type]
|
108
|
+
|
109
|
+
positions[type].times do
|
110
|
+
break if type_indices[type] >= @type_queues[type].size
|
111
|
+
|
112
|
+
pos = find_next_position(result, start_pos, start_pos + size)
|
113
|
+
break unless pos
|
114
|
+
|
115
|
+
result[pos] = @items[@type_queues[type][type_indices[type]]]
|
116
|
+
type_indices[type] += 1
|
117
|
+
end
|
106
118
|
end
|
107
119
|
end
|
108
120
|
|
109
|
-
def
|
110
|
-
|
111
|
-
return unless total > window_size
|
112
|
-
|
113
|
-
factor = window_size.to_f / total
|
114
|
-
targets.transform_values! { |count| (count * factor).floor }
|
121
|
+
def find_next_position(result, start_pos, end_pos)
|
122
|
+
(start_pos...end_pos).find { |i| result[i].nil? }
|
115
123
|
end
|
116
124
|
|
117
|
-
def
|
118
|
-
|
119
|
-
return unless remaining.positive?
|
125
|
+
def fill_gaps(result, type_indices, start_idx, end_idx)
|
126
|
+
ordered_types = @type_order || @type_queues.keys
|
120
127
|
|
121
|
-
|
122
|
-
|
123
|
-
end
|
128
|
+
(start_idx...end_idx).each do |i|
|
129
|
+
next unless result[i].nil?
|
124
130
|
|
125
|
-
|
126
|
-
|
127
|
-
return type_ratios.dup if total.zero?
|
131
|
+
ordered_types.each do |type|
|
132
|
+
next unless @type_queues[type] && type_indices[type] < @type_queues[type].size
|
128
133
|
|
129
|
-
|
134
|
+
result[i] = @items[@type_queues[type][type_indices[type]]]
|
135
|
+
type_indices[type] += 1
|
136
|
+
break
|
137
|
+
end
|
138
|
+
end
|
130
139
|
end
|
131
140
|
|
132
|
-
|
133
|
-
type_ratios
|
134
|
-
|
135
|
-
|
141
|
+
class WindowSlotCalculator
|
142
|
+
def initialize(type_ratios, type_order)
|
143
|
+
@type_ratios = type_ratios
|
144
|
+
@type_order = type_order
|
145
|
+
end
|
146
|
+
|
147
|
+
def calculate(window_size)
|
148
|
+
slots = build_initial_slots(window_size)
|
149
|
+
distribute_remaining_slots(slots)
|
150
|
+
slots
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def build_initial_slots(window_size)
|
156
|
+
slots = {}
|
157
|
+
remaining_ratio = 1.0
|
158
|
+
@remaining_slots = window_size
|
159
|
+
|
160
|
+
ordered_types.each do |t|
|
161
|
+
ratio = @type_ratios[t] || 0
|
162
|
+
target = calculate_target(window_size, ratio, remaining_ratio)
|
163
|
+
slots[t] = target
|
164
|
+
@remaining_slots -= target
|
165
|
+
remaining_ratio -= ratio
|
166
|
+
end
|
167
|
+
|
168
|
+
slots
|
169
|
+
end
|
170
|
+
|
171
|
+
def calculate_target(size, ratio, rem_ratio)
|
172
|
+
tgt = (size * (ratio / rem_ratio)).floor
|
173
|
+
tgt = [tgt, @remaining_slots].min
|
174
|
+
tgt = 1 if ratio.positive? && tgt.zero? && @remaining_slots.positive?
|
175
|
+
tgt
|
176
|
+
end
|
177
|
+
|
178
|
+
def distribute_remaining_slots(slots)
|
179
|
+
return if @remaining_slots <= 0
|
180
|
+
|
181
|
+
types = sorted_distribution_types
|
182
|
+
@remaining_slots.times { |i| slots[types[i % types.size]] += 1 }
|
183
|
+
end
|
184
|
+
|
185
|
+
def ordered_types
|
186
|
+
@type_order || @type_ratios.keys
|
187
|
+
end
|
188
|
+
|
189
|
+
def sorted_distribution_types
|
190
|
+
if @type_order
|
191
|
+
@type_order & @type_ratios.keys
|
192
|
+
else
|
193
|
+
@type_ratios.sort_by { |_t, r| -r }.map(&:first)
|
194
|
+
end
|
136
195
|
end
|
137
196
|
end
|
138
197
|
end
|
data/lib/type_balancer.rb
CHANGED
@@ -20,6 +20,7 @@ module TypeBalancer
|
|
20
20
|
|
21
21
|
# Register default strategies
|
22
22
|
StrategyFactory.register(:sliding_window, Strategies::SlidingWindowStrategy)
|
23
|
+
StrategyFactory.default_strategy = :sliding_window
|
23
24
|
|
24
25
|
# Load Ruby implementations
|
25
26
|
require_relative 'type_balancer/distribution_calculator'
|
@@ -43,7 +44,8 @@ module TypeBalancer
|
|
43
44
|
)
|
44
45
|
end
|
45
46
|
|
46
|
-
|
47
|
+
# rubocop:disable Metrics/ParameterLists
|
48
|
+
def self.balance(items, type_field: :type, type_order: nil, strategy: nil, window_size: nil, **strategy_options)
|
47
49
|
# Input validation
|
48
50
|
raise EmptyCollectionError, 'Collection cannot be empty' if items.empty?
|
49
51
|
|
@@ -56,11 +58,15 @@ module TypeBalancer
|
|
56
58
|
raise Error, "Cannot access type field '#{type_field}': #{e.message}"
|
57
59
|
end
|
58
60
|
|
61
|
+
# Merge window_size into strategy_options if provided
|
62
|
+
strategy_options = strategy_options.merge(window_size: window_size) if window_size
|
63
|
+
|
59
64
|
# Create calculator with strategy options
|
60
65
|
calculator = Calculator.new(
|
61
66
|
items,
|
62
67
|
type_field: type_field,
|
63
68
|
types: type_order || types,
|
69
|
+
type_order: type_order,
|
64
70
|
strategy: strategy,
|
65
71
|
**strategy_options
|
66
72
|
)
|
@@ -68,6 +74,7 @@ module TypeBalancer
|
|
68
74
|
# Balance items
|
69
75
|
calculator.call
|
70
76
|
end
|
77
|
+
# rubocop:enable Metrics/ParameterLists
|
71
78
|
|
72
79
|
# Backward compatibility methods
|
73
80
|
def self.extract_types(items, type_field)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: type_balancer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Carl Smith
|
@@ -50,6 +50,7 @@ files:
|
|
50
50
|
- lib/type_balancer/balancer.rb
|
51
51
|
- lib/type_balancer/batch_processing.rb
|
52
52
|
- lib/type_balancer/calculator.rb
|
53
|
+
- lib/type_balancer/configuration.rb
|
53
54
|
- lib/type_balancer/distribution_calculator.rb
|
54
55
|
- lib/type_balancer/distributor.rb
|
55
56
|
- lib/type_balancer/ordered_collection_manager.rb
|