1use rich::{Console, ConsoleOptions, Renderable, Segment, Table, Text};
4
5use super::group::{Aggregate, GroupBy};
6use super::sort::{sorted_indices, SortKey};
7use super::{frame_builders, headers, style, Column, Frame, Value};
8
9#[derive(Clone, Debug)]
55pub struct TableData {
56 columns: Vec<Column>,
57 rows: Vec<Vec<Value>>,
58 sort: Vec<SortKey>,
59 group: Option<GroupBy>,
60 totals: Vec<Aggregate>,
61 totals_label: String,
62 pub(super) frame: Frame,
63}
64
65frame_builders!([] TableData);
66
67impl TableData {
68 pub fn new(columns: impl IntoIterator<Item = Column>) -> Self {
70 TableData {
71 columns: columns.into_iter().collect(),
72 rows: Vec::new(),
73 sort: Vec::new(),
74 group: None,
75 totals: Vec::new(),
76 totals_label: "total".to_string(),
77 frame: Frame::default(),
78 }
79 }
80
81 pub fn push(&mut self, row: impl IntoIterator<Item = Value>) -> &mut Self {
83 let row = normalize(row, self.columns.len());
84 self.rows.push(row);
85 self
86 }
87
88 pub fn extend<R: IntoIterator<Item = Value>>(
90 &mut self,
91 rows: impl IntoIterator<Item = R>,
92 ) -> &mut Self {
93 for row in rows {
94 self.push(row);
95 }
96 self
97 }
98
99 pub fn sort_by(mut self, keys: impl IntoIterator<Item = SortKey>) -> Self {
101 self.set_sort(keys);
102 self
103 }
104
105 pub fn set_sort(&mut self, keys: impl IntoIterator<Item = SortKey>) {
107 self.sort = keys.into_iter().collect();
108 }
109
110 pub fn group_by(mut self, group: GroupBy) -> Self {
112 self.group = Some(group);
113 self
114 }
115
116 pub fn totals(
120 mut self,
121 label: impl Into<String>,
122 aggregates: impl IntoIterator<Item = Aggregate>,
123 ) -> Self {
124 self.totals_label = label.into();
125 self.totals = aggregates.into_iter().collect();
126 self
127 }
128
129 pub fn columns(&self) -> &[Column] {
131 &self.columns
132 }
133
134 pub fn rows(&self) -> &[Vec<Value>] {
136 &self.rows
137 }
138
139 pub fn sort_keys(&self) -> &[SortKey] {
141 &self.sort
142 }
143
144 pub fn order(&self) -> Vec<usize> {
146 sorted_indices(&self.rows, &self.sort)
147 }
148
149 pub fn to_table(&self, console: &Console) -> Table {
152 let headers = headers(console, &self.columns, &self.sort);
153 let mut table = self.frame.table(&self.columns, &headers, true, true);
154 let order = self.order();
155 match &self.group {
156 None => {
157 for &row in &order {
158 table.add_row_text(self.cells(&self.rows[row]));
159 }
160 }
161 Some(group) => {
162 let group_style = style(console, "table.group");
163 for g in group.groups(&self.rows, &order) {
164 let mut label = if g.key.is_empty() {
165 Text::new("(empty)")
166 } else {
167 self.columns
168 .get(group.column())
169 .map_or_else(|| g.key.to_text(), |c| c.cell(&g.key))
170 };
171 if group.column() != 0 {
172 if let Some(column) = self.columns.get(group.column()) {
173 label = Text::new(format!("{}: ", column.header())).append_text(&label);
174 }
175 }
176 let len = label.plain().len();
177 label.stylize(group_style.clone(), 0, len);
178 let mut header = vec![Text::new(""); self.columns.len()];
179 if let Some(first) = header.first_mut() {
180 *first = label;
181 }
182 table.add_row_text(header);
183 for &row in &g.rows {
184 table.add_row_text(self.cells(&self.rows[row]));
185 }
186 if !group.aggregates().is_empty() {
187 table.add_row_text(self.summary(
188 console,
189 group.summary_label(),
190 group.aggregates(),
191 &g.aggregates,
192 ));
193 }
194 }
195 }
196 }
197 if !self.totals.is_empty() {
198 let values: Vec<Value> = self
199 .totals
200 .iter()
201 .map(|aggregate| aggregate.over(&self.rows, &order))
202 .collect();
203 table.add_row_text(self.summary(console, &self.totals_label, &self.totals, &values));
204 }
205 table
206 }
207
208 fn cells(&self, row: &[Value]) -> Vec<Text> {
209 self.columns
210 .iter()
211 .zip(row)
212 .map(|(column, value)| column.cell(value))
213 .collect()
214 }
215
216 fn summary(
220 &self,
221 console: &Console,
222 label: &str,
223 aggregates: &[Aggregate],
224 values: &[Value],
225 ) -> Vec<Text> {
226 let mut cells: Vec<Option<Text>> = vec![None; self.columns.len()];
227 for (aggregate, value) in aggregates.iter().zip(values) {
228 let Some(column) = self.columns.get(aggregate.column()) else {
229 continue;
230 };
231 let text = if aggregate.is_count() {
232 value.to_text()
233 } else {
234 column.cell(value)
235 };
236 let cell = &mut cells[aggregate.column()];
237 *cell = Some(match cell.take() {
238 Some(previous) => previous.append_text(&Text::new(", ")).append_text(&text),
239 None => text,
240 });
241 }
242 if let Some(first) = cells.first_mut() {
243 *first = match first.take() {
244 None => Some(Text::new(label)),
245 Some(value) if label.is_empty() => Some(value),
246 Some(value) => Some(Text::new(format!("{label}: ")).append_text(&value)),
247 };
248 }
249 let aggregate_style = style(console, "table.aggregate");
250 cells
251 .into_iter()
252 .map(|cell| {
253 let mut text = cell.unwrap_or_else(|| Text::new(""));
254 let len = text.plain().len();
255 text.stylize(aggregate_style.clone(), 0, len);
256 text
257 })
258 .collect()
259 }
260}
261
262pub(crate) fn normalize(row: impl IntoIterator<Item = Value>, columns: usize) -> Vec<Value> {
264 let mut row: Vec<Value> = row.into_iter().take(columns).collect();
265 row.resize(columns, Value::Null);
266 row
267}
268
269impl Renderable for TableData {
270 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
271 self.to_table(console).rich_render(console, options)
272 }
273
274 fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
275 self.to_table(console).measure(console, options)
276 }
277}
278
279impl crate::a11y::AccessibleText for TableData {
280 fn accessible_text(&self, width: usize) -> String {
281 let console = Console::builder().width(width.max(1)).build();
282 self.to_table(&console).accessible_text(width)
283 }
284}