foundry_mcp/utils/
timestamp.rs

1//! Timestamp generation and parsing utilities
2
3use anyhow::Result;
4use chrono::{DateTime, Datelike, NaiveDate, Timelike, Utc};
5
6/// Generate ISO timestamp string for general use
7pub fn iso_timestamp() -> String {
8    Utc::now().to_rfc3339()
9}
10
11/// Generate timestamp for spec names (YYYYMMDD_HHMMSS format)
12pub fn spec_timestamp() -> String {
13    let now = Utc::now();
14    format!(
15        "{:04}{:02}{:02}_{:02}{:02}{:02}",
16        now.year(),
17        now.month(),
18        now.day(),
19        now.hour(),
20        now.minute(),
21        now.second()
22    )
23}
24
25/// Generate human-readable timestamp
26pub fn human_timestamp() -> String {
27    Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string()
28}
29
30/// Validate timestamp format (YYYYMMDD_HHMMSS)
31pub fn validate_timestamp_format(timestamp: &str) -> bool {
32    if timestamp.len() != 15 {
33        return false;
34    }
35
36    // Check format: 8 digits, underscore, 6 digits
37    let parts: Vec<&str> = timestamp.split('_').collect();
38    if parts.len() != 2 || parts[0].len() != 8 || parts[1].len() != 6 {
39        return false;
40    }
41
42    // Validate all characters are digits
43    parts[0].chars().all(|c| c.is_ascii_digit()) && parts[1].chars().all(|c| c.is_ascii_digit())
44}
45
46/// Parse timestamp from spec name with validation
47pub fn parse_spec_timestamp(spec_name: &str) -> Option<String> {
48    if let Some(underscore_pos) = spec_name.find('_')
49        && underscore_pos == 8
50        && spec_name.len() > 15
51    {
52        let timestamp_part = &spec_name[0..15];
53        if validate_timestamp_format(timestamp_part) {
54            return Some(timestamp_part.to_string());
55        }
56    }
57    None
58}
59
60/// Convert spec timestamp to ISO format
61pub fn spec_timestamp_to_iso(spec_timestamp: &str) -> Result<String> {
62    if !validate_timestamp_format(spec_timestamp) {
63        return Err(anyhow::anyhow!(
64            "Invalid timestamp format: {}",
65            spec_timestamp
66        ));
67    }
68
69    let date_part = &spec_timestamp[0..8];
70    let time_part = &spec_timestamp[9..15];
71
72    let year: i32 = date_part[0..4].parse()?;
73    let month: u32 = date_part[4..6].parse()?;
74    let day: u32 = date_part[6..8].parse()?;
75    let hour: u32 = time_part[0..2].parse()?;
76    let minute: u32 = time_part[2..4].parse()?;
77    let second: u32 = time_part[4..6].parse()?;
78
79    let naive_date = NaiveDate::from_ymd_opt(year, month, day)
80        .ok_or_else(|| anyhow::anyhow!("Invalid date values in timestamp"))?;
81    let naive_datetime = naive_date
82        .and_hms_opt(hour, minute, second)
83        .ok_or_else(|| anyhow::anyhow!("Invalid time values in timestamp"))?;
84
85    let datetime = DateTime::<Utc>::from_naive_utc_and_offset(naive_datetime, Utc);
86    Ok(datetime.to_rfc3339())
87}
88
89/// Convert ISO timestamp to spec format
90pub fn iso_to_spec_timestamp(iso_timestamp: &str) -> Result<String> {
91    let datetime = DateTime::parse_from_rfc3339(iso_timestamp)?;
92    let utc_datetime = datetime.with_timezone(&Utc);
93
94    Ok(format!(
95        "{:04}{:02}{:02}_{:02}{:02}{:02}",
96        utc_datetime.year(),
97        utc_datetime.month(),
98        utc_datetime.day(),
99        utc_datetime.hour(),
100        utc_datetime.minute(),
101        utc_datetime.second()
102    ))
103}
104
105/// Format timestamp for human-readable display
106pub fn format_timestamp_for_display(timestamp: &str) -> String {
107    if validate_timestamp_format(timestamp) {
108        format!(
109            "{}-{}-{} {}:{}:{}",
110            &timestamp[0..4],
111            &timestamp[4..6],
112            &timestamp[6..8],
113            &timestamp[9..11],
114            &timestamp[11..13],
115            &timestamp[13..15]
116        )
117    } else {
118        timestamp.to_string()
119    }
120}
121
122/// Extract feature name from spec name
123pub fn extract_feature_name(spec_name: &str) -> Option<String> {
124    if let Some(timestamp) = parse_spec_timestamp(spec_name)
125        && spec_name.len() > timestamp.len() + 1
126    {
127        return Some(spec_name[timestamp.len() + 1..].to_string());
128    }
129    None
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_validate_timestamp_format() {
138        assert!(validate_timestamp_format("20240824_120000"));
139        assert!(!validate_timestamp_format("2024824_120000")); // Too short date
140        assert!(!validate_timestamp_format("20240824_12000")); // Too short time
141        assert!(!validate_timestamp_format("20240824120000")); // Missing underscore
142        assert!(!validate_timestamp_format("2024082a_120000")); // Invalid character
143    }
144
145    #[test]
146    fn test_parse_spec_timestamp() {
147        assert_eq!(
148            parse_spec_timestamp("20240824_120000_user_auth"),
149            Some("20240824_120000".to_string())
150        );
151        assert_eq!(parse_spec_timestamp("invalid_name"), None);
152        assert_eq!(parse_spec_timestamp("20240824_user_auth"), None); // Invalid format
153    }
154
155    #[test]
156    fn test_spec_timestamp_to_iso() {
157        let result = spec_timestamp_to_iso("20240824_120000").unwrap();
158        assert!(result.starts_with("2024-08-24T12:00:00"));
159    }
160
161    #[test]
162    fn test_extract_feature_name() {
163        assert_eq!(
164            extract_feature_name("20240824_120000_user_auth"),
165            Some("user_auth".to_string())
166        );
167        assert_eq!(
168            extract_feature_name("20240824_120000_multi_part_feature"),
169            Some("multi_part_feature".to_string())
170        );
171        assert_eq!(extract_feature_name("invalid_name"), None);
172    }
173
174    #[test]
175    fn test_format_timestamp_for_display() {
176        assert_eq!(
177            format_timestamp_for_display("20240824_120000"),
178            "2024-08-24 12:00:00"
179        );
180        assert_eq!(format_timestamp_for_display("invalid"), "invalid");
181    }
182}