use crate::{
align_line_left, align_line_right, n_lines, width, Column, ColumnFormat, ColumnFormatShorthand,
FontStyle, FormatError, Table, VerticalAlign,
};
const DEFAULT_TABLE_HEIGHT: usize = 30;
#[derive(Debug)]
pub struct TableFormat {
pub column_formats: Option<Vec<ColumnFormatShorthand>>,
pub column_delimiter: String,
pub header_separator_delimiter: String,
pub header_separator_char: char,
pub include_header_row: bool,
pub include_header_separator_row: bool,
pub include_summary_row: bool,
pub include_summary_separator_row: bool,
pub render_height: Option<usize>,
pub max_render_width: Option<usize>,
pub indent: usize,
pub border_font_style: Option<FontStyle>,
pub label_font_style: Option<FontStyle>,
pub label_justify_left: bool,
}
impl Default for TableFormat {
fn default() -> TableFormat {
TableFormat {
column_formats: None,
column_delimiter: " │ ".to_string(),
header_separator_delimiter: "──┼──".to_string(),
header_separator_char: '─',
include_header_row: true,
include_header_separator_row: true,
include_summary_row: false,
include_summary_separator_row: false,
render_height: None,
max_render_width: None,
indent: 0,
border_font_style: None,
label_font_style: None,
label_justify_left: false,
}
}
}
#[derive(Debug)]
pub struct TableFormatFinal {
pub column_formats: Vec<ColumnFormat>,
pub column_delimiter: String,
pub header_separator_delimiter: String,
pub header_separator_char: char,
pub include_header_row: bool,
pub include_header_separator_row: bool,
pub include_summary_row: bool,
pub include_summary_separator_row: bool,
pub render_height: usize,
pub max_render_width: usize,
pub indent: usize,
pub border_font_style: FontStyle,
pub label_font_style: FontStyle,
pub label_justify_left: bool,
}
impl TableFormat {
pub fn add_column(&mut self, column_format: ColumnFormatShorthand) {
if self.column_formats.is_none() {
self.column_formats = Some(Vec::new());
}
if let Some(column_formats) = &mut self.column_formats {
column_formats.push(column_format);
}
}
pub fn print(&self, df: Table) -> Result<(), FormatError> {
let fmt = self.finalize(df.clone())?;
println!("{}", fmt.format(df)?);
Ok(())
}
pub fn format(&self, df: Table) -> Result<String, FormatError> {
let fmt = self.finalize(df.clone())?;
fmt.format(df)
}
fn finalize(&self, table: Table) -> Result<TableFormatFinal, FormatError> {
let column_formats: Vec<ColumnFormat> = match &self.column_formats {
Some(column_formats) => {
let mut fmts = Vec::new();
for (col_format, col) in column_formats.iter().zip(table.columns.iter()) {
fmts.push(col_format.clone().finalize(&col.data.column_type())?);
}
fmts
}
None => {
let fmts: Result<Vec<ColumnFormat>, FormatError> = table
.columns
.iter()
.map(|column| {
ColumnFormatShorthand::new()
.name(column.name.clone())
.finalize(&column.data.column_type())
})
.collect();
fmts?
}
};
let max_render_width = match self.max_render_width {
Some(value) => value,
None => {
let max_render_width = safe_sum_with_max_on_overflow(
column_formats.iter().map(|c| c.get_max_width()).collect(),
);
safe_sum_with_max_on_overflow(vec![
max_render_width,
self.column_delimiter.chars().count() * (column_formats.len() - 1),
])
}
};
let fmt = TableFormatFinal {
column_formats,
column_delimiter: self.column_delimiter.clone(),
header_separator_delimiter: self.header_separator_delimiter.clone(),
header_separator_char: self.header_separator_char,
include_header_row: self.include_header_row,
include_header_separator_row: self.include_header_separator_row,
include_summary_row: self.include_summary_row,
include_summary_separator_row: self.include_summary_separator_row,
render_height: self.render_height.unwrap_or(DEFAULT_TABLE_HEIGHT),
max_render_width,
indent: self.indent,
border_font_style: self.border_font_style.clone().unwrap_or_default(),
label_font_style: self.label_font_style.clone().unwrap_or_default(),
label_justify_left: self.label_justify_left,
};
Ok(fmt)
}
pub fn border_font_style<T: Into<FontStyle>>(mut self, font_style: T) -> Self {
self.border_font_style = Some(font_style.into());
self
}
pub fn label_font_style<T: Into<FontStyle>>(mut self, font_style: T) -> Self {
self.label_font_style = Some(font_style.into());
self
}
}
fn safe_sum_with_max_on_overflow(numbers: Vec<usize>) -> usize {
let mut sum: usize = 0;
for number in numbers {
match sum.checked_add(number) {
Some(s) => sum = s,
None => return usize::MAX,
};
}
sum
}
impl TableFormatFinal {
fn n_header_lines(&self) -> usize {
self.column_formats
.iter()
.map(|f| n_lines(&f.display_name) + 1)
.max()
.unwrap_or(0)
}
fn n_data_rows(&self) -> usize {
self.render_height
- (self.include_header_row as usize)
* (self.n_header_lines() + (self.include_header_separator_row as usize))
- (self.include_summary_row as usize)
* (1 + (self.include_summary_separator_row as usize))
}
fn total_rendered_width(&self, used_widths: &[usize]) -> usize {
used_widths.iter().sum::<usize>()
+ ((used_widths.len() as i64 - 1).max(0) as usize)
* self.column_delimiter.chars().count()
}
fn render_header_rows(&self, used_widths: &[usize], total_width: usize) -> Vec<String> {
let n_header_lines = self.n_header_lines();
let mut rows: Vec<String> = (0..n_header_lines)
.map(|_| String::with_capacity(total_width))
.collect();
for (c, width) in used_widths.iter().enumerate() {
if c != 0 {
for row in rows.iter_mut() {
row.push_str(
self.border_font_style
.format(&self.column_delimiter)
.as_str(),
);
}
}
let name = self.column_formats[c].display_name.as_str();
let lines: Vec<String> = name.split('\n').map(|s| s.to_string()).collect();
let bound = n_header_lines - lines.len();
for row in rows.iter_mut().take(bound) {
row.push_str(" ".repeat(*width).as_str());
}
for (row, line) in rows.iter_mut().skip(bound).zip(lines) {
let content = if self.label_justify_left {
format!("{:<width$}", line, width = width)
} else {
format!("{:>width$}", line, width = width)
};
row.push_str(self.label_font_style.format(content).as_str());
}
}
rows
}
fn render_header_separator_row(&self, used_widths: &[usize], total_width: usize) -> String {
let mut row = String::with_capacity(total_width);
let separator = self
.border_font_style
.format(self.header_separator_char.to_string());
for (c, width) in used_widths.iter().enumerate() {
if c != 0 {
row.push_str(
self.border_font_style
.format(&self.header_separator_delimiter)
.as_str(),
);
}
row.push_str(separator.repeat(*width).as_str());
}
row
}
fn render_columns(&self, df: Table) -> Result<(Vec<usize>, Vec<Vec<String>>), FormatError> {
let mut column_min_widths: Vec<usize> = vec![];
let mut column_max_widths: Vec<usize> = vec![];
for fmt in self.column_formats.iter() {
let min_width = fmt.header_width().max(fmt.get_min_width());
let max_width = fmt.get_max_width();
if min_width > max_width {
let msg = format!("min_width > max_width for column: {}", fmt.display_name);
return Err(FormatError::InvalidFormat(msg));
}
column_min_widths.push(min_width);
column_max_widths.push(max_width);
}
let total_min_width = column_min_widths.iter().sum::<usize>()
+ self.column_delimiter.chars().count() * (self.column_formats.len() - 1);
let n_used_columns = if total_min_width >= self.max_render_width {
let mut n_used_columns = 0;
let mut used_width = 0;
for min_width in column_min_widths.iter() {
if used_width > 0 {
used_width += self.column_delimiter.chars().count();
}
if used_width + min_width <= self.max_render_width {
n_used_columns += 1;
used_width += min_width;
} else {
break;
}
}
n_used_columns
} else {
self.column_formats.len()
};
let mut columns = Vec::with_capacity(n_used_columns);
let mut used_widths = Vec::with_capacity(n_used_columns);
let mut spare_room: usize = self.max_render_width
- column_min_widths.iter().take(n_used_columns).sum::<usize>()
- self.column_delimiter.chars().count() * ((n_used_columns as i64 - 1).max(0) as usize);
for (c, column_format) in self.column_formats.iter().take(n_used_columns).enumerate() {
if let Some(0) = df.n_rows {
used_widths.push(column_min_widths[c]);
columns.push(vec![]);
continue;
}
let min_width = column_min_widths[c];
let max_width = column_max_widths[c].min(min_width + spare_room);
let name = column_format.name.clone();
let column = column_format
.clone()
.min_width(min_width)
.max_width(max_width)
.format(df.column(name)?.data.as_ref())?;
let used_width = column
.iter()
.map(width)
.max()
.ok_or(FormatError::EmptyData(format!(
"empty column: {}",
column_format.name
)))?;
let used_width = used_width.max(min_width);
columns.push(column);
used_widths.push(used_width);
spare_room -= used_width - min_width;
}
Ok((used_widths, columns))
}
fn assemble_rows(
&self,
columns: Vec<Vec<String>>,
rows: &mut Vec<String>,
total_width: usize,
used_widths: Vec<usize>,
) {
let n_data_rows = match columns.first() {
Some(column) => column.len(),
None => return,
};
let buffers: Vec<String> = used_widths
.iter()
.map(|w| " ".repeat(*w).to_string())
.collect();
for r in 0..n_data_rows {
let mut columns_lines: Vec<Vec<String>> = Vec::new();
for (c, column) in columns.iter().enumerate() {
columns_lines.push(
column[r]
.split('\n')
.map(|s| match self.column_formats[c].horizontal_align {
super::column_format::HorizontalAlign::Left => {
align_line_left(s, used_widths[c])
}
super::column_format::HorizontalAlign::Right => {
align_line_right(s, used_widths[c])
}
})
.collect(),
)
}
let n_lines = columns_lines.iter().map(|c| c.len()).max().unwrap_or(0);
for (c, column_lines) in columns_lines.iter_mut().enumerate() {
if column_lines.len() < n_lines {
match self.column_formats[c].vertical_align {
VerticalAlign::Top => {
for _ in 0..(n_lines - column_lines.len()) {
column_lines.push(buffers[c].clone())
}
}
VerticalAlign::Bottom => {
let _ = column_lines.splice(
0..0,
vec![buffers[c].clone(); n_lines - column_lines.len()],
);
}
}
}
}
for l in 0..n_lines {
let mut line = String::with_capacity(total_width);
for (c, column_lines) in columns_lines.iter().enumerate() {
if c != 0 {
line.push_str(
self.border_font_style
.format(&self.column_delimiter)
.as_str(),
)
}
if let Some(style) = &self.column_formats[c].font_style {
line.push_str(style.format(column_lines[l].clone()).as_str())
} else {
line.push_str(column_lines[l].as_str())
}
}
rows.push(line)
}
}
}
pub(crate) fn format(&self, table: Table) -> Result<String, FormatError> {
let (used_widths, columns) = self.render_columns(table)?;
let total_width = self.total_rendered_width(&used_widths);
let mut rows = Vec::with_capacity(self.render_height);
if self.include_header_row {
for row in self.render_header_rows(&used_widths, total_width) {
rows.push(row);
}
if self.include_header_separator_row {
rows.push(self.render_header_separator_row(&used_widths, total_width));
}
};
self.assemble_rows(columns, &mut rows, total_width, used_widths);
if self.include_summary_row {
todo!("summary row")
}
let rows = if self.indent > 0 {
let indent = " ".repeat(self.indent);
rows.into_iter()
.map(|row| format!("{}{}", indent, row))
.collect()
} else {
rows
};
Ok(rows.join("\n"))
}
}