use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt;
use std::ops::Index as IndexTrait;
use std::sync::Arc;
use crate::types::Value;
fn natural_cmp(a: &str, b: &str) -> Ordering {
match (a.parse::<i64>(), b.parse::<i64>()) {
(Ok(na), Ok(nb)) => na.cmp(&nb),
_ => a.cmp(b),
}
}
#[derive(Debug, Clone)]
pub struct TextTable {
header: Arc<Vec<String>>,
rows: Vec<Row>,
superkey: Vec<String>,
column_index: HashMap<String, usize>,
}
impl TextTable {
pub fn new(header: Vec<String>) -> Self {
let column_index: HashMap<String, usize> = header
.iter()
.enumerate()
.map(|(i, name)| (name.to_lowercase(), i))
.collect();
Self {
header: Arc::new(header),
rows: Vec::new(),
superkey: Vec::new(),
column_index,
}
}
pub fn from_values(header: Vec<String>, values: Vec<Vec<Value>>) -> Self {
let column_index: HashMap<String, usize> = header
.iter()
.enumerate()
.map(|(i, name)| (name.to_lowercase(), i))
.collect();
let header = Arc::new(header);
let rows: Vec<Row> = values
.into_iter()
.map(|v| Row::new(v, Arc::clone(&header)))
.collect();
Self {
header,
rows,
superkey: Vec::new(),
column_index,
}
}
pub fn header(&self) -> &[String] {
&self.header
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn superkey(&self) -> &[String] {
&self.superkey
}
pub fn set_superkey(&mut self, keys: Vec<String>) {
self.superkey = keys;
}
pub fn add_keys(&mut self, keys: &[String]) {
for key in keys {
if !self.superkey.contains(key) {
self.superkey.push(key.clone());
}
}
}
pub fn append(&mut self, values: Vec<Value>) {
self.rows.push(Row::new(values, Arc::clone(&self.header)));
}
pub fn append_row(&mut self, row: Row) {
self.rows.push(row);
}
pub fn remove(&mut self, index: usize) -> Option<Row> {
if index < self.rows.len() {
Some(self.rows.remove(index))
} else {
None
}
}
pub fn get(&self, index: usize) -> Option<&Row> {
self.rows.get(index)
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut Row> {
self.rows.get_mut(index)
}
pub fn iter(&self) -> impl Iterator<Item = &Row> {
self.rows.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Row> {
self.rows.iter_mut()
}
pub fn filter<F>(&self, predicate: F) -> TextTable
where
F: Fn(&Row) -> bool,
{
let rows: Vec<Row> = self.rows.iter().filter(|r| predicate(r)).cloned().collect();
TextTable {
header: Arc::clone(&self.header),
rows,
superkey: self.superkey.clone(),
column_index: self.column_index.clone(),
}
}
pub fn sort(&mut self) {
if self.superkey.is_empty() {
self.rows.sort_by(|a, b| {
let va = a.values.first().map(|v| v.as_string()).unwrap_or_default();
let vb = b.values.first().map(|v| v.as_string()).unwrap_or_default();
natural_cmp(&va, &vb)
});
} else {
let key_indices: Vec<usize> = self
.superkey
.iter()
.filter_map(|k| self.column_index.get(&k.to_lowercase()).copied())
.collect();
self.rows.sort_by(|a, b| {
for &idx in &key_indices {
let va = a.values.get(idx).map(|v| v.as_string()).unwrap_or_default();
let vb = b.values.get(idx).map(|v| v.as_string()).unwrap_or_default();
match natural_cmp(&va, &vb) {
std::cmp::Ordering::Equal => continue,
other => return other,
}
}
std::cmp::Ordering::Equal
});
}
}
pub fn sort_by_key<K, F>(&mut self, f: F)
where
K: Ord,
F: Fn(&Row) -> K,
{
self.rows.sort_by_key(f);
}
pub fn sort_by<F>(&mut self, compare: F)
where
F: Fn(&Row, &Row) -> std::cmp::Ordering,
{
self.rows.sort_by(compare);
}
pub fn row_with(&self, column: &str, value: &str) -> Option<&Row> {
let idx = self.column_index.get(&column.to_lowercase())?;
self.rows.iter().find(|r| {
r.values
.get(*idx)
.map(|v| v.as_string() == value)
.unwrap_or(false)
})
}
pub fn formatted(&self) -> String {
if self.rows.is_empty() {
return String::new();
}
let mut widths: Vec<usize> = self.header.iter().map(|h| h.len()).collect();
for row in &self.rows {
for (i, value) in row.values.iter().enumerate() {
if i < widths.len() {
widths[i] = widths[i].max(value.as_string().len());
}
}
}
let mut output = String::new();
for (i, name) in self.header.iter().enumerate() {
if i > 0 {
output.push_str(" ");
}
output.push_str(&format!("{:width$}", name, width = widths[i]));
}
output.push('\n');
for (i, &width) in widths.iter().enumerate() {
if i > 0 {
output.push_str(" ");
}
output.push_str(&"-".repeat(width));
}
output.push('\n');
for row in &self.rows {
for (i, value) in row.values.iter().enumerate() {
if i > 0 {
output.push_str(" ");
}
if i < widths.len() {
output.push_str(&format!("{:width$}", value.as_string(), width = widths[i]));
}
}
output.push('\n');
}
output
}
pub fn to_csv(&self) -> String {
let mut output = String::new();
output.push_str(&self.header.join(", "));
output.push('\n');
for row in &self.rows {
let values: Vec<String> = row.values.iter().map(|v| v.as_string()).collect();
output.push_str(&values.join(", "));
output.push('\n');
}
output
}
pub fn into_values(self) -> Vec<Vec<Value>> {
self.rows.into_iter().map(|r| r.values).collect()
}
pub fn values(&self) -> impl Iterator<Item = &Vec<Value>> {
self.rows.iter().map(|r| &r.values)
}
pub fn clear(&mut self) {
self.rows.clear();
}
}
impl fmt::Display for TextTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.formatted())
}
}
impl<'a> IntoIterator for &'a TextTable {
type Item = &'a Row;
type IntoIter = std::slice::Iter<'a, Row>;
fn into_iter(self) -> Self::IntoIter {
self.rows.iter()
}
}
impl IntoIterator for TextTable {
type Item = Row;
type IntoIter = std::vec::IntoIter<Row>;
fn into_iter(self) -> Self::IntoIter {
self.rows.into_iter()
}
}
#[cfg(feature = "serde")]
impl TextTable {
pub fn into_deserialize<T>(self) -> Result<Vec<T>, super::CliTableError>
where
T: serde::de::DeserializeOwned,
{
let header: Vec<String> = self.header.iter().map(|s| s.to_lowercase()).collect();
self.rows
.into_iter()
.map(|row| {
crate::de::from_record_borrowed(&header, row.values)
.map_err(|e| super::CliTableError::Parse(crate::ParseError::DeserializeError(e.to_string())))
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct Row {
values: Vec<Value>,
header: Arc<Vec<String>>,
}
impl Row {
pub fn new(values: Vec<Value>, header: Arc<Vec<String>>) -> Self {
Self { values, header }
}
pub fn get(&self, column: &str) -> Option<&Value> {
let column_lower = column.to_lowercase();
self.header
.iter()
.position(|h| h.to_lowercase() == column_lower)
.and_then(|i| self.values.get(i))
}
pub fn get_mut(&mut self, column: &str) -> Option<&mut Value> {
let column_lower = column.to_lowercase();
self.header
.iter()
.position(|h| h.to_lowercase() == column_lower)
.and_then(|i| self.values.get_mut(i))
}
pub fn values(&self) -> &[Value] {
&self.values
}
pub fn header(&self) -> &[String] {
&self.header
}
pub fn to_map(&self) -> HashMap<String, Value> {
self.header
.iter()
.zip(self.values.iter())
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
}
impl IndexTrait<&str> for Row {
type Output = Value;
fn index(&self, column: &str) -> &Self::Output {
self.get(column)
.unwrap_or_else(|| panic!("column '{}' not found", column))
}
}
impl IndexTrait<usize> for Row {
type Output = Value;
fn index(&self, index: usize) -> &Self::Output {
&self.values[index]
}
}
impl fmt::Display for Row {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let values: Vec<String> = self.values.iter().map(|v| v.as_string()).collect();
write!(f, "{}", values.join(", "))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_table() -> TextTable {
let header = vec![
"Interface".into(),
"Status".into(),
"IP_Address".into(),
];
let values = vec![
vec![
Value::Single("eth0".into()),
Value::Single("up".into()),
Value::Single("192.168.1.1".into()),
],
vec![
Value::Single("eth1".into()),
Value::Single("down".into()),
Value::Single("10.0.0.1".into()),
],
vec![
Value::Single("lo".into()),
Value::Single("up".into()),
Value::Single("127.0.0.1".into()),
],
];
TextTable::from_values(header, values)
}
#[test]
fn test_table_creation() {
let table = sample_table();
assert_eq!(table.len(), 3);
assert_eq!(table.header().len(), 3);
}
#[test]
fn test_row_indexing_by_name() {
let table = sample_table();
let row = table.get(0).unwrap();
assert_eq!(row["Interface"].as_string(), "eth0");
assert_eq!(row["Status"].as_string(), "up");
assert_eq!(row["IP_Address"].as_string(), "192.168.1.1");
}
#[test]
fn test_row_indexing_case_insensitive() {
let table = sample_table();
let row = table.get(0).unwrap();
assert_eq!(row["interface"].as_string(), "eth0");
assert_eq!(row["INTERFACE"].as_string(), "eth0");
}
#[test]
fn test_filter() {
let table = sample_table();
let up_only = table.filter(|row| row["Status"].as_string() == "up");
assert_eq!(up_only.len(), 2);
}
#[test]
fn test_sort() {
let mut table = sample_table();
table.sort();
assert_eq!(table.get(0).unwrap()["Interface"].as_string(), "eth0");
assert_eq!(table.get(1).unwrap()["Interface"].as_string(), "eth1");
assert_eq!(table.get(2).unwrap()["Interface"].as_string(), "lo");
}
#[test]
fn test_sort_by_superkey() {
let mut table = sample_table();
table.set_superkey(vec!["Status".into(), "Interface".into()]);
table.sort();
assert_eq!(table.get(0).unwrap()["Status"].as_string(), "down");
}
#[test]
fn test_row_with() {
let table = sample_table();
let row = table.row_with("Interface", "eth1");
assert!(row.is_some());
assert_eq!(row.unwrap()["Status"].as_string(), "down");
}
#[test]
fn test_formatted_output() {
let table = sample_table();
let output = table.formatted();
assert!(output.contains("Interface"));
assert!(output.contains("eth0"));
assert!(output.contains("192.168.1.1"));
}
#[test]
fn test_iteration() {
let table = sample_table();
let count = table.iter().count();
assert_eq!(count, 3);
}
#[test]
fn test_append() {
let mut table = sample_table();
table.append(vec![
Value::Single("eth2".into()),
Value::Single("up".into()),
Value::Single("172.16.0.1".into()),
]);
assert_eq!(table.len(), 4);
}
#[test]
fn test_remove() {
let mut table = sample_table();
let removed = table.remove(1);
assert!(removed.is_some());
assert_eq!(table.len(), 2);
}
#[test]
fn test_to_map() {
let table = sample_table();
let row = table.get(0).unwrap();
let map = row.to_map();
assert_eq!(map.get("Interface").unwrap().as_string(), "eth0");
}
}