1use chrono::{DateTime, TimeZone, Utc};
7use chrono_tz::Tz;
8use human_date_parser::{ParseResult, from_human_time};
9
10use crate::{Error, Result, errors::UserInputError, user_input_error};
11
12#[derive(Debug)]
14pub struct App {
15 pub date: String,
17 pub format: String,
19 pub timezone: Tz,
21 pub now: Option<DateTime<Tz>>,
23}
24
25#[derive(Debug, Clone)]
27pub struct Preset {
28 pub name: String,
29 pub format: String,
30}
31
32pub fn process(app: &App, presets: &[Preset]) -> Result<String> {
37 let now = app
38 .now
39 .unwrap_or_else(|| app.timezone.from_utc_datetime(&Utc::now().naive_utc()));
40
41 let parsed = from_human_time(&app.date, now.naive_local()).map_err(|e| {
42 user_input_error!(
43 InvalidDateFormat,
44 "failed to parse human date '{}': {}",
45 app.date,
46 e
47 )
48 })?;
49
50 let fmt = resolve_format(&app.format, presets)?;
51
52 render_datetime(parsed, &fmt, now, app.timezone)
53}
54
55fn resolve_format(input: &str, presets: &[Preset]) -> Result<String> {
60 if input.is_empty() {
61 return Err(user_input_error!(MissingArgument, "empty --format"));
62 }
63
64 Ok(presets
65 .iter()
66 .find(|p| p.name == input)
67 .map(|p| p.format.clone())
68 .unwrap_or_else(|| input.to_owned()))
69}
70
71fn render_datetime(parsed: ParseResult, fmt: &str, now: DateTime<Tz>, tz: Tz) -> Result<String> {
76 use std::fmt::Write;
77
78 let naive = match parsed {
79 ParseResult::Date(d) => d.and_hms_opt(0, 0, 0).ok_or(std::fmt::Error)?,
80 ParseResult::DateTime(dt) => dt,
81 ParseResult::Time(t) => chrono::NaiveDateTime::new(now.date_naive(), t),
82 };
83
84 let zoned = tz
85 .from_local_datetime(&naive)
86 .single()
87 .ok_or(std::fmt::Error)?;
88
89 let mut out = String::new();
91 write!(&mut out, "{}", zoned.format(fmt))?;
92 Ok(out)
93}
94
95impl App {
96 #[inline]
97 pub fn new(date: String, format: String, timezone: Tz, now: Option<DateTime<Tz>>) -> Self {
98 Self {
99 date,
100 format,
101 timezone,
102 now,
103 }
104 }
105}
106
107impl Preset {
108 #[inline]
109 pub fn new(name: String, format: String) -> Self {
110 Self { name, format }
111 }
112}
113
114impl From<std::fmt::Error> for Error {
115 fn from(err: std::fmt::Error) -> Self {
116 Error::UserInput(UserInputError::UnsupportedFormat(err))
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
124
125 #[test]
126 fn resolve_format_returns_preset_when_found() {
127 let presets = [
128 Preset::new("iso".into(), "%Y-%m-%d".into()),
129 Preset::new("time".into(), "%H:%M".into()),
130 ];
131 let out = super::resolve_format("iso", &presets).unwrap();
132 assert_eq!(out, "%Y-%m-%d");
133 }
134
135 #[test]
136 fn resolve_format_returns_raw_when_not_preset() {
137 let presets = [Preset::new("iso".into(), "%Y-%m-%d".into())];
138 let out = super::resolve_format("%H:%M", &presets).unwrap();
139 assert_eq!(out, "%H:%M");
140 }
141
142 #[test]
143 fn resolve_format_fails_on_empty() {
144 let presets: [Preset; 0] = [];
145 assert!(super::resolve_format("", &presets).is_err());
146 }
147
148 #[test]
149 fn render_datetime_from_date() {
150 let ny = chrono_tz::UTC;
151 let now = ny.with_ymd_and_hms(2025, 6, 24, 12, 0, 0).unwrap();
152 let parsed = ParseResult::Date(NaiveDate::from_ymd_opt(2025, 6, 30).unwrap());
153 let out = super::render_datetime(parsed, "%Y-%m-%d", now, ny).unwrap();
154 assert_eq!(out, "2025-06-30");
155 }
156
157 #[test]
158 fn render_datetime_from_time() {
159 let tz = chrono_tz::UTC;
160 let now = tz.with_ymd_and_hms(2025, 6, 24, 0, 0, 0).unwrap();
161 let parsed = ParseResult::Time(NaiveTime::from_hms_opt(15, 30, 0).unwrap());
162 let out = super::render_datetime(parsed, "%Y-%m-%dT%H:%M:%S", now, tz).unwrap();
163 assert_eq!(out, "2025-06-24T15:30:00");
164 }
165
166 #[test]
167 fn render_datetime_handles_datetime_directly() {
168 let tz = chrono_tz::UTC;
169 let now = tz.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
170 let parsed_dt = NaiveDateTime::new(
171 NaiveDate::from_ymd_opt(2030, 1, 15).unwrap(),
172 NaiveTime::from_hms_opt(5, 45, 0).unwrap(),
173 );
174 let parsed = ParseResult::DateTime(parsed_dt);
175 let out = super::render_datetime(parsed, "%Y-%m-%d %H:%M", now, tz).unwrap();
176 assert_eq!(out, "2030-01-15 05:45");
177 }
178
179 #[test]
180 fn render_datetime_fails_on_ambiguous_local_time() {
181 let tz = chrono_tz::America::New_York;
182 let now = tz.with_ymd_and_hms(2025, 11, 1, 12, 0, 0).unwrap();
183 let ambiguous = NaiveDateTime::new(
184 NaiveDate::from_ymd_opt(2025, 11, 2).unwrap(),
185 NaiveTime::from_hms_opt(1, 30, 0).unwrap(),
186 );
187 let parsed = ParseResult::DateTime(ambiguous);
188
189 let err = super::render_datetime(parsed, "%Y-%m-%d %H:%M", now, tz).unwrap_err();
190 assert!(matches!(err, Error::UserInput(_)));
191 }
192
193 #[test]
194 fn process_with_preset_full_flow() {
195 let tz = chrono_tz::UTC;
196 let app = App::new("2025-06-24 10:00".into(), "iso".into(), tz, None);
197 let presets = [Preset::new("iso".into(), "%Y-%m-%dT%H:%M:%S".into())];
198 let out = process(&app, &presets).unwrap();
199 assert_eq!(out, "2025-06-24T10:00:00");
200 }
201
202 #[test]
203 fn process_with_raw_format() {
204 let tz = chrono_tz::UTC;
205 let now = tz.with_ymd_and_hms(2025, 6, 24, 0, 0, 0).unwrap();
206 let app = App::new("tomorrow".into(), "%Y-%m-%d".into(), tz, Some(now));
207 let out = process(&app, &[]).unwrap();
208 assert_eq!(out, "2025-06-25");
209 }
210
211 #[test]
212 fn process_errors_on_bad_date_expression() {
213 let tz = chrono_tz::UTC;
214 let app = App::new("???".into(), "%Y".into(), tz, None);
215 assert!(process(&app, &[]).is_err());
216 }
217
218 #[test]
219 fn process_errors_on_empty_format() {
220 let tz = chrono_tz::UTC;
221 let app = App::new("today".into(), "".into(), tz, None);
222 let err = process(&app, &[]).unwrap_err();
223 assert!(matches!(err, Error::UserInput(_)));
224 }
225}