use crate::config::{Config, ListType, TimeFormat};
use chrono::{NaiveDate, NaiveTime, Timelike};
use lazy_static::lazy_static;
use regex::Regex;
use std::path::PathBuf;
lazy_static! {
static ref TIME_PATTERN: Regex =
Regex::new(r"^(?:[-*]\s*)?(\d{2}:\d{2}(?::\d{2})?(?:\s*[AaPp][Mm])?)\s*(.+)$").unwrap();
}
pub fn format_time(time: NaiveTime, format: &TimeFormat) -> String {
match format {
TimeFormat::Hour24 => time.format("%H:%M:%S").to_string(),
TimeFormat::Hour12 => {
let hour = time.hour();
let minute = time.minute();
let second = time.second();
let period = if hour < 12 { "AM" } else { "PM" };
let hour12 = match hour {
0 => 12,
13..=23 => hour - 12,
_ => hour,
};
format!("{:02}:{:02}:{:02} {}", hour12, minute, second, period)
}
}
}
pub fn parse_time(time_str: &str) -> Option<NaiveTime> {
if time_str.matches(':').count() >= 2 {
let parts: Vec<&str> = time_str.split(':').collect();
if parts.len() >= 3 {
if let Ok(seconds) = parts[2]
.split_whitespace()
.next()
.unwrap_or("")
.parse::<u32>()
{
if seconds >= 60 {
return None;
}
}
}
}
if let Ok(time) = NaiveTime::parse_from_str(time_str, "%H:%M:%S") {
return Some(time);
}
if let Ok(time) = NaiveTime::parse_from_str(time_str, "%H:%M") {
return Some(NaiveTime::from_hms_opt(time.hour(), time.minute(), 0).unwrap());
}
let formats_with_seconds = vec![
"%I:%M:%S %p", "%I:%M:%S%p", "%l:%M:%S %p", "%l:%M:%S%p", ];
for format in formats_with_seconds {
if let Ok(time) = NaiveTime::parse_from_str(&time_str.to_uppercase(), format) {
if time.second() >= 60 {
continue;
}
return Some(time);
}
}
let formats = vec![
"%I:%M %p", "%I:%M%p", "%l:%M %p", "%l:%M%p", ];
for format in formats {
if let Ok(time) = NaiveTime::parse_from_str(&time_str.to_uppercase(), format) {
return Some(NaiveTime::from_hms_opt(time.hour(), time.minute(), 0).unwrap());
}
}
None
}
pub fn get_log_path_for_date(date: NaiveDate, config: &Config) -> PathBuf {
let mut path = PathBuf::from(&config.vault);
let year = date.format("%Y").to_string();
let month = date.format("%m").to_string();
let date_str = date.format("%Y-%m-%d").to_string();
let file_path = config
.file_path_format
.replace("{year}", &year)
.replace("{month}", &month)
.replace("{date}", &date_str);
path.push(file_path);
path
}
fn format_table_row(timestamp: &str, entry: &str, time_width: usize, entry_width: usize) -> String {
format!(
"| {:<width_t$} | {:<width_e$} |",
timestamp,
entry,
width_t = time_width,
width_e = entry_width
)
}
fn format_table_separator(time_width: usize, entry_width: usize) -> String {
format!(
"|{}|{}|",
"-".repeat(time_width + 2),
"-".repeat(entry_width + 2)
)
}
fn parse_entry(entry: &str) -> (String, String) {
if entry.starts_with('|') {
let parts: Vec<&str> = entry.split('|').collect();
if parts.len() >= 4 {
return (parts[1].trim().to_string(), parts[2].trim().to_string());
}
} else if entry.starts_with(['*', '-']) {
let content = entry.trim_start_matches(['-', '*', ' ']);
let time_patterns = [
r"^(\d{1,2}:\d{2}:\d{2})\s+(.+)$",
r"^(\d{1,2}:\d{2})\s+(.+)$",
r"^(\d{1,2}:\d{2}:\d{2}\s+[AaPp][Mm])\s+(.+)$",
r"^(\d{1,2}:\d{2}\s+[AaPp][Mm])\s+(.+)$",
];
for pattern in &time_patterns {
if let Ok(regex) = Regex::new(pattern) {
if let Some(captures) = regex.captures(content) {
let time = captures.get(1).unwrap().as_str().trim();
let entry_text = captures.get(2).unwrap().as_str().trim();
return (time.to_string(), entry_text.to_string());
}
}
}
if let Some(space_pos) = content.find(' ') {
if let Some(second_space) = content[space_pos + 1..].find(' ') {
return (
content[..space_pos + 1 + second_space].trim().to_string(),
content[space_pos + 1 + second_space + 1..]
.trim()
.to_string(),
);
}
}
}
(String::new(), String::new())
}
pub fn extract_log_entries(
content: &str,
section_header: &str,
list_type: &ListType,
config: &Config,
include_header: bool,
) -> (String, String, Vec<String>, ListType) {
let mut before = String::new();
let mut after = String::new();
let mut entries = Vec::new();
let mut found_type = list_type.clone();
let mut in_section = false;
let mut found_section = false;
let lines = content.lines().peekable();
for line in lines {
if line.starts_with(section_header) {
found_section = true;
in_section = true;
before = before.trim_end().to_string() + "\n\n";
continue;
}
if in_section {
if line.starts_with("##") {
in_section = false;
after = line.to_string();
continue;
}
let trimmed = line.trim();
if !trimmed.is_empty() {
if trimmed.starts_with('|') {
found_type = ListType::Table;
} else if trimmed.starts_with(['*', '-']) {
found_type = ListType::Bullet;
}
if !trimmed.contains("---")
&& trimmed != format!("| {} | {} |", config.time_label, config.event_label)
{
entries.push(line.to_string());
}
}
} else if !found_section {
before.push_str(line);
before.push('\n');
} else if !line.is_empty() {
after.push('\n');
after.push_str(line);
}
}
if found_type != *list_type {
let mut converted_entries = Vec::new();
if *list_type == ListType::Table {
let mut max_time_width = config.time_label.len();
let mut max_entry_width = config.event_label.len();
for entry in &entries {
let (time, text) = parse_entry(entry);
let formatted_time = if let Some(parsed_time) = parse_time(&time) {
format_time(parsed_time, &config.time_format)
} else {
time
};
max_time_width = max_time_width.max(formatted_time.len());
max_entry_width = max_entry_width.max(text.len());
}
if include_header {
converted_entries.push(format_table_row(
&config.time_label,
&config.event_label,
max_time_width,
max_entry_width,
));
converted_entries.push(format_table_separator(max_time_width, max_entry_width));
}
for entry in entries {
let (time, text) = parse_entry(&entry);
let formatted_time = if let Some(parsed_time) = parse_time(&time) {
format_time(parsed_time, &config.time_format)
} else {
time
};
converted_entries.push(format_table_row(
&formatted_time,
&text,
max_time_width,
max_entry_width,
));
}
} else {
if include_header {
converted_entries.push(format!(
"<!-- {} | {} -->",
config.time_label, config.event_label
));
}
for entry in entries {
let (time, text) = parse_entry(&entry);
if !time.is_empty() && !text.is_empty() {
let formatted_time = if let Some(parsed_time) = parse_time(&time) {
format_time(parsed_time, &config.time_format)
} else {
time
};
converted_entries.push(format!("- {} {}", formatted_time, text));
}
}
}
entries = converted_entries;
} else if *list_type == ListType::Table && found_type == ListType::Table && include_header {
let mut max_time_width = config.time_label.len();
let mut max_entry_width = config.event_label.len();
for entry in &entries {
let (time, text) = parse_entry(entry);
max_time_width = max_time_width.max(time.len());
max_entry_width = max_entry_width.max(text.len());
}
let mut rebuilt_entries = Vec::new();
rebuilt_entries.push(format_table_row(
&config.time_label,
&config.event_label,
max_time_width,
max_entry_width,
));
rebuilt_entries.push(format_table_separator(max_time_width, max_entry_width));
for entry in entries {
let (time, text) = parse_entry(&entry);
if !time.is_empty() && !text.is_empty() {
rebuilt_entries.push(format_table_row(
&time,
&text,
max_time_width,
max_entry_width,
));
}
}
entries = rebuilt_entries;
}
(before, after, entries, found_type)
}