1use std::{
2 cmp::max,
3 collections::{HashMap, HashSet},
4};
5
6use serde::Serialize;
7use serde_json::{Map, Value};
8use textwrap::{Options as WrapOptions, WordSplitter, wrap};
9
10use crate::{
11 alignment::{Alignment, DecimalLayout, align_cell},
12 constants::{MIN_PADDING, MULTILINE_FORMATS, SEPARATING_LINE},
13 format::{LineFormat, RowFormat, TableFormat, table_format},
14 options::{
15 FormatSpec, HeaderAlignment, Headers, MissingValues, RowAlignment, ShowIndex,
16 TableFormatChoice, TabulateOptions,
17 },
18 width::visible_width,
19};
20
21#[derive(thiserror::Error, Debug)]
23pub enum TabulateError {
24 #[error("unknown table format: {0}")]
26 UnknownFormat(String),
27 #[error("no rows to tabulate")]
29 EmptyData,
30 #[error(
32 "headers for a list of dicts must be a mapping or keyword such as 'keys' or 'firstrow'"
33 )]
34 InvalidHeadersForObjects,
35 #[error("header mode '{0}' is not yet supported in the Rust port")]
37 UnsupportedHeaders(String),
38 #[error("index length {found} does not match number of rows {expected}")]
40 IndexLengthMismatch {
41 expected: usize,
43 found: usize,
45 },
46 #[error("failed to serialise row: {0}")]
48 Serialization(String),
49}
50
51pub fn tabulate<Data, Row>(
53 tabular_data: Data,
54 options: TabulateOptions,
55) -> Result<String, TabulateError>
56where
57 Data: IntoIterator<Item = Row>,
58 Row: Serialize,
59{
60 Tabulator::new(options).tabulate(tabular_data)
61}
62
63pub struct Tabulator {
65 options: TabulateOptions,
66}
67
68impl Tabulator {
69 pub fn new(options: TabulateOptions) -> Self {
71 Self { options }
72 }
73
74 pub fn tabulate<Data, Row>(&self, tabular_data: Data) -> Result<String, TabulateError>
76 where
77 Data: IntoIterator<Item = Row>,
78 Row: Serialize,
79 {
80 let (format, format_name_opt) = self.resolve_format()?;
81 let mut options = self.options.clone();
82 let format_name_lower = format_name_opt.map(|name| name.to_ascii_lowercase());
83 let format_name_key = format_name_lower.as_deref();
84
85 if matches!(format_name_key, Some("pretty")) {
86 options.disable_numparse = true;
87 if options.num_align.is_none() {
88 options.num_align = Some(Alignment::Center);
89 }
90 if options.str_align.is_none() {
91 options.str_align = Some(Alignment::Center);
92 }
93 }
94 if matches!(format_name_key, Some("colon_grid")) && options.col_global_align.is_none() {
95 options.col_global_align = Some(Alignment::Left);
96 }
97
98 let mut normalized = normalize_tabular_data(tabular_data, &options)?;
99
100 let default_index = normalized.default_index.clone();
101 let default_index_header = normalized.default_index_header.clone();
102 apply_show_index(
103 &mut normalized.rows,
104 &mut normalized.headers,
105 &options.show_index,
106 options.disable_numparse,
107 default_index,
108 default_index_header,
109 )?;
110
111 apply_wrapping(&mut normalized.rows, &mut normalized.headers, &options);
112
113 let render_plan = RenderPlan::build(
114 &normalized.rows,
115 &normalized.headers,
116 format,
117 &options,
118 options.table_format_name(),
119 );
120 Ok(render_plan.render())
121 }
122
123 fn resolve_format(&self) -> Result<(&TableFormat, Option<&str>), TabulateError> {
124 match &self.options.table_format {
125 TableFormatChoice::Name(name) => table_format(name)
126 .map(|format| (format as &TableFormat, Some(name.as_str())))
127 .ok_or_else(|| TabulateError::UnknownFormat(name.clone())),
128 TableFormatChoice::Custom(format) => Ok((&**format, None)),
129 }
130 }
131}
132
133#[derive(Clone, Debug)]
134struct CellData {
135 text: String,
136 value: ParsedValue,
137}
138
139impl CellData {
140 fn new(text: String, value: ParsedValue) -> Self {
141 Self { text, value }
142 }
143
144 fn empty() -> Self {
145 Self::new(String::new(), ParsedValue::String)
146 }
147}
148
149#[derive(Clone, Debug)]
150enum ParsedValue {
151 Int(i128),
152 Float(f64),
153 Bool(bool),
154 String,
155}
156
157fn parse_value(text: &str, disable_numparse: bool) -> ParsedValue {
158 if disable_numparse {
159 return ParsedValue::String;
160 }
161 let trimmed = text.trim();
162 if trimmed.is_empty() {
163 return ParsedValue::String;
164 }
165
166 let normalized = normalize_numeric_candidate(trimmed);
167 if let Ok(value) = normalized.parse::<i128>() {
168 return ParsedValue::Int(value);
169 }
170
171 if let Ok(value) = normalized.parse::<f64>() {
172 return ParsedValue::Float(value);
173 }
174
175 match trimmed {
176 "true" | "True" => ParsedValue::Bool(true),
177 "false" | "False" => ParsedValue::Bool(false),
178 _ => ParsedValue::String,
179 }
180}
181
182#[derive(Clone, Debug)]
183enum RowKind {
184 Data(Vec<CellData>),
185 Separator,
186}
187
188struct NormalizedTable {
189 headers: Vec<String>,
190 rows: Vec<RowKind>,
191 default_index: Option<Vec<String>>,
192 default_index_header: Option<String>,
193}
194
195#[derive(Clone, Debug)]
196enum RowValue {
197 Array(Vec<Value>),
198 Object(Vec<(String, Value)>),
199 Separator,
200}
201
202fn process_item(value: Value, output: &mut Vec<Value>) {
203 output.push(value);
204}
205
206fn expand_columnar_object(row_values: &mut Vec<RowValue>) -> Option<Vec<String>> {
207 if row_values.len() != 1 {
208 return None;
209 }
210
211 let original_row = row_values.remove(0);
212 let entries = match original_row {
213 RowValue::Object(entries) => entries,
214 other => {
215 row_values.insert(0, other);
216 return None;
217 }
218 };
219
220 let mut columns = Vec::with_capacity(entries.len());
221 let mut keys = Vec::with_capacity(entries.len());
222 let mut max_len = 0usize;
223
224 for (key, value) in entries.iter() {
225 match value {
226 Value::Array(items) => {
227 max_len = max(max_len, items.len());
228 columns.push(items.clone());
229 keys.push(key.clone());
230 }
231 _ => {
232 row_values.insert(0, RowValue::Object(entries));
233 return None;
234 }
235 }
236 }
237
238 if max_len == 0 {
239 row_values.clear();
240 return Some(keys);
241 }
242
243 for column in columns.iter_mut() {
244 while column.len() < max_len {
245 column.push(Value::Null);
246 }
247 }
248
249 let mut expanded = Vec::with_capacity(max_len);
250 for idx in 0..max_len {
251 let mut row = Vec::with_capacity(columns.len());
252 for column in &columns {
253 row.push(column[idx].clone());
254 }
255 expanded.push(RowValue::Array(row));
256 }
257
258 *row_values = expanded;
259 Some(keys)
260}
261
262struct DataFrameData {
263 columns: Vec<String>,
264 rows: Vec<Vec<Value>>,
265 index: Option<Vec<Value>>,
266 index_label: Option<String>,
267}
268
269struct NumpyRecArrayData {
270 columns: Vec<String>,
271 rows: Vec<Vec<Value>>,
272}
273
274fn parse_dataframe(value: Value) -> Option<DataFrameData> {
275 let mut map = match value {
276 Value::Object(map) => map,
277 _ => return None,
278 };
279
280 match map.remove("__tabulate_dataframe__") {
281 Some(Value::Bool(true)) => {}
282 _ => return None,
283 }
284
285 let columns = match map.remove("columns")? {
286 Value::Array(values) => values.into_iter().map(value_to_plain_string).collect(),
287 _ => return None,
288 };
289
290 let rows = match map.remove("data")? {
291 Value::Array(rows) => {
292 let mut result = Vec::with_capacity(rows.len());
293 for row in rows {
294 match row {
295 Value::Array(values) => result.push(values),
296 _ => return None,
297 }
298 }
299 result
300 }
301 _ => return None,
302 };
303
304 let index = match map.remove("index") {
305 Some(Value::Array(values)) => Some(values),
306 Some(Value::Null) | None => None,
307 _ => return None,
308 };
309
310 let mut index_label = match map.remove("index_label") {
311 Some(Value::String(label)) => Some(label),
312 Some(Value::Null) => None,
313 Some(Value::Array(labels)) => {
314 let combined = labels
315 .into_iter()
316 .map(value_to_plain_string)
317 .collect::<Vec<_>>()
318 .join(" ");
319 if combined.is_empty() {
320 None
321 } else {
322 Some(combined)
323 }
324 }
325 Some(_) => return None,
326 None => None,
327 };
328
329 if index_label.is_none() {
330 if let Some(Value::String(label)) = map.remove("index_name") {
331 index_label = Some(label);
332 } else if let Some(Value::Array(labels)) = map.remove("index_names") {
333 let combined = labels
334 .into_iter()
335 .map(value_to_plain_string)
336 .collect::<Vec<_>>()
337 .join(" ");
338 if !combined.is_empty() {
339 index_label = Some(combined);
340 }
341 }
342 }
343
344 Some(DataFrameData {
345 columns,
346 rows,
347 index,
348 index_label,
349 })
350}
351
352fn parse_numpy_recarray(value: &Value) -> Option<NumpyRecArrayData> {
353 let map = match value {
354 Value::Object(map) => map,
355 _ => return None,
356 };
357 match map.get("__tabulate_numpy_recarray__") {
358 Some(Value::Bool(true)) => {}
359 _ => return None,
360 }
361 let dtype = match map.get("dtype") {
362 Some(Value::Array(items)) => items
363 .iter()
364 .filter_map(|entry| match entry {
365 Value::Array(parts) if !parts.is_empty() => parts
366 .first()
367 .and_then(|name| name.as_str())
368 .map(str::to_string),
369 _ => None,
370 })
371 .collect::<Vec<_>>(),
372 _ => return None,
373 };
374 if dtype.is_empty() {
375 return None;
376 }
377 let rows = match map.get("rows") {
378 Some(Value::Array(items)) => items
379 .iter()
380 .map(|row| match row {
381 Value::Array(values) => values.clone(),
382 _ => Vec::new(),
383 })
384 .collect::<Vec<_>>(),
385 _ => return None,
386 };
387 if rows.iter().any(|row| row.len() != dtype.len()) {
388 return None;
389 }
390 Some(NumpyRecArrayData {
391 columns: dtype,
392 rows,
393 })
394}
395
396fn value_to_plain_string(value: Value) -> String {
397 match value {
398 Value::String(s) => s,
399 Value::Number(n) => n.to_string(),
400 Value::Bool(flag) => flag.to_string(),
401 Value::Null => String::new(),
402 other => other.to_string(),
403 }
404}
405
406fn normalize_tabular_data<Data, Row>(
407 tabular_data: Data,
408 options: &TabulateOptions,
409) -> Result<NormalizedTable, TabulateError>
410where
411 Data: IntoIterator<Item = Row>,
412 Row: Serialize,
413{
414 let mut raw_rows = Vec::new();
415 for item in tabular_data {
416 process_item(
417 serde_json::to_value(item)
418 .map_err(|err| TabulateError::Serialization(err.to_string()))?,
419 &mut raw_rows,
420 );
421 }
422
423 let mut keys_order = Vec::new();
424 let mut seen_keys = HashSet::new();
425
426 let mut row_values = Vec::new();
427 let mut default_index: Option<Vec<String>> = None;
428 let mut default_index_header: Option<String> = None;
429 let mut has_dataframe = false;
430 let mut had_object_row = false;
431 for value in raw_rows {
432 if let Some(recarray) = parse_numpy_recarray(&value) {
433 for column in &recarray.columns {
434 if seen_keys.insert(column.clone()) {
435 keys_order.push(column.clone());
436 }
437 }
438 for row in recarray.rows {
439 row_values.push(RowValue::Array(row));
440 }
441 continue;
442 }
443
444 if let Some(dataframe) = parse_dataframe(value.clone()) {
445 has_dataframe = true;
446 let DataFrameData {
447 columns,
448 rows,
449 index,
450 index_label,
451 } = dataframe;
452
453 let row_count = rows.len();
454
455 if default_index.is_none()
456 && let Some(index_values) = index
457 && index_values.len() == row_count
458 {
459 let mut converted = Vec::with_capacity(index_values.len());
460 for value in index_values {
461 let cell = cell_from_value(value, options, None);
462 converted.push(cell.text);
463 }
464 default_index = Some(converted);
465 }
466 if default_index_header.is_none() {
467 default_index_header = index_label;
468 }
469
470 for value in columns.iter() {
471 if seen_keys.insert(value.clone()) {
472 keys_order.push(value.clone());
473 }
474 }
475
476 for mut row in rows.into_iter() {
477 if row.len() < columns.len() {
478 row.resize(columns.len(), Value::Null);
479 }
480 let entries = columns
481 .iter()
482 .cloned()
483 .zip(row.into_iter())
484 .collect::<Vec<_>>();
485 row_values.push(RowValue::Object(entries));
486 had_object_row = true;
487 }
488 continue;
489 }
490
491 if is_separator_value(&value) {
492 row_values.push(RowValue::Separator);
493 continue;
494 }
495 match value {
496 Value::Array(values) => row_values.push(RowValue::Array(values)),
497 Value::Object(map) => {
498 had_object_row = true;
499 row_values.push(RowValue::Object(map.into_iter().collect()));
500 }
501 other => row_values.push(RowValue::Array(vec![other])),
502 }
503 }
504
505 let columnar_keys = expand_columnar_object(&mut row_values);
506
507 if had_object_row
508 && columnar_keys.is_none()
509 && !has_dataframe
510 && let Headers::Explicit(values) = &options.headers
511 && !values.is_empty()
512 {
513 return Err(TabulateError::InvalidHeadersForObjects);
514 }
515
516 let mut headers = Vec::new();
517 if let Some(keys) = &columnar_keys {
518 for key in keys {
519 if seen_keys.insert(key.clone()) {
520 keys_order.push(key.clone());
521 }
522 }
523 }
524 let mut first_header_map: Option<Vec<(String, Value)>> = None;
525 let header_mapping = match &options.headers {
526 Headers::Mapping(entries) => Some(entries.clone()),
527 _ => None,
528 };
529 let mapping_keys: Option<Vec<String>> = header_mapping
530 .as_ref()
531 .map(|entries| entries.iter().map(|(key, _)| key.clone()).collect());
532
533 if matches!(options.headers, Headers::FirstRow)
534 && let Some(index) = row_values
535 .iter()
536 .position(|row| matches!(row, RowValue::Array(_) | RowValue::Object(_)))
537 {
538 match row_values.remove(index) {
539 RowValue::Array(values) => {
540 headers = values
541 .into_iter()
542 .map(|value| value_to_header(&value, options.preserve_whitespace))
543 .collect();
544 }
545 RowValue::Object(entries) => {
546 for (key, _) in &entries {
547 if seen_keys.insert(key.clone()) {
548 keys_order.push(key.clone());
549 }
550 }
551 first_header_map = Some(entries);
552 }
553 RowValue::Separator => {}
554 }
555 }
556
557 for row in &row_values {
558 if let RowValue::Object(entries) = row {
559 for (key, _) in entries {
560 if seen_keys.insert(key.clone()) {
561 keys_order.push(key.clone());
562 }
563 }
564 }
565 }
566
567 if let Some(mapping_keys) = &mapping_keys {
568 for key in mapping_keys {
569 if seen_keys.insert(key.clone()) {
570 keys_order.push(key.clone());
571 }
572 }
573 }
574
575 if let Some(entries) = &first_header_map {
576 let mut header_lookup = HashMap::new();
577 for (key, value) in entries {
578 header_lookup.insert(key.clone(), value.clone());
579 }
580 headers = keys_order
581 .iter()
582 .map(|key| {
583 header_lookup
584 .get(key)
585 .map(|value| value_to_header(value, options.preserve_whitespace))
586 .unwrap_or_else(|| key.clone())
587 })
588 .collect();
589 }
590
591 let mut column_count = 0usize;
592 for row in &row_values {
593 match row {
594 RowValue::Array(values) => column_count = column_count.max(values.len()),
595 RowValue::Object(_) => column_count = column_count.max(keys_order.len()),
596 RowValue::Separator => {}
597 }
598 }
599
600 if let Some(mapping) = &header_mapping {
601 column_count = column_count.max(mapping.len());
602 }
603
604 let candidate_headers = headers;
605 let headers = match &options.headers {
606 Headers::Explicit(values) => values.clone(),
607 Headers::Mapping(entries) => entries.iter().map(|(_, label)| label.clone()).collect(),
608 Headers::None => {
609 if candidate_headers.is_empty() {
610 vec![String::new(); column_count]
611 } else {
612 candidate_headers.clone()
613 }
614 }
615 Headers::FirstRow => candidate_headers.clone(),
616 Headers::Keys => {
617 if !keys_order.is_empty() {
618 keys_order.clone()
619 } else {
620 (0..column_count).map(|idx| idx.to_string()).collect()
621 }
622 }
623 };
624
625 let mut headers = headers;
626 column_count = column_count.max(headers.len());
627 if headers.len() < column_count {
628 let pad = column_count - headers.len();
629 let mut padded = Vec::with_capacity(column_count);
630 padded.extend((0..pad).map(|_| String::new()));
631 padded.extend(headers);
632 headers = padded;
633 }
634
635 let object_columns = if let Some(mapping_keys) = &mapping_keys {
636 mapping_keys.clone()
637 } else if !keys_order.is_empty() {
638 keys_order.clone()
639 } else {
640 headers.clone()
641 };
642
643 let mut rows = Vec::new();
644 for row in row_values {
645 match row {
646 RowValue::Separator => rows.push(RowKind::Separator),
647 RowValue::Array(values) => {
648 let mut cells = if let Some(keys) = &columnar_keys {
649 let mut map = HashMap::new();
650 for (idx, value) in values.into_iter().enumerate() {
651 if let Some(key) = keys.get(idx) {
652 map.insert(key.clone(), value);
653 }
654 }
655 let mut ordered = Vec::with_capacity(column_count);
656 for (col_idx, key) in object_columns.iter().enumerate() {
657 let value = map.remove(key).unwrap_or(Value::Null);
658 ordered.push(cell_from_value(value, options, Some(col_idx)));
659 }
660 ordered
661 } else {
662 values
663 .into_iter()
664 .enumerate()
665 .map(|(col_idx, value)| cell_from_value(value, options, Some(col_idx)))
666 .collect::<Vec<_>>()
667 };
668 while cells.len() < column_count {
669 cells.push(CellData::empty());
670 }
671 rows.push(RowKind::Data(cells));
672 }
673 RowValue::Object(entries) => {
674 let mut map = Map::new();
675 for (key, value) in entries {
676 map.insert(key, value);
677 }
678 let mut cells = Vec::with_capacity(column_count);
679 for (col_idx, key) in object_columns.iter().enumerate() {
680 let value = map.get(key).cloned().unwrap_or(Value::Null);
681 cells.push(cell_from_value(value, options, Some(col_idx)));
682 }
683 while cells.len() < column_count {
684 cells.push(CellData::empty());
685 }
686 rows.push(RowKind::Data(cells));
687 }
688 }
689 }
690
691 Ok(NormalizedTable {
692 headers,
693 rows,
694 default_index,
695 default_index_header,
696 })
697}
698
699fn cell_from_value(value: Value, options: &TabulateOptions, column: Option<usize>) -> CellData {
700 let text = match value {
701 Value::Null => String::new(),
702 Value::Bool(flag) => flag.to_string(),
703 Value::Number(number) => number.to_string(),
704 Value::String(s) => normalize_cell_text(&s, options.preserve_whitespace),
705 other => other.to_string(),
706 };
707 let parsed = parse_value(&text, options.is_numparse_disabled(column));
708 CellData::new(text, parsed)
709}
710
711fn value_to_header(value: &Value, preserve_whitespace: bool) -> String {
712 match value {
713 Value::String(s) => normalize_cell_text(s, preserve_whitespace),
714 Value::Null => String::new(),
715 Value::Bool(flag) => flag.to_string(),
716 Value::Number(number) => number.to_string(),
717 other => other.to_string(),
718 }
719}
720
721fn normalize_cell_text(text: &str, preserve_whitespace: bool) -> String {
722 if preserve_whitespace {
723 text.to_string()
724 } else {
725 text.trim().to_string()
726 }
727}
728
729fn is_separator_value(value: &Value) -> bool {
730 match value {
731 Value::String(text) => is_separator_str(text),
732 Value::Array(items) => items.iter().take(2).any(|item| match item {
733 Value::String(text) => is_separator_str(text),
734 _ => false,
735 }),
736 _ => false,
737 }
738}
739
740fn is_separator_str(value: &str) -> bool {
741 value.trim() == SEPARATING_LINE
742}
743
744fn apply_show_index(
745 rows: &mut [RowKind],
746 headers: &mut Vec<String>,
747 show_index: &ShowIndex,
748 disable_numparse: bool,
749 default_index: Option<Vec<String>>,
750 default_index_header: Option<String>,
751) -> Result<(), TabulateError> {
752 let data_row_count = rows
753 .iter()
754 .filter(|row| matches!(row, RowKind::Data(_)))
755 .count();
756
757 match show_index {
758 ShowIndex::Default => {
759 if let Some(values) = default_index {
760 if values.len() != data_row_count {
761 return Err(TabulateError::IndexLengthMismatch {
762 expected: data_row_count,
763 found: values.len(),
764 });
765 }
766 prepend_index_column(
767 rows,
768 headers,
769 values,
770 disable_numparse,
771 default_index_header,
772 );
773 }
774 Ok(())
775 }
776 ShowIndex::Never => Ok(()),
777 ShowIndex::Always => {
778 let values = match default_index {
779 Some(values) => values,
780 None => (0..data_row_count).map(|idx| idx.to_string()).collect(),
781 };
782 if values.len() != data_row_count {
783 return Err(TabulateError::IndexLengthMismatch {
784 expected: data_row_count,
785 found: values.len(),
786 });
787 }
788 prepend_index_column(
789 rows,
790 headers,
791 values,
792 disable_numparse,
793 default_index_header,
794 );
795 Ok(())
796 }
797 ShowIndex::Values(values) => {
798 if values.len() != data_row_count {
799 return Err(TabulateError::IndexLengthMismatch {
800 expected: data_row_count,
801 found: values.len(),
802 });
803 }
804 prepend_index_column(rows, headers, values.clone(), disable_numparse, None);
805 Ok(())
806 }
807 }
808}
809
810fn prepend_index_column(
811 rows: &mut [RowKind],
812 headers: &mut Vec<String>,
813 values: Vec<String>,
814 disable_numparse: bool,
815 header_label: Option<String>,
816) {
817 let mut iter = values.into_iter();
818 let mut inserted = false;
819 for row in rows.iter_mut() {
820 if let RowKind::Data(cells) = row
821 && let Some(value) = iter.next()
822 {
823 let parsed = parse_value(&value, disable_numparse);
824 cells.insert(0, CellData::new(value, parsed));
825 inserted = true;
826 }
827 }
828 if inserted {
829 headers.insert(0, header_label.unwrap_or_default());
830 }
831}
832
833fn apply_wrapping(rows: &mut [RowKind], headers: &mut [String], options: &TabulateOptions) {
834 let column_count = headers.len();
835 if column_count == 0 {
836 return;
837 }
838
839 let col_widths = expand_widths(&options.max_col_widths, column_count);
840 let header_widths = expand_widths(&options.max_header_col_widths, column_count);
841
842 for (idx, width_opt) in header_widths.iter().copied().enumerate() {
843 if let Some(width) = width_opt
844 && width > 0
845 && max_line_width(&headers[idx], options.enable_widechars) > width
846 {
847 let wrapped = wrap_text(
848 &headers[idx],
849 width,
850 options.break_long_words,
851 options.break_on_hyphens,
852 );
853 headers[idx] = wrapped.join("\n");
854 }
855 }
856
857 for row in rows.iter_mut() {
858 if let RowKind::Data(cells) = row {
859 for (idx, cell) in cells.iter_mut().enumerate() {
860 if let Some(width) = col_widths.get(idx).copied().flatten() {
861 if width == 0 {
862 continue;
863 }
864 if max_line_width(&cell.text, options.enable_widechars) <= width {
865 continue;
866 }
867 if !options.is_numparse_disabled(Some(idx))
868 && matches!(cell.value, ParsedValue::Int(_) | ParsedValue::Float(_))
869 {
870 continue;
871 }
872 let wrapped = wrap_text(
873 &cell.text,
874 width,
875 options.break_long_words,
876 options.break_on_hyphens,
877 );
878 if !wrapped.is_empty() {
879 cell.text = wrapped.join("\n");
880 cell.value = ParsedValue::String;
881 }
882 }
883 }
884 }
885 }
886}
887
888fn expand_widths(spec: &Option<Vec<Option<usize>>>, column_count: usize) -> Vec<Option<usize>> {
889 match spec {
890 None => vec![None; column_count],
891 Some(values) => {
892 if values.is_empty() {
893 return vec![None; column_count];
894 }
895 let mut result = Vec::with_capacity(column_count);
896 let mut iter = values.iter().cloned();
897 let mut last = iter.next().unwrap_or(None);
898 result.push(last);
899 for _idx in 1..column_count {
900 if let Some(value) = iter.next() {
901 last = value;
902 }
903 result.push(last);
904 }
905 result
906 }
907 }
908}
909
910fn wrap_text(
911 text: &str,
912 width: usize,
913 break_long_words: bool,
914 break_on_hyphens: bool,
915) -> Vec<String> {
916 if width == 0 {
917 return vec![text.to_string()];
918 }
919
920 let base = WrapOptions::new(width).break_words(break_long_words);
921 let options = if break_on_hyphens {
922 base.word_splitter(WordSplitter::HyphenSplitter)
923 } else {
924 base.word_splitter(WordSplitter::NoHyphenation)
925 };
926
927 let mut lines = Vec::new();
928 for raw_line in text.split('\n') {
929 if raw_line.is_empty() {
930 lines.push(String::new());
931 continue;
932 }
933 let wrapped = wrap(raw_line, &options);
934 if wrapped.is_empty() {
935 lines.push(raw_line.to_string());
936 } else {
937 lines.extend(wrapped.into_iter().map(|segment| segment.into_owned()));
938 }
939 }
940
941 if lines.is_empty() {
942 vec![text.to_string()]
943 } else {
944 lines
945 }
946}
947
948#[derive(Clone, Copy, Debug, PartialEq, Eq)]
949enum ColumnType {
950 Int,
951 Float,
952 Bool,
953 String,
954}
955
956impl ColumnType {
957 fn more_generic(self, other: ColumnType) -> ColumnType {
958 use ColumnType::*;
959 match (self, other) {
960 (String, _) | (_, String) => String,
961 (Float, _) | (_, Float) => Float,
962 (Int, Int) => Int,
963 (Int, Bool) | (Bool, Int) => Int,
964 (Bool, Bool) => Bool,
965 }
966 }
967}
968
969struct RenderPlan {
970 has_header: bool,
971 header_lines: Vec<String>,
972 data_lines: Vec<Vec<String>>,
973 sequence: Vec<RowMarker>,
974 line_above: Option<String>,
975 line_below_header: Option<String>,
976 line_between_rows: Option<String>,
977 line_below: Option<String>,
978 separator_fallback: Option<String>,
979}
980
981#[derive(Clone, Debug)]
982enum RowMarker {
983 Data(usize),
984 Separator,
985}
986
987impl RenderPlan {
988 fn build(
989 rows: &[RowKind],
990 headers: &[String],
991 format: &TableFormat,
992 options: &TabulateOptions,
993 format_name: Option<&str>,
994 ) -> Self {
995 let mut data_rows = Vec::new();
996 let mut sequence = Vec::new();
997 for row in rows {
998 match row {
999 RowKind::Data(cells) => {
1000 let index = data_rows.len();
1001 data_rows.push(cells.clone());
1002 sequence.push(RowMarker::Data(index));
1003 }
1004 RowKind::Separator => sequence.push(RowMarker::Separator),
1005 }
1006 }
1007
1008 let row_count = data_rows.len();
1009 let column_count = headers.len();
1010 let mut formatted_rows = vec![vec![String::new(); column_count]; row_count];
1011 let mut column_types = vec![ColumnType::Bool; column_count];
1012
1013 for col in 0..column_count {
1014 let mut current_type = ColumnType::Bool;
1015 let mut seen_non_empty = false;
1016 for row in &data_rows {
1017 let cell = &row[col];
1018 if cell.text.is_empty() && matches!(cell.value, ParsedValue::String) {
1019 continue;
1020 }
1021 seen_non_empty = true;
1022 let value_type = match cell.value {
1023 ParsedValue::Int(_) => ColumnType::Int,
1024 ParsedValue::Float(_) => ColumnType::Float,
1025 ParsedValue::Bool(_) => ColumnType::Bool,
1026 ParsedValue::String => ColumnType::String,
1027 };
1028 current_type = current_type.more_generic(value_type);
1029 if current_type == ColumnType::String {
1030 break;
1031 }
1032 }
1033 if !seen_non_empty {
1034 current_type = ColumnType::String;
1035 }
1036 column_types[col] = if options.is_numparse_disabled(Some(col)) {
1037 ColumnType::String
1038 } else {
1039 current_type
1040 };
1041 }
1042
1043 let float_formats = resolve_formats(&options.float_format, column_count, "g");
1044 let int_formats = resolve_formats(&options.int_format, column_count, "");
1045 let missing_values = resolve_missing_values(&options.missing_values, column_count);
1046
1047 for (row_idx, row) in data_rows.iter().enumerate() {
1048 for col_idx in 0..column_count {
1049 formatted_rows[row_idx][col_idx] = format_cell(
1050 &row[col_idx],
1051 column_types[col_idx],
1052 &float_formats[col_idx],
1053 &int_formats[col_idx],
1054 &missing_values[col_idx],
1055 );
1056 }
1057 }
1058
1059 let formatted_headers: Vec<String> = headers.to_vec();
1060 let format_name_lower = format_name.map(|name| name.to_ascii_lowercase());
1061 let format_name_key = format_name_lower.as_deref().unwrap_or("");
1062 let supports_multiline = format_name_lower
1063 .as_deref()
1064 .map(|name| MULTILINE_FORMATS.contains(&name))
1065 .unwrap_or(false);
1066
1067 let mut content_widths = vec![0usize; column_count];
1068 let mut decimal_widths: Vec<Option<DecimalLayout>> = vec![None; column_count];
1069
1070 let enable_widechars = options.enable_widechars;
1071
1072 for col_idx in 0..column_count {
1073 for row in &formatted_rows {
1074 content_widths[col_idx] = max(
1075 content_widths[col_idx],
1076 cell_width(&row[col_idx], supports_multiline, enable_widechars),
1077 );
1078 }
1079 content_widths[col_idx] = max(
1080 content_widths[col_idx],
1081 cell_width(
1082 &formatted_headers[col_idx],
1083 supports_multiline,
1084 enable_widechars,
1085 ),
1086 );
1087 }
1088
1089 if format_name_key == "moinmoin" {
1090 for (idx, width) in content_widths.iter_mut().enumerate() {
1091 match column_types.get(idx).copied().unwrap_or(ColumnType::String) {
1092 ColumnType::String => *width += 1,
1093 _ => *width += 2,
1094 }
1095 }
1096 } else if format_name_key == "colon_grid" {
1097 for (idx, width) in content_widths.iter_mut().enumerate() {
1098 if matches!(column_types.get(idx), Some(ColumnType::String)) {
1099 *width += 1;
1100 }
1101 }
1102 }
1103
1104 #[cfg(test)]
1105 if format_name_key == "moinmoin" {
1106 dbg!(&content_widths);
1107 dbg!(&formatted_headers);
1108 dbg!(&formatted_rows);
1109 }
1110
1111 let has_header = headers.iter().any(|h| !h.is_empty());
1112 let min_padding = if format_name_key == "pretty" {
1113 0
1114 } else {
1115 MIN_PADDING
1116 };
1117
1118 if has_header {
1119 let format_requires_padding = format_name_key.starts_with("latex");
1120 let apply_min_padding = matches!(format.header_row, RowFormat::Static(_))
1121 || format.padding == 0
1122 || format_requires_padding;
1123 for col_idx in 0..column_count {
1124 let header_width = cell_width(
1125 &formatted_headers[col_idx],
1126 supports_multiline,
1127 enable_widechars,
1128 );
1129 if apply_min_padding {
1130 content_widths[col_idx] =
1131 max(content_widths[col_idx], header_width + min_padding);
1132 } else {
1133 content_widths[col_idx] = max(content_widths[col_idx], header_width);
1134 }
1135 }
1136 }
1137
1138 let column_aligns = initialise_alignments(column_count, &column_types, options);
1139 let header_aligns = initialise_header_alignments(column_count, &column_aligns, options);
1140 for col_idx in 0..column_count {
1141 if column_aligns[col_idx] == Alignment::Decimal {
1142 let mut max_integer_width_value = 0usize;
1143 let mut max_fraction_width_value = 0usize;
1144 for row in &formatted_rows {
1145 let (integer_width, fraction_width) =
1146 max_integer_fraction_width(&row[col_idx], '.', enable_widechars);
1147 max_integer_width_value = max(max_integer_width_value, integer_width);
1148 max_fraction_width_value = max(max_fraction_width_value, fraction_width);
1149 }
1150 let (header_integer, header_fraction) =
1151 max_integer_fraction_width(&formatted_headers[col_idx], '.', enable_widechars);
1152 max_integer_width_value = max(max_integer_width_value, header_integer);
1153 max_fraction_width_value = max(max_fraction_width_value, header_fraction);
1154 decimal_widths[col_idx] = Some(DecimalLayout {
1155 integer: max_integer_width_value,
1156 fraction: max_fraction_width_value,
1157 });
1158 let decimal_total = if max_fraction_width_value > 0 {
1159 max_integer_width_value + 1 + max_fraction_width_value
1160 } else {
1161 max_integer_width_value
1162 };
1163 content_widths[col_idx] = max(content_widths[col_idx], decimal_total);
1164 }
1165 }
1166 let padding = format.padding;
1167 let column_widths: Vec<usize> = content_widths.iter().map(|w| w + padding * 2).collect();
1168
1169 #[cfg(test)]
1170 if format_name_key == "moinmoin" {
1171 println!("moin widths {:?}", column_widths);
1172 println!("headers {:?}", formatted_headers);
1173 println!("rows {:?}", formatted_rows);
1174 }
1175
1176 let mut aligned_headers = Vec::with_capacity(column_count);
1177 for col_idx in 0..column_count {
1178 let alignment = match header_aligns[col_idx] {
1179 Alignment::Decimal => Alignment::Right,
1180 other => other,
1181 };
1182 let header_decimal_layout = match header_aligns[col_idx] {
1183 Alignment::Decimal => None,
1184 _ => decimal_widths[col_idx],
1185 };
1186 let aligned = align_cell(
1187 &formatted_headers[col_idx],
1188 content_widths[col_idx],
1189 alignment,
1190 '.',
1191 header_decimal_layout,
1192 supports_multiline,
1193 true,
1194 enable_widechars,
1195 );
1196 let padded = apply_padding(&aligned, padding, supports_multiline);
1197 aligned_headers.push(padded);
1198 }
1199
1200 #[cfg(test)]
1201 if format_name_key == "moinmoin" {
1202 dbg!(&aligned_headers);
1203 }
1204
1205 let mut aligned_rows = vec![Vec::with_capacity(column_count); row_count];
1206 for col_idx in 0..column_count {
1207 for row_idx in 0..row_count {
1208 let cell_text = &formatted_rows[row_idx][col_idx];
1209 let enforce_left_alignment = supports_multiline
1210 || column_aligns[col_idx] != Alignment::Left
1211 || !cell_text.contains('\n');
1212 let aligned = align_cell(
1213 cell_text,
1214 content_widths[col_idx],
1215 column_aligns[col_idx],
1216 '.',
1217 decimal_widths[col_idx],
1218 supports_multiline,
1219 enforce_left_alignment,
1220 enable_widechars,
1221 );
1222 aligned_rows[row_idx].push(apply_padding(&aligned, padding, supports_multiline));
1223 }
1224 }
1225
1226 let header_lines = if has_header {
1227 render_multiline_row(
1228 &format.header_row,
1229 &aligned_headers,
1230 &column_widths,
1231 &header_aligns,
1232 RowAlignment::Top,
1233 format_name_key,
1234 supports_multiline,
1235 )
1236 } else {
1237 Vec::new()
1238 };
1239
1240 let row_aligns = initialise_row_alignments(row_count, options);
1241
1242 let mut data_lines = Vec::with_capacity(row_count);
1243 for (row_idx, row) in aligned_rows.iter().enumerate() {
1244 let alignment = row_aligns
1245 .get(row_idx)
1246 .copied()
1247 .unwrap_or(RowAlignment::Top);
1248 data_lines.push(render_multiline_row(
1249 &format.data_row,
1250 row,
1251 &column_widths,
1252 &column_aligns,
1253 alignment,
1254 format_name_key,
1255 supports_multiline,
1256 ));
1257 }
1258
1259 let hide_line_above = has_header && format.hides("lineabove");
1260 let hide_line_below_header = has_header && format.hides("linebelowheader");
1261 let hide_line_between_rows = has_header && format.hides("linebetweenrows");
1262 let hide_line_below = has_header && format.hides("linebelow");
1263
1264 let line_above = render_line(
1265 &format.line_above,
1266 &column_widths,
1267 &column_aligns,
1268 hide_line_above,
1269 );
1270 let line_below_header = render_line(
1271 &format.line_below_header,
1272 &column_widths,
1273 &column_aligns,
1274 hide_line_below_header,
1275 );
1276 let line_between_rows = render_line(
1277 &format.line_between_rows,
1278 &column_widths,
1279 &column_aligns,
1280 hide_line_between_rows,
1281 );
1282 let line_below = render_line(
1283 &format.line_below,
1284 &column_widths,
1285 &column_aligns,
1286 hide_line_below,
1287 );
1288
1289 let separator_fallback = if line_between_rows.is_some() {
1290 None
1291 } else {
1292 line_below_header
1293 .clone()
1294 .or_else(|| line_below.clone())
1295 .or_else(|| line_above.clone())
1296 };
1297
1298 Self {
1299 has_header,
1300 header_lines,
1301 data_lines,
1302 sequence,
1303 line_above,
1304 line_below_header,
1305 line_between_rows,
1306 line_below,
1307 separator_fallback,
1308 }
1309 }
1310
1311 fn render(self) -> String {
1312 let RenderPlan {
1313 has_header,
1314 header_lines,
1315 data_lines,
1316 sequence,
1317 line_above,
1318 line_below_header,
1319 line_between_rows,
1320 line_below,
1321 separator_fallback,
1322 } = self;
1323
1324 let mut output = Vec::new();
1325 if let Some(line) = line_above {
1326 output.push(line);
1327 }
1328
1329 if has_header {
1330 output.extend(header_lines);
1331 if let Some(line) = line_below_header {
1332 output.push(line);
1333 }
1334 }
1335
1336 if !sequence.is_empty() {
1337 for (idx, marker) in sequence.iter().enumerate() {
1338 match marker {
1339 RowMarker::Data(index) => {
1340 if let Some(lines) = data_lines.get(*index) {
1341 for line in lines {
1342 output.push(line.clone());
1343 }
1344 }
1345 if let Some(next_marker) = sequence.get(idx + 1)
1346 && matches!(next_marker, RowMarker::Data(_))
1347 && let Some(line) = &line_between_rows
1348 {
1349 output.push(line.clone());
1350 }
1351 }
1352 RowMarker::Separator => {
1353 if let Some(line) = &line_between_rows {
1354 output.push(line.clone());
1355 } else if let Some(line) = &separator_fallback {
1356 if !line.is_empty() {
1357 output.push(line.clone());
1358 } else {
1359 output.push(String::new());
1360 }
1361 } else {
1362 output.push(String::new());
1363 }
1364 }
1365 }
1366 }
1367 }
1368
1369 if let Some(line) = line_below {
1370 output.push(line);
1371 }
1372
1373 output.join("\n")
1374 }
1375}
1376
1377fn resolve_formats(spec: &FormatSpec, column_count: usize, default: &str) -> Vec<String> {
1378 match spec {
1379 FormatSpec::Default => vec![default.to_string(); column_count],
1380 FormatSpec::Fixed(fmt) => vec![fmt.clone(); column_count],
1381 FormatSpec::PerColumn(list) => {
1382 let mut result = Vec::with_capacity(column_count);
1383 for idx in 0..column_count {
1384 result.push(
1385 list.get(idx)
1386 .cloned()
1387 .unwrap_or_else(|| default.to_string()),
1388 );
1389 }
1390 result
1391 }
1392 }
1393}
1394
1395fn resolve_missing_values(spec: &MissingValues, column_count: usize) -> Vec<String> {
1396 match spec {
1397 MissingValues::Single(value) => vec![value.clone(); column_count],
1398 MissingValues::PerColumn(values) => {
1399 if values.is_empty() {
1400 return vec![String::new(); column_count];
1401 }
1402 let mut result = Vec::with_capacity(column_count);
1403 for idx in 0..column_count {
1404 result.push(
1405 values
1406 .get(idx)
1407 .cloned()
1408 .unwrap_or_else(|| values.last().cloned().unwrap()),
1409 );
1410 }
1411 result
1412 }
1413 }
1414}
1415
1416fn format_cell(
1417 cell: &CellData,
1418 column_type: ColumnType,
1419 float_format: &str,
1420 int_format: &str,
1421 missing_value: &str,
1422) -> String {
1423 if cell.text.is_empty() {
1424 return missing_value.to_string();
1425 }
1426
1427 match column_type {
1428 ColumnType::Int => match cell.value {
1429 ParsedValue::Int(value) => format_int_value(value, int_format),
1430 _ => cell.text.clone(),
1431 },
1432 ColumnType::Float => match cell.value {
1433 ParsedValue::Float(value) => format_float_value(value, float_format),
1434 ParsedValue::Int(value) => format_float_value(value as f64, float_format),
1435 _ => cell.text.clone(),
1436 },
1437 ColumnType::Bool => match cell.value {
1438 ParsedValue::Bool(flag) => format_bool_value(flag),
1439 _ => cell.text.clone(),
1440 },
1441 ColumnType::String => cell.text.clone(),
1442 }
1443}
1444
1445fn format_bool_value(flag: bool) -> String {
1446 if flag {
1447 "True".to_string()
1448 } else {
1449 "False".to_string()
1450 }
1451}
1452
1453fn format_int_value(value: i128, spec: &str) -> String {
1454 let mut body = spec.trim().to_string();
1455 if body.is_empty() {
1456 return value.to_string();
1457 }
1458
1459 let thousands = if body.contains(',') {
1460 body = body.replace(',', "");
1461 true
1462 } else {
1463 false
1464 };
1465
1466 let mut base = 10;
1467 let mut uppercase = false;
1468 if let Some(ch) = body.chars().last() {
1469 match ch {
1470 'x' => {
1471 base = 16;
1472 body.pop();
1473 }
1474 'X' => {
1475 base = 16;
1476 uppercase = true;
1477 body.pop();
1478 }
1479 'b' | 'B' => {
1480 base = 2;
1481 uppercase = ch.is_uppercase();
1482 body.pop();
1483 }
1484 'o' | 'O' => {
1485 base = 8;
1486 uppercase = ch.is_uppercase();
1487 body.pop();
1488 }
1489 _ => {}
1490 }
1491 }
1492
1493 let mut formatted = match base {
1494 16 => {
1495 if uppercase {
1496 format!("{:X}", value)
1497 } else {
1498 format!("{:x}", value)
1499 }
1500 }
1501 8 => {
1502 let text = format!("{:o}", value);
1503 if uppercase { text.to_uppercase() } else { text }
1504 }
1505 2 => {
1506 let sign = if value < 0 { "-" } else { "" };
1507 let digits = format!("{:b}", value.abs());
1508 format!("{sign}{digits}")
1509 }
1510 _ => value.to_string(),
1511 };
1512
1513 if thousands && base == 10 {
1514 formatted = apply_thousands_to_integer_string(&formatted);
1515 }
1516
1517 formatted
1518}
1519
1520fn format_float_value(value: f64, spec: &str) -> String {
1521 let fmt = parse_float_format(spec);
1522
1523 if value.is_nan() {
1524 return if fmt.uppercase {
1525 "NAN".to_string()
1526 } else {
1527 "nan".to_string()
1528 };
1529 }
1530 if value.is_infinite() {
1531 let inf = if value.is_sign_negative() {
1532 "-inf"
1533 } else {
1534 "inf"
1535 };
1536 return if fmt.uppercase {
1537 inf.to_uppercase()
1538 } else {
1539 inf.to_string()
1540 };
1541 }
1542
1543 let mut rendered = match fmt.style {
1544 FloatStyle::Fixed => {
1545 let precision = fmt.precision.unwrap_or(6);
1546 if fmt.percent {
1547 let mut text = format!("{:.*}", precision, value * 100.0);
1548 text = trim_trailing_zeros(text);
1549 if text.is_empty() {
1550 text = "0".to_string();
1551 }
1552 text.push('%');
1553 if fmt.thousands {
1554 apply_thousands_to_number(&text)
1555 } else {
1556 text
1557 }
1558 } else {
1559 format!("{:.*}", precision, value)
1560 }
1561 }
1562 FloatStyle::Exponent => {
1563 let precision = fmt.precision.unwrap_or(6);
1564 let formatted = if fmt.uppercase {
1565 format!("{:.*E}", precision, value)
1566 } else {
1567 format!("{:.*e}", precision, value)
1568 };
1569 if let Some(pos) = formatted.find('e').or_else(|| formatted.find('E')) {
1570 let exp_char = formatted.as_bytes()[pos] as char;
1571 let mantissa = &formatted[..pos];
1572 let exponent = normalize_exponent(&formatted[pos + 1..]);
1573 format!("{mantissa}{exp_char}{exponent}")
1574 } else {
1575 formatted
1576 }
1577 }
1578 FloatStyle::General => format_float_general(value, fmt.precision, fmt.uppercase),
1579 };
1580
1581 if (fmt.style != FloatStyle::Fixed || !fmt.percent) && fmt.thousands {
1582 rendered = apply_thousands_to_number(&rendered);
1583 }
1584
1585 rendered
1586}
1587
1588#[derive(Clone, Copy, PartialEq, Eq)]
1589enum FloatStyle {
1590 General,
1591 Fixed,
1592 Exponent,
1593}
1594
1595#[derive(Clone, Copy)]
1596struct ParsedFloatFormat {
1597 style: FloatStyle,
1598 precision: Option<usize>,
1599 thousands: bool,
1600 uppercase: bool,
1601 percent: bool,
1602}
1603
1604impl Default for ParsedFloatFormat {
1605 fn default() -> Self {
1606 Self {
1607 style: FloatStyle::General,
1608 precision: None,
1609 thousands: false,
1610 uppercase: false,
1611 percent: false,
1612 }
1613 }
1614}
1615
1616fn parse_float_format(spec: &str) -> ParsedFloatFormat {
1617 let mut fmt = ParsedFloatFormat::default();
1618 if spec.is_empty() {
1619 return fmt;
1620 }
1621
1622 let mut body = spec.trim().to_string();
1623 if body.contains(',') {
1624 body = body.replace(',', "");
1625 fmt.thousands = true;
1626 }
1627
1628 if let Some(ch) = body.chars().last() {
1629 match ch {
1630 'f' | 'F' => {
1631 fmt.style = FloatStyle::Fixed;
1632 fmt.uppercase = ch.is_uppercase();
1633 body.pop();
1634 }
1635 'e' | 'E' => {
1636 fmt.style = FloatStyle::Exponent;
1637 fmt.uppercase = ch.is_uppercase();
1638 body.pop();
1639 }
1640 'g' | 'G' => {
1641 fmt.style = FloatStyle::General;
1642 fmt.uppercase = ch.is_uppercase();
1643 body.pop();
1644 }
1645 '%' => {
1646 fmt.style = FloatStyle::Fixed;
1647 fmt.percent = true;
1648 body.pop();
1649 }
1650 _ => {}
1651 }
1652 }
1653
1654 if let Some(dot) = body.find('.') {
1655 let precision_part = &body[dot + 1..];
1656 if !precision_part.is_empty() {
1657 fmt.precision = precision_part.parse::<usize>().ok();
1658 }
1659 }
1660
1661 fmt
1662}
1663
1664fn format_float_general(value: f64, precision: Option<usize>, uppercase: bool) -> String {
1665 let precision = precision.unwrap_or(6).max(1);
1666 if value == 0.0 {
1667 return "0".to_string();
1668 }
1669
1670 let abs = value.abs();
1671 let exponent = if abs == 0.0 {
1672 0
1673 } else {
1674 abs.log10().floor() as i32
1675 };
1676 let use_exponent = exponent < -4 || exponent >= precision as i32;
1677
1678 if use_exponent {
1679 let formatted = if uppercase {
1680 format!("{:.*E}", precision - 1, value)
1681 } else {
1682 format!("{:.*e}", precision - 1, value)
1683 };
1684 if let Some(pos) = formatted.find('e').or_else(|| formatted.find('E')) {
1685 let exp_char = formatted.as_bytes()[pos] as char;
1686 let mantissa = trim_trailing_zeros(formatted[..pos].to_string());
1687 let exponent = normalize_exponent(&formatted[pos + 1..]);
1688 format!("{mantissa}{exp_char}{exponent}")
1689 } else {
1690 formatted
1691 }
1692 } else {
1693 let decimals = (precision as i32 - exponent - 1).max(0) as usize;
1694 let text = format!("{:.*}", decimals, value);
1695 trim_trailing_zeros(text)
1696 }
1697}
1698
1699fn trim_trailing_zeros(mut value: String) -> String {
1700 if let Some(dot) = value.find('.') {
1701 let mut trim_len = value.len();
1702 while trim_len > dot && value.as_bytes()[trim_len - 1] == b'0' {
1703 trim_len -= 1;
1704 }
1705 if trim_len > dot && value.as_bytes()[trim_len - 1] == b'.' {
1706 trim_len -= 1;
1707 }
1708 value.truncate(trim_len);
1709 }
1710 value
1711}
1712
1713fn apply_thousands_to_integer_string(value: &str) -> String {
1714 let (sign, digits) = if let Some(rest) = value.strip_prefix('-') {
1715 ("-", rest)
1716 } else if let Some(rest) = value.strip_prefix('+') {
1717 ("+", rest)
1718 } else {
1719 ("", value)
1720 };
1721 let formatted = apply_thousands_to_integer(digits);
1722 format!("{sign}{formatted}")
1723}
1724
1725fn apply_thousands_to_integer(digits: &str) -> String {
1726 if digits.len() <= 3 {
1727 return digits.to_string();
1728 }
1729
1730 let mut result = String::new();
1731 let chars: Vec<char> = digits.chars().collect();
1732 let mut index = chars.len() % 3;
1733 if index == 0 {
1734 index = 3;
1735 }
1736 result.extend(&chars[..index]);
1737 while index < chars.len() {
1738 result.push(',');
1739 result.extend(&chars[index..index + 3]);
1740 index += 3;
1741 }
1742 result
1743}
1744
1745fn apply_thousands_to_number(text: &str) -> String {
1746 if text.contains('e') || text.contains('E') {
1747 return text.to_string();
1748 }
1749 let (body, suffix) = if let Some(stripped) = text.strip_suffix('%') {
1750 (stripped, "%")
1751 } else {
1752 (text, "")
1753 };
1754 let (sign, rest) = if let Some(stripped) = body.strip_prefix('-') {
1755 ("-", stripped)
1756 } else if let Some(stripped) = body.strip_prefix('+') {
1757 ("+", stripped)
1758 } else {
1759 ("", body)
1760 };
1761 let mut parts = rest.splitn(2, '.');
1762 let int_part = parts.next().unwrap_or("");
1763 let frac_part = parts.next();
1764 let formatted_int = apply_thousands_to_integer(int_part);
1765 let mut result = String::new();
1766 result.push_str(sign);
1767 result.push_str(&formatted_int);
1768 if let Some(frac) = frac_part
1769 && !frac.is_empty()
1770 {
1771 result.push('.');
1772 result.push_str(frac);
1773 }
1774 result.push_str(suffix);
1775 result
1776}
1777
1778fn normalize_numeric_candidate(input: &str) -> String {
1779 input
1780 .chars()
1781 .filter(|ch| *ch != ',' && *ch != '_' && *ch != ' ')
1782 .collect()
1783}
1784
1785fn normalize_exponent(exponent: &str) -> String {
1786 let (sign, digits) = if exponent.starts_with(['+', '-']) {
1787 exponent.split_at(1)
1788 } else {
1789 ("+", exponent)
1790 };
1791 let trimmed = digits.trim_start_matches('0');
1792 let core = if trimmed.is_empty() { "0" } else { trimmed };
1793 let padded = if core.len() < 2 {
1794 format!("{:0>2}", core)
1795 } else {
1796 core.to_string()
1797 };
1798 format!("{sign}{padded}")
1799}
1800
1801fn initialise_alignments(
1802 column_count: usize,
1803 column_types: &[ColumnType],
1804 options: &TabulateOptions,
1805) -> Vec<Alignment> {
1806 let mut aligns = vec![Alignment::Left; column_count];
1807 let num_align = options.num_align.unwrap_or(Alignment::Decimal);
1808 let str_align = options.str_align.unwrap_or(Alignment::Left);
1809
1810 for (idx, col_type) in column_types.iter().enumerate() {
1811 aligns[idx] = match col_type {
1812 ColumnType::Int | ColumnType::Float => num_align,
1813 _ => str_align,
1814 };
1815 }
1816
1817 if let Some(global) = options.col_global_align {
1818 aligns.iter_mut().for_each(|align| *align = global);
1819 }
1820
1821 for (idx, custom) in options.col_align.iter().enumerate() {
1822 if let Some(alignment) = custom
1823 && idx < aligns.len()
1824 {
1825 aligns[idx] = *alignment;
1826 }
1827 }
1828
1829 aligns
1830}
1831
1832fn initialise_header_alignments(
1833 column_count: usize,
1834 column_aligns: &[Alignment],
1835 options: &TabulateOptions,
1836) -> Vec<Alignment> {
1837 let mut aligns = if let Some(global) = options.headers_global_align {
1838 vec![global; column_count]
1839 } else {
1840 column_aligns.to_vec()
1841 };
1842
1843 for (idx, custom) in options.headers_align.iter().enumerate() {
1844 if idx >= aligns.len() {
1845 break;
1846 }
1847 if let Some(spec) = custom {
1848 match spec {
1849 HeaderAlignment::Align(alignment) => {
1850 aligns[idx] = *alignment;
1851 }
1852 HeaderAlignment::SameAsColumn => {
1853 if let Some(column_alignment) = column_aligns.get(idx) {
1854 aligns[idx] = *column_alignment;
1855 }
1856 }
1857 }
1858 }
1859 }
1860
1861 aligns
1862}
1863
1864fn initialise_row_alignments(row_count: usize, options: &TabulateOptions) -> Vec<RowAlignment> {
1865 let default = options.row_global_align.unwrap_or(RowAlignment::Top);
1866 let mut aligns = vec![default; row_count];
1867 for (idx, custom) in options.row_align.iter().enumerate() {
1868 if idx >= aligns.len() {
1869 break;
1870 }
1871 if let Some(alignment) = custom {
1872 aligns[idx] = *alignment;
1873 }
1874 }
1875 aligns
1876}
1877
1878fn apply_padding(text: &str, padding: usize, supports_multiline: bool) -> String {
1879 if padding == 0 {
1880 return text.to_string();
1881 }
1882 let pad = " ".repeat(padding);
1883 if supports_multiline {
1884 split_cell_lines(text)
1885 .into_iter()
1886 .map(|line| format!("{pad}{line}{pad}"))
1887 .collect::<Vec<_>>()
1888 .join("\n")
1889 } else {
1890 format!("{pad}{text}{pad}")
1891 }
1892}
1893
1894fn split_cell_lines(text: &str) -> Vec<&str> {
1895 text.split('\n').collect()
1896}
1897
1898fn cell_width(text: &str, supports_multiline: bool, enable_widechars: bool) -> usize {
1899 if supports_multiline {
1900 max_line_width(text, enable_widechars)
1901 } else {
1902 full_text_width(text, enable_widechars)
1903 }
1904}
1905
1906fn max_line_width(text: &str, enable_widechars: bool) -> usize {
1907 split_cell_lines(text)
1908 .into_iter()
1909 .map(|line| visible_width(line, enable_widechars))
1910 .max()
1911 .unwrap_or(0)
1912}
1913
1914fn full_text_width(text: &str, enable_widechars: bool) -> usize {
1915 let lines = split_cell_lines(text);
1916 if lines.is_empty() {
1917 return 0;
1918 }
1919 let mut total = 0usize;
1920 let last_index = lines.len().saturating_sub(1);
1921 for (idx, line) in lines.iter().enumerate() {
1922 total += visible_width(line, enable_widechars);
1923 if idx < last_index {
1924 total += 1; }
1926 }
1927 total
1928}
1929
1930fn max_integer_fraction_width(
1931 text: &str,
1932 decimal_marker: char,
1933 enable_widechars: bool,
1934) -> (usize, usize) {
1935 split_cell_lines(text)
1936 .into_iter()
1937 .map(|line| integer_fraction_width_line(line, decimal_marker, enable_widechars))
1938 .fold((0, 0), |(max_int, max_frac), (int_w, frac_w)| {
1939 (max(max_int, int_w), max(max_frac, frac_w))
1940 })
1941}
1942
1943fn integer_fraction_width_line(
1944 value: &str,
1945 decimal_marker: char,
1946 enable_widechars: bool,
1947) -> (usize, usize) {
1948 match value.find(decimal_marker) {
1949 Some(pos) => {
1950 let integer_width = visible_width(&value[..pos], enable_widechars);
1951 let fraction_width =
1952 visible_width(&value[pos + decimal_marker.len_utf8()..], enable_widechars);
1953 (integer_width, fraction_width)
1954 }
1955 None => (visible_width(value, enable_widechars), 0),
1956 }
1957}
1958
1959fn render_line(
1960 spec: &LineFormat,
1961 col_widths: &[usize],
1962 col_aligns: &[Alignment],
1963 suppressed: bool,
1964) -> Option<String> {
1965 if suppressed {
1966 return None;
1967 }
1968 match spec {
1969 LineFormat::None => None,
1970 LineFormat::Static(line) => {
1971 let mut out = String::new();
1972 out.push_str(line.begin.as_ref());
1973 for (idx, width) in col_widths.iter().enumerate() {
1974 if idx > 0 {
1975 out.push_str(line.separator.as_ref());
1976 }
1977 out.push_str(&line.fill.as_ref().repeat(*width));
1978 }
1979 out.push_str(line.end.as_ref());
1980 if out.is_empty() { None } else { Some(out) }
1981 }
1982 LineFormat::Text(text) => {
1983 let owned = text.clone().into_owned();
1984 if owned.is_empty() { None } else { Some(owned) }
1985 }
1986 LineFormat::Dynamic(func) => {
1987 let rendered = func(col_widths, col_aligns);
1988 if rendered.is_empty() {
1989 None
1990 } else {
1991 Some(rendered)
1992 }
1993 }
1994 }
1995}
1996
1997fn render_multiline_row(
1998 spec: &RowFormat,
1999 cells: &[String],
2000 col_widths: &[usize],
2001 col_aligns: &[Alignment],
2002 row_alignment: RowAlignment,
2003 format_name: &str,
2004 supports_multiline: bool,
2005) -> Vec<String> {
2006 match spec {
2007 RowFormat::None => Vec::new(),
2008 RowFormat::Dynamic(func) => vec![func(cells, col_widths, col_aligns)],
2009 RowFormat::Static(_) => {
2010 let split_cells: Vec<Vec<String>> = cells
2011 .iter()
2012 .map(|cell| {
2013 split_cell_lines(cell)
2014 .into_iter()
2015 .map(|line| line.to_string())
2016 .collect()
2017 })
2018 .collect();
2019 let line_count = split_cells
2020 .iter()
2021 .map(|lines| lines.len())
2022 .max()
2023 .unwrap_or(0);
2024 if line_count <= 1 || !supports_multiline {
2025 return vec![render_row(spec, cells, col_widths, col_aligns)];
2026 }
2027 let mut per_column_lines: Vec<Vec<String>> = Vec::with_capacity(cells.len());
2028 for (col_idx, lines) in split_cells.iter().enumerate() {
2029 let width = *col_widths.get(col_idx).unwrap_or(&0);
2030 per_column_lines.push(pad_cell_lines(
2031 lines,
2032 width,
2033 line_count,
2034 row_alignment,
2035 format_name,
2036 ));
2037 }
2038 let mut output = Vec::with_capacity(line_count);
2039 for line_idx in 0..line_count {
2040 let line_cells = per_column_lines
2041 .iter()
2042 .map(|column_lines| column_lines[line_idx].clone())
2043 .collect::<Vec<_>>();
2044 output.push(render_row(spec, &line_cells, col_widths, col_aligns));
2045 }
2046 output
2047 }
2048 }
2049}
2050
2051fn pad_cell_lines(
2052 lines: &[String],
2053 width: usize,
2054 total_lines: usize,
2055 alignment: RowAlignment,
2056 format_name: &str,
2057) -> Vec<String> {
2058 let mut result = Vec::with_capacity(total_lines);
2059 let blank = " ".repeat(width);
2060 let line_count = lines.len();
2061 if line_count >= total_lines {
2062 result.extend_from_slice(&lines[..total_lines]);
2063 return result;
2064 }
2065 let padding = total_lines - line_count;
2066 match alignment {
2067 RowAlignment::Top => {
2068 result.extend_from_slice(lines);
2069 result.extend((0..padding).map(|_| blank.clone()));
2070 }
2071 RowAlignment::Bottom => {
2072 let treat_as_top = lines.len() <= 1 && format_name.starts_with("pipe");
2073 if treat_as_top {
2074 result.extend_from_slice(lines);
2075 result.extend((0..padding).map(|_| blank.clone()));
2076 } else {
2077 result.extend((0..padding).map(|_| blank.clone()));
2078 result.extend_from_slice(lines);
2079 }
2080 }
2081 RowAlignment::Center => {
2082 let top = padding / 2;
2083 let bottom = padding - top;
2084 result.extend((0..top).map(|_| blank.clone()));
2085 result.extend_from_slice(lines);
2086 result.extend((0..bottom).map(|_| blank.clone()));
2087 }
2088 }
2089 result
2090}
2091
2092fn render_row(
2093 spec: &RowFormat,
2094 cells: &[String],
2095 col_widths: &[usize],
2096 col_aligns: &[Alignment],
2097) -> String {
2098 match spec {
2099 RowFormat::None => String::new(),
2100 RowFormat::Static(row) => {
2101 let mut out = String::new();
2102 out.push_str(row.begin.as_ref());
2103 for (idx, cell) in cells.iter().enumerate() {
2104 if idx > 0 {
2105 out.push_str(row.separator.as_ref());
2106 }
2107 out.push_str(cell);
2108 }
2109 out.push_str(row.end.as_ref());
2110 out.trim_end().to_string()
2111 }
2112 RowFormat::Dynamic(func) => func(cells, col_widths, col_aligns),
2113 }
2114}