1use crate::datetime::RosettaDateTime;
4use crate::delta::RosettaDelta;
5use crate::i18n::LanguageData;
6
7pub fn format_datetime(dt: &RosettaDateTime, fmt: &str, lang: Option<&LanguageData>) -> String {
27 let mut result = String::with_capacity(fmt.len() + 16);
28 let mut chars = fmt.chars().peekable();
29
30 while let Some(c) = chars.next() {
31 if c == '%' {
32 if let Some(&spec) = chars.peek() {
33 chars.next();
34 match spec {
35 'Y' => result.push_str(&format!("{:04}", dt.year())),
36 'm' => result.push_str(&format!("{:02}", dt.month())),
37 'd' => result.push_str(&format!("{:02}", dt.day())),
38 'H' => result.push_str(&format!("{:02}", dt.hour())),
39 'M' => result.push_str(&format!("{:02}", dt.minute())),
40 'S' => result.push_str(&format!("{:02}", dt.second())),
41 'z' => {
42 let off = dt.offset();
43 let sign = if off.total_seconds >= 0 { '+' } else { '-' };
44 let abs = off.total_seconds.unsigned_abs();
45 let h = abs / 3600;
46 let m = (abs % 3600) / 60;
47 result.push_str(&format!("{}{:02}{:02}", sign, h, m));
48 }
49 'Z' => {
50 result.push_str(&format!("{}", dt.offset()));
51 }
52 'A' => {
53 let wd = dt.weekday() as usize;
54 if let Some(l) = lang {
55 result.push_str(l.weekdays_long.get(wd).unwrap_or(&"?"));
56 } else {
57 let defaults = [
58 "Monday",
59 "Tuesday",
60 "Wednesday",
61 "Thursday",
62 "Friday",
63 "Saturday",
64 "Sunday",
65 ];
66 result.push_str(defaults.get(wd).unwrap_or(&"?"));
67 }
68 }
69 'a' => {
70 let wd = dt.weekday() as usize;
71 if let Some(l) = lang {
72 result.push_str(l.weekdays_short.get(wd).unwrap_or(&"?"));
73 } else {
74 let defaults = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
75 result.push_str(defaults.get(wd).unwrap_or(&"?"));
76 }
77 }
78 'B' => {
79 let mo = (dt.month() as usize).wrapping_sub(1);
80 if let Some(l) = lang {
81 result.push_str(l.months_long.get(mo).unwrap_or(&"?"));
82 } else {
83 let defaults = [
84 "January",
85 "February",
86 "March",
87 "April",
88 "May",
89 "June",
90 "July",
91 "August",
92 "September",
93 "October",
94 "November",
95 "December",
96 ];
97 result.push_str(defaults.get(mo).unwrap_or(&"?"));
98 }
99 }
100 'b' => {
101 let mo = (dt.month() as usize).wrapping_sub(1);
102 if let Some(l) = lang {
103 result.push_str(l.months_short.get(mo).unwrap_or(&"?"));
104 } else {
105 let defaults = [
106 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
107 "Oct", "Nov", "Dec",
108 ];
109 result.push_str(defaults.get(mo).unwrap_or(&"?"));
110 }
111 }
112 'p' => {
113 let is_pm = dt.hour() >= 12;
114 if let Some(l) = lang {
115 if is_pm {
116 result.push_str(l.pm_indicators.first().unwrap_or(&"PM"));
117 } else {
118 result.push_str(l.am_indicators.first().unwrap_or(&"AM"));
119 }
120 } else {
121 result.push_str(if is_pm { "PM" } else { "AM" });
122 }
123 }
124 '%' => result.push('%'),
125 other => {
126 result.push('%');
127 result.push(other);
128 }
129 }
130 } else {
131 result.push('%');
132 }
133 } else {
134 result.push(c);
135 }
136 }
137
138 result
139}
140
141pub fn time_ago(
148 dt: &RosettaDateTime,
149 now: &RosettaDateTime,
150 lang: Option<&LanguageData>,
151) -> String {
152 let delta = RosettaDelta::between(dt, now);
153 let abs_secs = delta.abs_seconds();
154 let is_past = delta.is_positive(); let (value, unit_idx) = pick_dominant_unit(abs_secs);
157
158 let unit_names_en = ["second", "minute", "hour", "day", "week", "month", "year"];
159
160 if let Some(l) = lang {
161 let unit_str = l
162 .time_units
163 .get(unit_idx)
164 .and_then(|(_, kws)| kws.first())
165 .unwrap_or(&unit_names_en[unit_idx]);
166
167 if is_past {
168 let ago = l.ago_words.first().unwrap_or(&"ago");
169 if l.code == "zh" {
171 format!("{}{}{}", value, unit_str, ago)
172 } else {
173 let plural = if value != 1 { "s" } else { "" };
174 format!("{} {}{} {}", value, unit_str, plural, ago)
175 }
176 } else {
177 let future = l.future_words.first().unwrap_or(&"later");
178 let prefix = l.future_prefix.first();
179 if l.code == "zh" {
180 format!("{}{}{}", value, unit_str, future)
181 } else if let Some(p) = prefix {
182 let plural = if value != 1 { "s" } else { "" };
183 format!("{} {} {}{}", p, value, unit_str, plural)
184 } else {
185 let plural = if value != 1 { "s" } else { "" };
186 format!("{} {}{} {}", value, unit_str, plural, future)
187 }
188 }
189 } else {
190 let unit = unit_names_en[unit_idx];
192 let plural = if value != 1 { "s" } else { "" };
193 if is_past {
194 format!("{} {}{} ago", value, unit, plural)
195 } else {
196 format!("in {} {}{}", value, unit, plural)
197 }
198 }
199}
200
201fn pick_dominant_unit(abs_secs: u64) -> (u64, usize) {
205 if abs_secs < 60 {
206 (abs_secs, 0) } else if abs_secs < 3600 {
208 (abs_secs / 60, 1) } else if abs_secs < 86400 {
210 (abs_secs / 3600, 2) } else if abs_secs < 7 * 86400 {
212 (abs_secs / 86400, 3) } else if abs_secs < 30 * 86400 {
214 (abs_secs / (7 * 86400), 4) } else if abs_secs < 365 * 86400 {
216 (abs_secs / (30 * 86400), 5) } else {
218 (abs_secs / (365 * 86400), 6) }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::timezone::TzOffset;
226
227 #[test]
228 fn test_format_strftime() {
229 let dt = RosettaDateTime::from_components(2023, 3, 15, 14, 30, 45, TzOffset::from_hm(8, 0))
230 .unwrap();
231 assert_eq!(
232 format_datetime(&dt, "%Y-%m-%d %H:%M:%S %Z", None),
233 "2023-03-15 14:30:45 +08:00"
234 );
235 assert_eq!(format_datetime(&dt, "%Y/%m/%d", None), "2023/03/15");
236 }
237
238 #[test]
239 fn test_format_weekday_month() {
240 let dt = RosettaDateTime::from_components(2023, 3, 15, 14, 30, 0, TzOffset::UTC).unwrap();
241 let result = format_datetime(&dt, "%A, %B %d, %Y", None);
242 assert_eq!(result, "Wednesday, March 15, 2023");
244 }
245
246 #[test]
247 fn test_time_ago_english() {
248 let now = RosettaDateTime::from_components(2023, 10, 15, 12, 0, 0, TzOffset::UTC).unwrap();
249 let past = now.clone().add_hours(-3);
250 assert_eq!(time_ago(&past, &now, None), "3 hours ago");
251
252 let future = now.clone().add_days(2);
253 assert_eq!(time_ago(&future, &now, None), "in 2 days");
254 }
255
256 #[cfg(feature = "lang-zh")]
257 #[test]
258 fn test_time_ago_chinese() {
259 use crate::i18n::zh::CHINESE;
260 let now = RosettaDateTime::from_components(2023, 10, 15, 12, 0, 0, TzOffset::UTC).unwrap();
261 let past = now.clone().add_hours(-3);
262 let result = time_ago(&past, &now, Some(&CHINESE));
263 assert_eq!(result, "3个小时前");
264 }
265}