datetime_parse/
lib.rs

1//! A rust library for parsing date/time strings from various formats
2//! and normalising to a standard fixed offset format (rfc3339).
3//! Parsed date will be returned `DateTime<FixedOffset>`
4//!
5
6use chrono::{
7    DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, ParseError,
8    TimeZone,
9};
10
11#[cfg(test)]
12mod tests;
13
14type Error = String;
15
16/// DateTimeFixedOffset returns a str containing date time to a
17/// standard datetime fixed offset RFC 3339 format.
18///
19/// ## Example usage:
20/// ```
21/// use datetime_parse::DateTimeFixedOffset;
22///
23/// let date_str = "Mon, 6 Jul 1970 15:30:00 PDT";
24/// let result = date_str.parse::<DateTimeFixedOffset>();
25/// assert!(result.is_ok());
26/// match result {
27///     Ok(parsed) => println!("{} => {:?}", date_str, parsed.0),
28///     Err(e) => println!("Error: {}", e)
29/// }
30/// ```
31#[derive(Debug)]
32pub struct DateTimeFixedOffset(pub DateTime<FixedOffset>);
33
34impl std::str::FromStr for DateTimeFixedOffset {
35    type Err = Error;
36
37    fn from_str(s: &str) -> Result<Self, Error> {
38        parse_from(s).map(DateTimeFixedOffset)
39    }
40}
41
42/// parse_from interprets the input date/time slice and returns a normalised parsed date/time
43/// as DateTime<FixedOffset> or will return an Error
44fn parse_from(date_time: &str) -> Result<DateTime<FixedOffset>, Error> {
45    if date_time.is_empty() {
46        Err("cannot be empty".to_string())
47    } else {
48        let date_time = standardize_date(date_time);
49        from_unix_timestamp(&date_time)
50            .or_else(|_| DateTime::parse_from_str(&date_time, "%+"))
51            .or_else(|_| from_datetime_with_tz(&date_time))
52            .or_else(|_| from_datetime_without_tz(&date_time))
53            .or_else(|_| from_date_without_tz(&date_time))
54            .or_else(|_| from_time_without_tz(&date_time))
55            .or_else(|_| from_time_with_tz(&date_time))
56            .or_else(|_| try_yms_hms_tz(&date_time))
57            .or_else(|_| try_dmmmy_hms_tz(&date_time))
58            .or_else(|_| try_mmmddyyyy_hms_tz(&date_time))
59            .or_else(|_| from_datetime_with_tz_before_year(&date_time))
60            .or_else(|_| try_others(&date_time))
61    }
62}
63
64fn from_unix_timestamp(s: &str) -> Result<DateTime<FixedOffset>, Error> {
65    let tts = s.parse::<i64>().map_err(|e| e.to_string())?;
66    let dt = if s.len() <= 10 {
67        //timestamp in seconds
68        chrono::naive::NaiveDateTime::from_timestamp(tts, 0)
69    } else if s.len() <= 13 {
70        //timestamp in milliseconds
71        chrono::naive::NaiveDateTime::from_timestamp(tts / 1000, (tts % 1000) as u32 * 1000000)
72    } else if s.len() <= 16 {
73        //timestamp in microseconds
74        chrono::naive::NaiveDateTime::from_timestamp(tts / 1000000, (tts % 1000000) as u32 * 1000)
75    } else {
76        //timestamp in nanoseconds
77        chrono::naive::NaiveDateTime::from_timestamp(tts / 1000000000, (tts % 1000000000) as u32)
78    };
79    Ok(chrono::DateTime::<FixedOffset>::from_utc(
80        dt,
81        FixedOffset::east(0),
82    ))
83}
84/// Convert a `datetime` string to `DateTime<FixedOffset>`
85fn from_datetime_with_tz(s: &str) -> Result<DateTime<FixedOffset>, ParseError> {
86    DateTime::parse_from_rfc3339(s)
87        .or_else(|_| DateTime::parse_from_rfc2822(s))
88        .or_else(|_| DateTime::parse_from_str(s, "%Y-%m-%dT%T%.f%z"))
89        .or_else(|_| DateTime::parse_from_str(s, "%Y-%m-%d %T%#z"))
90        .or_else(|_| DateTime::parse_from_str(s, "%Y-%m-%d %T.%f%#z"))
91        .or_else(|_| DateTime::parse_from_str(s, "%B %d %Y %T %#z"))
92        .or_else(|_| DateTime::parse_from_str(s, "%B %d %Y %T.%f%#z"))
93        .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %Y %T.%f%#z"))
94        .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %Y %T %#z"))
95        .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %T %#z %Y"))
96        .or_else(|_| DateTime::parse_from_str(s, "%A %B %d %T %#z %Y"))
97        .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %T.%f %#z %Y"))
98        .or_else(|_| DateTime::parse_from_str(s, "%A %B %d %T.%f %#z %Y"))
99        .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %H:%M %#z %Y"))
100        .or_else(|_| DateTime::parse_from_str(s, "%A %B %d %H:%M %#z %Y"))
101        .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %I:%M %P %#z %Y"))
102        .or_else(|_| DateTime::parse_from_str(s, "%A %B %d %I:%M %P %#z %Y"))
103        .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %I:%M%P %#z %Y"))
104        .or_else(|_| DateTime::parse_from_str(s, "%A %B %d %I:%M%P %#z %Y"))
105}
106
107/// Convert a `datetime` string, that which mostly does not have a timezone info
108/// to Datetime fixed offset with local timezone
109fn from_datetime_without_tz(s: &str) -> Result<DateTime<FixedOffset>, ParseError> {
110    Local
111        .datetime_from_str(s, "%Y-%m-%dT%T")
112        .or_else(|_| Local.datetime_from_str(s, "%c"))
113        .or_else(|_| Local.datetime_from_str(s, "%Y-%m-%dT%T.%f"))
114        .or_else(|_| Local.datetime_from_str(s, "%Y-%m-%d %T"))
115        .or_else(|_| Local.datetime_from_str(s, "%Y-%m-%d %T.%f"))
116        .or_else(|_| Local.datetime_from_str(s, "%B %d %Y %T"))
117        .or_else(|_| Local.datetime_from_str(s, "%B %d %Y %T.%f"))
118        .or_else(|_| Local.datetime_from_str(s, "%B %d, %Y %T"))
119        .or_else(|_| Local.datetime_from_str(s, "%B %d, %Y %T.%f"))
120        .or_else(|_| Local.datetime_from_str(s, "%Y-%m-%d %T"))
121        .or_else(|_| Local.datetime_from_str(s, "%Y-%m-%d %T.%f"))
122        .or_else(|_| Local.datetime_from_str(s, "%A %d %B %Y %T.%f"))
123        .or_else(|_| Local.datetime_from_str(s, "%A %d %B %Y %T"))
124        .or_else(|_| Local.datetime_from_str(s, "%A %d %B %Y %I:%M%P"))
125        .or_else(|_| Local.datetime_from_str(s, "%A %d %B %Y %I:%M %P"))
126        .or_else(|_| Local.datetime_from_str(s, "%A %d %B %Y %I:%M:%S%P"))
127        .or_else(|_| Local.datetime_from_str(s, "%A %d %B %Y %I:%M:%S %P"))
128        .or_else(|_| Local.datetime_from_str(s, "%A %d %m %Y %I:%M%P"))
129        .or_else(|_| Local.datetime_from_str(s, "%A %d %m %Y %I:%M %P"))
130        .or_else(|_| Local.datetime_from_str(s, "%A %d %m %Y %I:%M:%S%P"))
131        .or_else(|_| Local.datetime_from_str(s, "%A %d %m %Y %I:%M:%S %P"))
132        .or_else(|_| Local.datetime_from_str(s, "%d %B %Y %I:%M%P"))
133        .or_else(|_| Local.datetime_from_str(s, "%d %B %Y %I:%M %P"))
134        .or_else(|_| Local.datetime_from_str(s, "%d %B %Y %I:%M:%S%P"))
135        .or_else(|_| Local.datetime_from_str(s, "%d %B %Y %I:%M:%S %P"))
136        .or_else(|_| Local.datetime_from_str(s, "%d %m %Y %I:%M%P"))
137        .or_else(|_| Local.datetime_from_str(s, "%d %m %Y %I:%M %P"))
138        .or_else(|_| Local.datetime_from_str(s, "%d %m %Y %I:%M:%S%P"))
139        .or_else(|_| Local.datetime_from_str(s, "%d %m %Y %I:%M:%S %P"))
140        .or_else(|_| Local.datetime_from_str(s, "%-m-%-d-%Y %-H:%-M:%-S %p"))
141        .map(|x| x.with_timezone(x.offset()))
142}
143
144/// Convert just `date` string without time or timezone information to Datetime fixed offset with local timezone
145fn from_date_without_tz(s: &str) -> Result<DateTime<FixedOffset>, Error> {
146    NaiveDate::parse_from_str(s, "%Y-%m-%d")
147        .or_else(|_| NaiveDate::parse_from_str(s, "%m-%d-%y"))
148        .or_else(|_| NaiveDate::parse_from_str(s, "%D"))
149        .or_else(|_| NaiveDate::parse_from_str(s, "%F"))
150        .or_else(|_| NaiveDate::parse_from_str(s, "%v"))
151        .or_else(|_| NaiveDate::parse_from_str(s, "%B %d %Y"))
152        .or_else(|_| NaiveDate::parse_from_str(s, "%d %B %Y"))
153        .map(|x| x.and_hms(0, 0, 0))
154        .map(|x| Local.from_local_datetime(&x))
155        .map_err(|e| e.to_string())
156        .map(|x| x.unwrap().with_timezone(x.unwrap().offset()))
157}
158
159/// Convert just `time` string without date or timezone information
160/// to Datetime fixed offset with local timezone & current date
161fn from_time_without_tz(s: &str) -> Result<DateTime<FixedOffset>, ParseError> {
162    NaiveTime::parse_from_str(s, "%T")
163        .or_else(|_| NaiveTime::parse_from_str(s, "%I:%M%P"))
164        .or_else(|_| NaiveTime::parse_from_str(s, "%I:%M %P"))
165        .map(|x| Local::now().date().and_time(x).unwrap())
166        .map(|x| x.with_timezone(x.offset()))
167}
168
169/// Convert just `time` string without date but timezone information
170/// to Datetime fixed offset with local timezone & current date
171fn from_time_with_tz(s: &str) -> Result<DateTime<FixedOffset>, Error> {
172    if let Some((dt, tz)) = is_tz_alpha(s) {
173        let date = format!("{} {}", Local::today().format("%Y-%m-%d"), dt);
174        to_rfc2822(&date, tz)
175    } else {
176        Err("custom parsing failed".to_string())
177    }
178}
179
180/// Convert datetime with timezone information before the year
181/// eg: Wed Jul 1, 3:33pm PST 1970
182fn from_datetime_with_tz_before_year(s: &str) -> Result<DateTime<FixedOffset>, Error> {
183    let tokens = s.split_whitespace().collect::<Vec<_>>();
184    if tokens.len() < 2 {
185        return Err("custom parsing failed".to_string());
186    }
187    let dt = tokens[..tokens.len() - 2].join(" ") + " " + tokens.last().unwrap();
188    let tz = tokens[tokens.len() - 2];
189    to_rfc2822(&dt, tz)
190}
191
192/// Try to parse the following types of dates
193/// 1970-12-25 16:16:16 PST
194/// 1970-12-25 16:16 PST
195fn try_yms_hms_tz(s: &str) -> Result<DateTime<FixedOffset>, Error> {
196    if let Some((dt, tz)) = is_tz_alpha(s) {
197        to_rfc2822(dt, tz)
198    } else {
199        Err("custom parsing failed".to_string())
200    }
201}
202
203/// Try to parse the following types of dates
204/// 1 Jan 1970 22:00:00 PDT
205/// 1 Jan, 1970 22:00:00.000 PDT
206/// 1 Jan, 1970; 22:00:00 PDT
207fn try_dmmmy_hms_tz(s: &str) -> Result<DateTime<FixedOffset>, Error> {
208    if let Some((dt, tz)) = is_tz_alpha(s) {
209        to_rfc2822(dt, tz)
210    } else {
211        Err("custom parsing failed".to_string())
212    }
213}
214
215// Feb 14 2022 13:13:55 GMT+00:00
216// Feb 14 2022 13:13:55 GMT+0000
217// Wed Jul 1 1970 13:13:55 GMT+0000
218fn try_mmmddyyyy_hms_tz(s: &str) -> Result<DateTime<FixedOffset>, Error> {
219    let tz = s.rsplitn(2, ' ').take(2).collect::<Vec<_>>()[0].replace("GMT", "");
220    let dt = if s.rsplitn(2, ' ').count() > 1 {
221        s.rsplitn(2, ' ').take(2).collect::<Vec<_>>()[1].to_string()
222    } else {
223        return Err("custom parsing failed".to_string());
224    };
225    if !tz.is_empty() {
226        let x = dt + " " + &tz.replace(':', "");
227        DateTime::parse_from_str(&x, "%B %d %Y %H:%M:%S %z")
228            .or_else(|_| DateTime::parse_from_str(&x, "%B %d %Y %I:%M:%S%P %z"))
229            .or_else(|_| DateTime::parse_from_str(&x, "%B %d %Y %I:%M:%S %P %z"))
230            .or_else(|_| DateTime::parse_from_str(&x, "%A %B %d %Y %H:%M:%S %z"))
231            .or_else(|_| DateTime::parse_from_str(&x, "%A %B %d %Y %I:%M%P %z"))
232            .or_else(|_| DateTime::parse_from_str(&x, "%A %B %d %Y %I:%M %P %z"))
233            .map_err(|e| e.to_string())
234    } else {
235        Err("custom parsing failed".to_string())
236    }
237}
238
239/// Try to parse the following types of dates
240/// Feb 12 12:12:12 or Feb 12, 12:12
241/// Feb 12 or 12 Feb
242fn try_others(s: &str) -> Result<DateTime<FixedOffset>, Error> {
243    let date = s.split_whitespace().collect::<Vec<_>>();
244    let year = Local::now().year();
245    if date.len().eq(&2) && date[0].chars().all(char::is_alphabetic) {
246        // trying Feb 12
247        NaiveDate::parse_from_str(&format!("{} {}", s, year), "%B %d %Y")
248            .map(|x| x.and_hms(0, 0, 0))
249            .map(|x| Local.from_local_datetime(&x))
250            .map_err(|e| e.to_string())
251            .map(|x| x.unwrap().with_timezone(x.unwrap().offset()))
252    } else if date.len().eq(&2) && date[1].chars().all(char::is_alphabetic) {
253        // trying 12 Feb
254        NaiveDate::parse_from_str(&format!("{} {}", s, year), "%d %B %Y")
255            .map(|x| x.and_hms(0, 0, 0))
256            .map(|x| Local.from_local_datetime(&x))
257            .map_err(|e| e.to_string())
258            .map(|x| x.unwrap().with_timezone(x.unwrap().offset()))
259    } else if date.len().eq(&3) && date[0].replace(',', "").chars().all(char::is_alphabetic) {
260        // trying Feb 12 14:00:01 or Feb 12, 14:00:01 or Feb 12 14:00
261        Local
262            .datetime_from_str(
263                &format!("{} {} {} {}", date[0], date[1], year, date[2]),
264                "%B %d %Y %H:%M",
265            )
266            .or_else(|_| {
267                Local.datetime_from_str(
268                    &format!("{} {} {} {}", date[0], date[1], year, date[2]),
269                    "%B %d %Y %T",
270                )
271            })
272            .or_else(|_| {
273                Local.datetime_from_str(
274                    &format!("{} {} {} {}", date[0], date[1], year, date[2]),
275                    "%B %d %Y %I:%M%P",
276                )
277            })
278            .map(|x| x.with_timezone(x.offset()))
279            .map_err(|e| e.to_string())
280    } else if date.len().eq(&3) && date[1].chars().all(char::is_alphabetic) {
281        // trying 12 Feb 14:00:01 or 12 Feb, 14:00:01 or 12 Feb 14:00
282        Local
283            .datetime_from_str(
284                &format!("{} {} {} {}", date[0], date[1], year, date[2]),
285                "%d %B %Y %H:%M",
286            )
287            .or_else(|_| {
288                Local.datetime_from_str(
289                    &format!("{} {} {} {}", date[0], date[1], year, date[2]),
290                    "%d %B %Y %T",
291                )
292            })
293            .or_else(|_| {
294                Local.datetime_from_str(
295                    &format!("{} {} {} {}", date[0], date[1], year, date[2]),
296                    "%d %B %Y %I:%M%P",
297                )
298            })
299            .map(|x| x.with_timezone(x.offset()))
300            .map_err(|e| e.to_string())
301    } else if date.len().eq(&4) && date[0].chars().all(char::is_alphabetic) {
302        // trying Feb 12 3:33 pm
303        Local
304            .datetime_from_str(
305                &format!("{} {} {} {} {}", date[0], date[1], year, date[2], date[3]),
306                "%B %d %Y %I:%M %P",
307            )
308            .map(|x| x.with_timezone(x.offset()))
309            .map_err(|e| e.to_string())
310    } else if date.len().eq(&4) && date[1].chars().all(char::is_alphabetic) {
311        // trying 12 Feb 3:33 pm
312        Local
313            .datetime_from_str(
314                &format!("{} {} {} {} {}", date[0], date[1], year, date[2], date[3]),
315                "%d %B %Y %I:%M %P",
316            )
317            .map(|x| x.with_timezone(x.offset()))
318            .map_err(|e| e.to_string())
319    } else {
320        Err("failed brute force parsing".to_string())
321    }
322}
323
324/// Checks if the last characters are alphabet and assumes it to be TimeZone
325/// and returns the tuple of (date_part, timezone_part)
326fn is_tz_alpha(s: &str) -> Option<(&str, &str)> {
327    let mut dtz = s.trim().rsplitn(2, ' ');
328    let tz = dtz.next().unwrap_or_default();
329    let dt = dtz.next().unwrap_or_default();
330    if tz.chars().all(char::is_alphabetic) {
331        Some((dt, tz))
332    } else {
333        None
334    }
335}
336
337/// Convert the given date/time and timezone information into RFC 2822 format
338fn to_rfc2822(s: &str, tz: &str) -> Result<DateTime<FixedOffset>, Error> {
339    NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
340        .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %I:%M%P"))
341        .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %I:%M %P"))
342        .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M"))
343        .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %Y %T"))
344        .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %Y %T.%f"))
345        .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %Y %H:%M"))
346        .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %Y %T"))
347        .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %Y %T.%f"))
348        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %Y %T.%f"))
349        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %Y %T"))
350        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %Y %T"))
351        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %Y %T.%f"))
352        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %Y %T.%f"))
353        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %Y %T"))
354        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %Y %T"))
355        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %Y %T.%f"))
356        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %T.%f %Y"))
357        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %T %Y"))
358        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %T.%f %Y"))
359        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %T %Y"))
360        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %T.%f %Y"))
361        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %T %Y"))
362        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %m %d %H:%M %Y"))
363        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %H:%M %Y"))
364        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %H:%M %Y"))
365        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %H:%M %Y"))
366        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %m %d %I:%M%P %Y"))
367        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %I:%M%P %Y"))
368        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %I:%M %P %Y"))
369        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %I:%M%P %Y"))
370        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %I:%M %P %Y"))
371        .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %I:%M%P %Y"))
372        .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %m %T.%f %Y"))
373        .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %m %T %Y"))
374        .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %T.%f %Y"))
375        .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %T %Y"))
376        .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %T.%f %Y"))
377        .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %T %Y"))
378        .or_else(|_| NaiveDateTime::parse_from_str(s, "%m %d %I:%M %Y"))
379        .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %m %I:%M %Y"))
380        .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %I:%M %Y"))
381        .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %I:%M %Y"))
382        .or_else(|_| NaiveDateTime::parse_from_str(s, "%m %d %I:%M%P %Y"))
383        .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %m %I:%M%P %Y"))
384        .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %I:%M %P %Y"))
385        .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %I:%M%P %Y"))
386        .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %I:%M %P %Y"))
387        .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %I:%M%P %Y"))
388        .and_then(|x| {
389            DateTime::parse_from_rfc2822(
390                (x.format("%a, %d %b %Y %H:%M:%S").to_string() + " " + tz).as_str(),
391            )
392        })
393        .map_err(|e| e.to_string())
394}
395
396/// converts date/time string from having '.' or '/' to '-'
397/// and remove extra characters like ',', ';'
398/// eg: 12/13/2000 to 12-13-2000 or 12/13/2000 12:12:12.14 to 12-13-2000 12:12:12.14
399fn standardize_date(s: &str) -> String {
400    if s.len() < 8 {
401        s.to_string()
402    } else {
403        s.chars()
404            .take(8)
405            .map(|mut x| {
406                if x.eq(&'.') || x.eq(&'/') {
407                    x = '-'
408                };
409                x
410            })
411            .collect::<String>()
412            + &s[8..]
413    }
414    .replace(" UTC", " GMT")
415    .replace(" UT", " GMT")
416    .replace([',', ';'], "")
417}