1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
//! Ratio-based size resolution.
//!
//! Port of `rich/_ratio.py`. [`ratio_resolve`] distributes a total span among
//! a set of edges, each of which may pin a fixed `size`, or flex by `ratio`
//! down to a `minimum_size`; [`Layout`](crate::layout::Layout) sizes its split
//! regions with it. [`ratio_reduce`] and [`ratio_distribute`] size `Table`
//! columns.
/// One participant in a [`ratio_resolve`] distribution.
#[derive(Debug, Clone, Copy)]
pub struct Edge {
/// A fixed size, if pinned.
pub size: Option<usize>,
/// Flex weight when `size` is `None` (defaults to 1 upstream).
pub ratio: usize,
/// The smallest size a flexible edge may shrink to.
pub minimum_size: usize,
}
impl Edge {
pub fn new(size: Option<usize>, ratio: usize, minimum_size: usize) -> Self {
Edge {
size,
ratio,
minimum_size,
}
}
}
/// Distribute `total` across `edges`, returning a concrete size per edge.
///
/// Direct port of `rich._ratio.ratio_resolve`, with its `Fraction`
/// arithmetic done exactly on integers (`portion = remaining / ratio_sum`
/// is kept as a numerator over `ratio_sum`), so no float rounding can drop
/// a cell.
pub fn ratio_resolve(total: usize, edges: &[Edge]) -> Vec<usize> {
// `edge.size or None`: a size of 0 is flexible, like no size.
let mut sizes: Vec<Option<usize>> = edges
.iter()
.map(|e| e.size.filter(|&size| size > 0))
.collect();
// Resolve one flexible edge per pass until all are fixed.
while sizes.iter().any(Option::is_none) {
let flexible: Vec<usize> = sizes
.iter()
.enumerate()
.filter(|(_, s)| s.is_none())
.map(|(i, _)| i)
.collect();
let fixed_sum: i128 = sizes.iter().flatten().map(|&s| s as i128).sum();
let remaining = total as i128 - fixed_sum;
if remaining <= 0 {
// No room for flexible edges: give each its minimum (or its size).
return sizes
.iter()
.zip(edges)
.map(|(size, edge)| match size {
Some(s) => *s,
None => edge.minimum_size.max(1),
})
.collect();
}
// `portion = Fraction(remaining, sum(edge.ratio or 1 ...))`. `u128`
// holds any `usize` product, so nothing here overflows.
let remaining = remaining as u128;
let ratio_sum: u128 = flexible
.iter()
.map(|&i| edges[i].ratio.max(1) as u128)
.fold(0, u128::saturating_add);
// If any flexible edge would fall below its minimum
// (`portion * edge.ratio <= edge.minimum_size`), pin it and retry —
// a newly fixed size changes the remaining distribution.
let mut pinned = false;
for &i in &flexible {
let edge = &edges[i];
if remaining * edge.ratio as u128
<= (edge.minimum_size as u128).saturating_mul(ratio_sum)
{
sizes[i] = Some(edge.minimum_size);
pinned = true;
break;
}
}
if !pinned {
// `size, remainder = divmod(portion * edge.ratio + remainder, 1)`,
// in units of `1 / ratio_sum`.
let mut remainder = 0u128;
for &i in &flexible {
let value = (remaining * edges[i].ratio as u128).saturating_add(remainder);
sizes[i] = Some(usize::try_from(value / ratio_sum).unwrap_or(usize::MAX));
remainder = value % ratio_sum;
}
break;
}
}
sizes.into_iter().map(|s| s.unwrap_or(0)).collect()
}
/// Reduce `values` by `total`, distributed across slots by `ratios` (capped by
/// `maximums`): a slot whose maximum is 0 takes no share. Port of
/// `rich._ratio.ratio_reduce`.
pub fn ratio_reduce(total: i64, ratios: &[i64], maximums: &[i64], values: &[i64]) -> Vec<i64> {
let ratios: Vec<i64> = ratios
.iter()
.zip(maximums)
.map(|(&r, &m)| if m != 0 { r } else { 0 })
.collect();
let mut total_ratio: i128 = ratios.iter().map(|&r| i128::from(r)).sum();
if total_ratio == 0 {
return values.to_vec();
}
let mut total_remaining = total;
let mut result = Vec::with_capacity(values.len());
for ((&ratio, &maximum), &value) in ratios.iter().zip(maximums).zip(values) {
if ratio != 0 && total_ratio > 0 {
// `round(ratio * total_remaining / total_ratio)`: Python rounds
// half to even.
let distributed = maximum.min(
(ratio as f64 * total_remaining as f64 / total_ratio as f64).round_ties_even()
as i64,
);
result.push(value - distributed);
total_remaining -= distributed;
total_ratio -= i128::from(ratio);
} else {
result.push(value);
}
}
result
}
/// Divide `total` across slots proportionally to `ratios` (ceil each share),
/// each share floored at the matching `minimums` entry when given. Port of
/// `rich._ratio.ratio_distribute`; where upstream asserts the ratios sum to
/// more than 0, the whole total goes to the first slot.
pub fn ratio_distribute(total: i64, ratios: &[i64], minimums: Option<&[i64]>) -> Vec<i64> {
// Upstream zeroes the ratio of any slot whose minimum is 0 (falsy).
let ratios: Vec<i64> = match minimums {
Some(mins) => ratios
.iter()
.zip(mins)
.map(|(&r, &m)| if m != 0 { r } else { 0 })
.collect(),
None => ratios.to_vec(),
};
// Python ints never overflow; `ratio * total_remaining` can exceed i64
// for a huge ratio, so the arithmetic runs in i128.
let mut total_ratio: i128 = ratios.iter().map(|&r| i128::from(r)).sum();
let mut total_remaining = i128::from(total);
let mut result = Vec::with_capacity(ratios.len());
for (index, &ratio) in ratios.iter().enumerate() {
let ratio = i128::from(ratio);
let minimum = i128::from(minimums.map_or(0, |m| m[index]));
let distributed = if total_ratio > 0 {
// `max(minimum, ceil(ratio * total_remaining / total_ratio))`;
// the remaining total goes negative once a minimum overshoots.
let numerator = ratio * total_remaining;
minimum.max(-(-numerator).div_euclid(total_ratio))
} else {
total_remaining
};
result.push(i64::try_from(distributed).unwrap_or(if distributed < 0 {
i64::MIN
} else {
i64::MAX
}));
total_ratio -= ratio;
total_remaining -= distributed;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
fn edges(specs: &[(Option<usize>, usize)]) -> Vec<Edge> {
specs
.iter()
.map(|&(size, ratio)| Edge::new(size, ratio, 1))
.collect()
}
#[test]
fn even_split_carries_remainder() {
// 23 across two ratio-1 edges → 11, 12 (matches upstream's divmod).
assert_eq!(
ratio_resolve(23, &edges(&[(None, 1), (None, 1)])),
vec![11, 12]
);
}
#[test]
fn even_split_exact() {
assert_eq!(
ratio_resolve(24, &edges(&[(None, 1), (None, 1)])),
vec![12, 12]
);
}
#[test]
fn fixed_and_flex() {
// One flexible (ratio 3) + one fixed size 5, total 24 → 19, 5.
assert_eq!(
ratio_resolve(24, &edges(&[(None, 3), (Some(5), 1)])),
vec![19, 5]
);
}
#[test]
fn ratio_weighting() {
// 3:1 across 24 → 18, 6.
assert_eq!(
ratio_resolve(24, &edges(&[(None, 3), (None, 1)])),
vec![18, 6]
);
}
}