use std::collections::LinkedList;
use std::error::Error;
#[derive(Debug, Clone)]
struct GanttTask {
name: String,
start: u128,
duration: u128,
}
pub struct GanttConfig {
pub timeline_width: usize,
pub name_width: usize,
pub num_markers: usize,
}
impl Default for GanttConfig {
fn default() -> Self {
Self {
timeline_width: 80,
name_width: 30,
num_markers: 6,
}
}
}
pub fn to_ascii_gantt(
start_duration_collection: &LinkedList<(String, u128, u128)>,
config: Option<GanttConfig>,
) -> Result<String, Box<dyn Error + Send + Sync>> {
let config = config.unwrap_or_default();
if start_duration_collection.is_empty() {
return Err("No timing data available".to_string().into());
}
let tasks: Vec<GanttTask> = start_duration_collection
.iter()
.map(|(name, start, duration)| GanttTask {
name: name.clone(),
start: *start,
duration: *duration,
})
.collect();
let min_start = tasks.iter().map(|t| t.start).min().unwrap_or(0);
let max_end = tasks
.iter()
.map(|t| t.start + t.duration)
.max()
.unwrap_or(0);
let total_duration = max_end - min_start;
if total_duration == 0 {
return Err("All tasks have zero duration".to_string().into());
}
let mut output = String::new();
output.push_str("Timeline (ms):\n");
let marker_line = generate_time_markers(
min_start,
max_end,
config.num_markers,
config.name_width,
config.timeline_width,
);
output.push_str(&marker_line);
output.push('\n');
let separator =
generate_separator(config.num_markers, config.name_width, config.timeline_width);
output.push_str(&separator);
output.push('\n');
for task in &tasks {
let task_line = generate_task_line(
&task.name,
task.start,
task.duration,
min_start,
total_duration,
&config,
);
output.push_str(&task_line);
output.push('\n');
}
Ok(output)
}
fn format_time_hms(ms: u128) -> String {
let total_seconds = ms / 1000;
let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
let deciseconds = (ms % 1000) / 100;
format!("{:02}:{:02}:{:02}.{}", hours, minutes, seconds, deciseconds)
}
fn generate_time_markers(
min_start: u128,
max_end: u128,
num_markers: usize,
name_width: usize,
timeline_width: usize,
) -> String {
let mut line = " ".repeat(name_width);
let segment_width = timeline_width / (num_markers - 1);
for i in 0..num_markers {
let time = min_start + (max_end - min_start) * i as u128 / (num_markers - 1) as u128;
let relative_time = time - min_start;
let marker = format_time_hms(relative_time);
let tick_position = name_width + i * segment_width;
let current_len = line.len();
let padding = tick_position.saturating_sub(current_len);
line.push_str(&" ".repeat(padding));
line.push_str(&marker);
}
line
}
fn generate_separator(num_markers: usize, name_width: usize, timeline_width: usize) -> String {
let mut line = String::new();
line.push_str(&" ".repeat(name_width));
let segment_width = timeline_width / (num_markers - 1);
for i in 0..num_markers {
if i == 0 {
line.push('|');
} else {
line.push_str(&"-".repeat(segment_width - 1));
line.push('|');
}
}
line
}
fn generate_task_line(
name: &str,
start: u128,
duration: u128,
min_start: u128,
total_duration: u128,
config: &GanttConfig,
) -> String {
let mut line = String::new();
let display_name = if name.len() > config.name_width - 2 {
format!("{}.. ", &name[..config.name_width - 4])
} else {
format!("{:<width$}", name, width = config.name_width)
};
line.push_str(&display_name);
let relative_start = start.saturating_sub(min_start);
let start_pos =
(relative_start as f64 / total_duration as f64 * config.timeline_width as f64) as usize;
let bar_length =
((duration as f64 / total_duration as f64 * config.timeline_width as f64) as usize).max(1);
line.push_str(&" ".repeat(start_pos));
line.push('[');
if bar_length > 2 {
line.push_str(&"=".repeat(bar_length - 2));
}
line.push(']');
line.push_str(&format!(" {}ms", duration));
line
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_collection() {
let collection = LinkedList::new();
let result = to_ascii_gantt(&collection, None);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "No timing data available");
}
#[test]
fn test_single_task() {
let mut collection = LinkedList::new();
collection.push_back(("task1".to_string(), 0, 100));
let result = to_ascii_gantt(&collection, None).unwrap();
assert!(result.contains("task1"));
assert!(result.contains("100ms"));
}
#[test]
fn test_multiple_tasks() {
let mut collection = LinkedList::new();
collection.push_back(("task1".to_string(), 0, 100));
collection.push_back(("task2".to_string(), 50, 150));
collection.push_back(("task3".to_string(), 100, 100));
let result = to_ascii_gantt(&collection, None).unwrap();
assert!(result.contains("task1"));
assert!(result.contains("task2"));
assert!(result.contains("task3"));
assert!(result.contains("Timeline (ms)"));
}
#[test]
fn test_custom_config() {
let mut collection = LinkedList::new();
collection.push_back(("task1".to_string(), 0, 100));
let config = GanttConfig {
timeline_width: 40,
name_width: 20,
num_markers: 5,
};
let result = to_ascii_gantt(&collection, Some(config));
assert!(result.unwrap().contains("task1"));
}
#[test]
fn test_long_task_name() {
let mut collection = LinkedList::new();
collection.push_back(("very_long_task_name_that_exceeds_width".to_string(), 0, 100));
let config = GanttConfig {
timeline_width: 60,
name_width: 20,
num_markers: 6,
};
let result = to_ascii_gantt(&collection, Some(config));
assert!(result.unwrap().contains(".."));
}
}