#![allow(dead_code)]
use chrono::{DateTime, Local, NaiveDate, Utc};
use std::collections::HashSet;
use std::io::{self, BufRead, IsTerminal};
use tdt_core::core::cache::EntityCache;
use tdt_core::core::identity::EntityId;
use tdt_core::core::shortid::ShortIdIndex;
pub fn format_short_id(id: &EntityId) -> String {
let s = id.to_string();
if s.len() > 16 {
format!("{}...", &s[..13])
} else {
s
}
}
pub fn format_short_id_str(id: &str) -> String {
if id.len() > 16 {
format!("{}...", &id[..13])
} else {
id.to_string()
}
}
pub fn truncate_str(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}
pub fn escape_csv(s: &str) -> String {
if s.contains(',') || s.contains('"') || s.contains('\n') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_string()
}
}
pub fn read_ids_from_stdin() -> Option<Vec<String>> {
let stdin = io::stdin();
if stdin.is_terminal() {
return None;
}
let ids: Vec<String> = stdin
.lock()
.lines()
.map_while(|line| line.ok())
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect();
if ids.is_empty() {
None
} else {
Some(ids)
}
}
pub fn stdin_has_data() -> bool {
!io::stdin().is_terminal()
}
pub fn read_single_id_from_stdin() -> Option<String> {
let stdin = io::stdin();
if stdin.is_terminal() {
return None;
}
let mut line = String::new();
if stdin.lock().read_line(&mut line).ok()? > 0 {
let id = line.trim().to_string();
if !id.is_empty() {
return Some(id);
}
}
None
}
pub fn resolve_id_arg(arg: &Option<String>) -> Result<String, &'static str> {
match arg {
Some(id) if id == "-" => {
read_single_id_from_stdin().ok_or("No ID provided on stdin")
}
Some(id) if !id.is_empty() => {
Ok(id.clone())
}
_ => {
read_single_id_from_stdin()
.ok_or("No ID provided. Use: tdt <cmd> show <ID> or pipe an ID")
}
}
}
pub fn resolve_linked_to(
linked_to: &[String],
via: Option<&str>,
short_ids: &ShortIdIndex,
cache: &EntityCache,
) -> Option<HashSet<String>> {
if linked_to.is_empty() {
return None;
}
let raw_ids = if linked_to.len() == 1 && linked_to[0] == "-" {
read_ids_from_stdin()?
} else {
linked_to.to_vec()
};
let resolved: Vec<String> = raw_ids
.iter()
.map(|id| short_ids.resolve(id).unwrap_or_else(|| id.clone()))
.collect();
Some(cache.get_ids_linked_to(&resolved, via))
}
pub fn format_datetime_local(dt: &DateTime<Utc>) -> String {
let local: DateTime<Local> = dt.with_timezone(&Local);
local.format("%Y-%m-%d %H:%M").to_string()
}
pub fn format_date_local(dt: &DateTime<Utc>) -> String {
let local: DateTime<Local> = dt.with_timezone(&Local);
local.format("%Y-%m-%d").to_string()
}
pub fn format_naive_date(date: &NaiveDate) -> String {
date.format("%Y-%m-%d").to_string()
}
#[allow(dead_code)]
pub struct MarkdownTable {
headers: Vec<String>,
rows: Vec<Vec<String>>,
}
impl MarkdownTable {
pub fn new<S: AsRef<str>>(headers: Vec<S>) -> Self {
Self {
headers: headers
.into_iter()
.map(|h| h.as_ref().to_string())
.collect(),
rows: Vec::new(),
}
}
pub fn add_row<S: AsRef<str>>(&mut self, row: Vec<S>) {
self.rows
.push(row.into_iter().map(|c| c.as_ref().to_string()).collect());
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn render(&self) -> String {
if self.headers.is_empty() {
return String::new();
}
let mut widths: Vec<usize> = self.headers.iter().map(|h| h.len()).collect();
for row in &self.rows {
for (i, cell) in row.iter().enumerate() {
if i < widths.len() {
widths[i] = widths[i].max(cell.len());
}
}
}
let mut output = String::new();
output.push('|');
for (i, header) in self.headers.iter().enumerate() {
output.push_str(&format!(" {:<width$} |", header, width = widths[i]));
}
output.push('\n');
output.push('|');
for width in &widths {
output.push_str(&format!("{:-<width$}|", "", width = width + 2));
}
output.push('\n');
for row in &self.rows {
output.push('|');
for (i, cell) in row.iter().enumerate() {
let width = widths.get(i).copied().unwrap_or(0);
output.push_str(&format!(" {:<width$} |", cell, width = width));
}
output.push('\n');
}
output
}
}
pub fn smart_round(value: f64, reference_precision: f64) -> f64 {
let decimal_places = determine_decimal_places(reference_precision);
round_to_places(value, decimal_places + 1)
}
pub fn round_to_places(value: f64, decimal_places: u32) -> f64 {
let multiplier = 10_f64.powi(decimal_places as i32);
(value * multiplier).round() / multiplier
}
fn determine_decimal_places(reference: f64) -> u32 {
if reference == 0.0 {
return 4; }
let abs_ref = reference.abs();
if abs_ref >= 1.0 {
1
} else if abs_ref >= 0.1 {
2
} else if abs_ref >= 0.01 {
3
} else if abs_ref >= 0.001 {
4
} else if abs_ref >= 0.0001 {
5
} else {
6 }
}
#[cfg(test)]
mod tests {
use super::*;
use tdt_core::core::identity::EntityPrefix;
#[test]
fn test_format_short_id() {
let id = EntityId::new(EntityPrefix::Req);
let formatted = format_short_id(&id);
assert!(formatted.len() <= 16);
assert!(formatted.ends_with("..."));
}
#[test]
fn test_format_short_id_str() {
assert_eq!(format_short_id_str("SHORT"), "SHORT");
assert_eq!(
format_short_id_str("REQ-01J123456789ABCDEF123456"),
"REQ-01J123456..."
);
}
#[test]
fn test_truncate_str() {
assert_eq!(truncate_str("hello", 10), "hello");
assert_eq!(truncate_str("hello world", 8), "hello...");
assert_eq!(truncate_str("hi", 2), "hi");
}
#[test]
fn test_escape_csv() {
assert_eq!(escape_csv("simple"), "simple");
assert_eq!(escape_csv("with,comma"), "\"with,comma\"");
assert_eq!(escape_csv("with\"quote"), "\"with\"\"quote\"");
assert_eq!(escape_csv("with\nnewline"), "\"with\nnewline\"");
}
#[test]
fn test_round_to_places() {
assert!((round_to_places(1.23456, 2) - 1.23).abs() < 1e-10);
assert!((round_to_places(1.23456, 3) - 1.235).abs() < 1e-10);
assert!((round_to_places(1.23456, 4) - 1.2346).abs() < 1e-10);
assert!((round_to_places(-0.019999999999999574, 4) - (-0.02)).abs() < 1e-10);
}
#[test]
fn test_smart_round() {
assert!((smart_round(0.019999999999999574, 0.01) - 0.02).abs() < 1e-10);
assert!((smart_round(-0.019999999999999574, 0.01) - (-0.02)).abs() < 1e-10);
assert!((smart_round(0.1234567, 0.1) - 0.123).abs() < 1e-10);
assert!((smart_round(0.00123456789, 0.001) - 0.00123).abs() < 1e-10);
}
#[test]
fn test_determine_decimal_places() {
assert_eq!(determine_decimal_places(1.0), 1);
assert_eq!(determine_decimal_places(0.5), 2); assert_eq!(determine_decimal_places(0.1), 2);
assert_eq!(determine_decimal_places(0.05), 3); assert_eq!(determine_decimal_places(0.01), 3);
assert_eq!(determine_decimal_places(0.001), 4);
assert_eq!(determine_decimal_places(0.0), 4); }
#[test]
fn test_markdown_table() {
let mut table = MarkdownTable::new(vec!["ID", "Name", "Value"]);
table.add_row(vec!["1", "Alpha", "100"]);
table.add_row(vec!["2", "Beta", "2000"]);
let output = table.render();
assert!(output.contains("| ID | Name | Value |"));
assert!(output.contains("| 1 | Alpha | 100 |"));
assert!(output.contains("| 2 | Beta | 2000 |"));
}
#[test]
fn test_markdown_table_empty() {
let table = MarkdownTable::new(vec!["A", "B"]);
assert!(table.is_empty());
}
}