astrolabe/util/
parse.rs

1use super::{
2    constants::{
3        MONTH_ABBREVIATED, MONTH_WIDE, SECS_PER_HOUR, SECS_PER_HOUR_U64, SECS_PER_MINUTE,
4        SECS_PER_MINUTE_U64, WDAY_WIDE,
5    },
6    format::get_length,
7};
8use crate::{
9    errors::{invalid_format::create_invalid_format, AstrolabeError},
10    Date, DateUtilities,
11};
12
13/// Parses the offset part from an RFC 3339 timestamp string to offset seconds
14pub(crate) fn parse_offset(string: &str) -> Result<i32, AstrolabeError> {
15    if string.starts_with('Z') {
16        return Ok(0);
17    }
18    if string.len() != 6 {
19        return Err(create_invalid_format(
20            "Failed parsing the offset from the RFC 3339 string. Format should be +XX:XX or -XX:XX"
21                .to_string(),
22        ));
23    }
24
25    let hour = string[1..3].parse::<u32>().map_err(|_| {
26        create_invalid_format(
27            "Failed parsing the hour of the offset from the RFC 3339 string".to_string(),
28        )
29    })?;
30    let min = string[4..6].parse::<u32>().map_err(|_| {
31        create_invalid_format(
32            "Failed parsing the minute of the offset from the RFC 3339 string".to_string(),
33        )
34    })?;
35
36    if hour > 23 {
37        return Err(create_invalid_format(
38            "Failed parsing the hour of the offset from the RFC 3339 string. Hour has to be less than 24".to_string(),
39        ));
40    } else if min > 59 {
41        return Err(create_invalid_format(
42            "Failed parsing the minute of the offset from the RFC 3339 string. Minute has to be less than 60".to_string(),
43        ));
44    }
45
46    let offset = hour * SECS_PER_HOUR + min * SECS_PER_MINUTE;
47
48    Ok(if string.starts_with('+') {
49        offset as i32
50    } else {
51        -(offset as i32)
52    })
53}
54
55/// Parse a format string and return parts to format
56pub(crate) fn parse_format_string(format: &str) -> Vec<String> {
57    let escaped_format = format.replace("''", "\u{0000}");
58
59    let mut parts: Vec<String> = Vec::new();
60    let mut currently_escaped = false;
61
62    for char in escaped_format.chars() {
63        match char {
64            '\'' => {
65                if !currently_escaped {
66                    parts.push(char.to_string());
67                } else {
68                    // Using unwrap because it's safe to assume that parts has a length of at least 1
69                    parts.last_mut().unwrap().push(char);
70                }
71                currently_escaped = !currently_escaped;
72            }
73            _ => {
74                if currently_escaped || parts.last().unwrap_or(&"".to_string()).starts_with(char) {
75                    // Using unwrap because it's safe to assume that parts has a length of at least 1
76                    parts.last_mut().unwrap().push(char);
77                } else {
78                    parts.push(char.to_string());
79                }
80            }
81        };
82    }
83    parts
84}
85
86pub(crate) struct ParsedPart {
87    pub(crate) value: i64,
88    pub(crate) unit: ParseUnit,
89}
90
91pub(crate) enum ParseUnit {
92    Year,
93    Month,
94    DayOfMonth,
95    DayOfYear,
96    Hour,
97    Period,
98    PeriodHour,
99    Minute,
100    Second,
101    Decis,
102    Centis,
103    Millis,
104    Micros,
105    Nanos,
106    Offset,
107}
108
109#[derive(Default)]
110pub(crate) struct ParsedDate {
111    pub(crate) year: Option<i32>,
112    pub(crate) month: Option<u32>,
113    pub(crate) day_of_month: Option<u32>,
114    pub(crate) day_of_year: Option<u32>,
115}
116
117#[derive(Default)]
118pub(crate) struct ParsedTime {
119    pub(crate) hour: Option<u64>,
120    pub(crate) period_hour: Option<u64>,
121    pub(crate) period: Option<Period>,
122    pub(crate) minute: Option<u64>,
123    pub(crate) second: Option<u64>,
124    pub(crate) decis: Option<u64>,
125    pub(crate) centis: Option<u64>,
126    pub(crate) millis: Option<u64>,
127    pub(crate) micros: Option<u64>,
128    pub(crate) nanos: Option<u64>,
129    pub(crate) offset: Option<i32>,
130}
131
132pub(crate) enum Period {
133    AM = 0,
134    PM = 12,
135}
136
137/// Formats string parts based on https://www.unicode.org/reports/tr35/tr35-dates.html#table-date-field-symbol-table
138/// **Note**: Not all field types/symbols are implemented.
139pub(crate) fn parse_part(
140    chars: &str,
141    string: &mut String,
142) -> Result<Option<ParsedPart>, AstrolabeError> {
143    // Using unwrap because it's safe to assume that chars has a length of at least 1
144    let first_char = chars.chars().next().unwrap();
145    Ok(match first_char {
146        'G' | 'y' | 'q' | 'M' | 'w' | 'd' | 'D' | 'e' => parse_date_part(chars, string)?,
147        'a' | 'b' | 'h' | 'H' | 'K' | 'k' | 'm' | 's' | 'n' | 'X' | 'x' => {
148            parse_time_part(chars, string)?
149        }
150        _ => {
151            remove_part(chars.len(), string)?;
152            None
153        }
154    })
155}
156
157/// Parse string parts based on https://www.unicode.org/reports/tr35/tr35-dates.html#table-date-field-symbol-table
158/// This function only parses date parts while ignoring time related parts (E.g. hour, minute)
159pub(crate) fn parse_date_part(
160    chars: &str,
161    string: &mut String,
162) -> Result<Option<ParsedPart>, AstrolabeError> {
163    // Using unwrap because it's safe to assume that chars has a length of at least 1
164    let first_char = chars.chars().next().unwrap();
165    Ok(match first_char {
166        'G' => match chars.len() {
167            1..=3 => {
168                remove_part(2, string)?;
169                None
170            }
171            5 => {
172                remove_part(1, string)?;
173                None
174            }
175            _ => {
176                if string.starts_with("Before Christ") {
177                    // Using unwrap because it's safe to assume that the string is long enough
178                    remove_part("Before Christ".len(), string).unwrap();
179                    None
180                } else if string.starts_with("Anno Domini") {
181                    // Using unwrap because it's safe to assume that the string is long enough
182                    remove_part("Anno Domini".len(), string).unwrap();
183                    None
184                } else {
185                    return Err(create_invalid_format(format!(
186                        "Could not parse '{}' from given string.",
187                        chars
188                    )));
189                }
190            }
191        },
192        'y' => match chars.len() {
193            2 => {
194                if string.starts_with('-') {
195                    let year = pick_part::<i32>(3, string, "year")?;
196                    Some(ParsedPart {
197                        value: year as i64,
198                        unit: ParseUnit::Year,
199                    })
200                } else {
201                    let sub_century_year = pick_part::<i32>(2, string, "year")?;
202                    let current_century = Date::now().year() / 1000 * 1000;
203                    Some(ParsedPart {
204                        value: (current_century + sub_century_year) as i64,
205                        unit: ParseUnit::Year,
206                    })
207                }
208            }
209            1 | 3 | 4 => {
210                let mut year_length = usize::from(string.starts_with('-'));
211                let string_length = string.chars().count();
212                while string_length > year_length
213                    && string.chars().nth(year_length).unwrap().is_ascii_digit()
214                {
215                    year_length += 1;
216                }
217
218                let year = pick_part::<i32>(year_length, string, "year")?;
219
220                Some(ParsedPart {
221                    value: year as i64,
222                    unit: ParseUnit::Year,
223                })
224            }
225            _ => {
226                let year = if string.starts_with('-') {
227                    pick_part::<i32>(chars.len() + 1, string, "year")?
228                } else {
229                    pick_part::<i32>(chars.len(), string, "year")?
230                };
231
232                Some(ParsedPart {
233                    value: year as i64,
234                    unit: ParseUnit::Year,
235                })
236            }
237        },
238        'q' => match chars.len() {
239            1 | 2 => {
240                remove_part(chars.len(), string)?;
241                None
242            }
243            3 => {
244                remove_part(2, string)?;
245                None
246            }
247            4 => {
248                for quarter in ["1st quarter", "2nd quarter", "3rd quarter", "4th quarter"] {
249                    if string.starts_with(quarter) {
250                        // Using unwrap because it's safe to assume that the string is long enough
251                        remove_part(quarter.len(), string).unwrap();
252                        return Ok(None);
253                    };
254                }
255                return Err(create_invalid_format(format!(
256                    "Could not parse '{}' from given string.",
257                    chars
258                )));
259            }
260            _ => {
261                remove_part(1, string)?;
262                None
263            }
264        },
265        'M' => parse_month(chars.len(), string)?,
266        'w' => match chars.len() {
267            1 => match string.chars().nth(1) {
268                Some(char) if char.is_ascii_digit() => {
269                    // Using unwrap because it's safe to assume that the string is long enough
270                    remove_part(2, string).unwrap();
271                    None
272                }
273                _ => {
274                    remove_part(1, string)?;
275                    None
276                }
277            },
278            _ => {
279                remove_part(get_length(chars.len(), 2, 2), string)?;
280                None
281            }
282        },
283        'd' => match chars.len() {
284            1 => match string.chars().nth(1) {
285                Some(char) if char.is_ascii_digit() => {
286                    let day = pick_part::<u32>(2, string, "day of month")?;
287
288                    Some(ParsedPart {
289                        value: day as i64,
290                        unit: ParseUnit::DayOfMonth,
291                    })
292                }
293                _ => {
294                    let day = pick_part::<u32>(1, string, "day of month")?;
295
296                    Some(ParsedPart {
297                        value: day as i64,
298                        unit: ParseUnit::DayOfMonth,
299                    })
300                }
301            },
302            _ => {
303                let day = pick_part::<u32>(2, string, "day of month")?;
304
305                Some(ParsedPart {
306                    value: day as i64,
307                    unit: ParseUnit::DayOfMonth,
308                })
309            }
310        },
311        'D' => match chars.len() {
312            2 => match string.chars().nth(2) {
313                Some(char) if char.is_ascii_digit() => {
314                    // Using unwrap because it's safe to assume that the string is long enough
315                    let day = pick_part::<u32>(3, string, "day of year").unwrap();
316
317                    Some(ParsedPart {
318                        value: day as i64,
319                        unit: ParseUnit::DayOfYear,
320                    })
321                }
322                _ => {
323                    let day = pick_part::<u32>(2, string, "day of year")?;
324
325                    Some(ParsedPart {
326                        value: day as i64,
327                        unit: ParseUnit::DayOfYear,
328                    })
329                }
330            },
331            3 => {
332                let day = pick_part::<u32>(3, string, "day of year")?;
333
334                Some(ParsedPart {
335                    value: day as i64,
336                    unit: ParseUnit::DayOfYear,
337                })
338            }
339            _ => match string.chars().nth(1) {
340                Some(char) if char.is_ascii_digit() => match string.chars().nth(2) {
341                    Some(char) if char.is_ascii_digit() => {
342                        // Using unwrap because it's safe to assume that the string is long enough
343                        let day = pick_part::<u32>(3, string, "day of year").unwrap();
344
345                        Some(ParsedPart {
346                            value: day as i64,
347                            unit: ParseUnit::DayOfYear,
348                        })
349                    }
350                    _ => {
351                        // Using unwrap because it's safe to assume that the string is long enough
352                        let day = pick_part::<u32>(2, string, "day of year").unwrap();
353
354                        Some(ParsedPart {
355                            value: day as i64,
356                            unit: ParseUnit::DayOfYear,
357                        })
358                    }
359                },
360                _ => {
361                    let day = pick_part::<u32>(1, string, "day of year")?;
362
363                    Some(ParsedPart {
364                        value: day as i64,
365                        unit: ParseUnit::DayOfYear,
366                    })
367                }
368            },
369        },
370        'e' => parse_wday(chars.len(), string)?,
371        _ => {
372            remove_part(chars.len(), string)?;
373            None
374        }
375    })
376}
377
378/// Parse string parts based on https://www.unicode.org/reports/tr35/tr35-dates.html#table-date-field-symbol-table
379/// This function only parses time parts while ignoring date related parts (E.g. year, day)
380pub(crate) fn parse_time_part(
381    chars: &str,
382    string: &mut String,
383) -> Result<Option<ParsedPart>, AstrolabeError> {
384    // Using unwrap because it's safe to assume that chars has a length of at least 1
385    let first_char = chars.chars().next().unwrap();
386    Ok(match first_char {
387        'a' => match chars.len() {
388            4 => {
389                let period = pick_part::<String>(4, string, "period")?;
390                match period.as_str() {
391                    "a.m." => Some(ParsedPart {
392                        value: 0,
393                        unit: ParseUnit::Period,
394                    }),
395                    "p.m." => Some(ParsedPart {
396                        value: 1,
397                        unit: ParseUnit::Period,
398                    }),
399                    _ => {
400                        return Err(create_invalid_format(format!(
401                            "Could not parse '{}' from given string.",
402                            chars
403                        )));
404                    }
405                }
406            }
407            5 => {
408                let period = pick_part::<String>(1, string, "period")?;
409                match period.as_str() {
410                    "a" => Some(ParsedPart {
411                        value: 0,
412                        unit: ParseUnit::Period,
413                    }),
414                    "p" => Some(ParsedPart {
415                        value: 1,
416                        unit: ParseUnit::Period,
417                    }),
418                    _ => {
419                        return Err(create_invalid_format(format!(
420                            "Could not parse '{}' from given string.",
421                            chars
422                        )));
423                    }
424                }
425            }
426            _ => {
427                let period = pick_part::<String>(2, string, "period")?;
428                match period.as_str() {
429                    "am" | "AM" => Some(ParsedPart {
430                        value: 0,
431                        unit: ParseUnit::Period,
432                    }),
433                    "pm" | "PM" => Some(ParsedPart {
434                        value: 1,
435                        unit: ParseUnit::Period,
436                    }),
437                    _ => {
438                        return Err(create_invalid_format(format!(
439                            "Could not parse '{}' from given string.",
440                            chars
441                        )));
442                    }
443                }
444            }
445        },
446        'b' => match chars.len() {
447            4 => {
448                for (n, period) in ["a.m.", "midnight", "p.m.", "noon"].iter().enumerate() {
449                    if string.starts_with(period) {
450                        // Using unwrap because it's safe to assume that the string is long enough
451                        remove_part(period.len(), string).unwrap();
452                        return Ok(Some(ParsedPart {
453                            value: if n <= 1 { 0 } else { 1 },
454                            unit: ParseUnit::Period,
455                        }));
456                    };
457                }
458                return Err(create_invalid_format(format!(
459                    "Could not parse '{}' from given string.",
460                    chars
461                )));
462            }
463            5 => {
464                for (n, period) in ["a", "mi", "p", "n"].iter().enumerate() {
465                    if string.starts_with(period) {
466                        // Using unwrap because it's safe to assume that the string is long enough
467                        remove_part(period.len(), string).unwrap();
468                        return Ok(Some(ParsedPart {
469                            value: if n <= 1 { 0 } else { 1 },
470                            unit: ParseUnit::Period,
471                        }));
472                    };
473                }
474                return Err(create_invalid_format(format!(
475                    "Could not parse '{}' from given string.",
476                    chars
477                )));
478            }
479            _ => {
480                for (n, period) in ["am", "AM", "midnight", "pm", "PM", "noon"]
481                    .iter()
482                    .enumerate()
483                {
484                    if string.starts_with(period) {
485                        // Using unwrap because it's safe to assume that the string is long enough
486                        remove_part(period.len(), string).unwrap();
487                        return Ok(Some(ParsedPart {
488                            value: if n <= 2 { 0 } else { 1 },
489                            unit: ParseUnit::Period,
490                        }));
491                    };
492                }
493                return Err(create_invalid_format(format!(
494                    "Could not parse '{}' from given string.",
495                    chars
496                )));
497            }
498        },
499        'h' => match chars.len() {
500            1 => match string.chars().nth(1) {
501                Some(char) if char.is_ascii_digit() => {
502                    let hour = pick_part::<u32>(2, string, "hour")?;
503                    Some(ParsedPart {
504                        value: if hour == 12 { 0 } else { hour } as i64,
505                        unit: ParseUnit::PeriodHour,
506                    })
507                }
508                _ => {
509                    let hour = pick_part::<u32>(1, string, "hour")?;
510                    Some(ParsedPart {
511                        // Hour cannot be 12
512                        value: hour as i64,
513                        unit: ParseUnit::PeriodHour,
514                    })
515                }
516            },
517            _ => {
518                let hour = pick_part::<u32>(2, string, "hour")?;
519                Some(ParsedPart {
520                    value: if hour == 12 { 0 } else { hour } as i64,
521                    unit: ParseUnit::PeriodHour,
522                })
523            }
524        },
525        'H' => match chars.len() {
526            1 => match string.chars().nth(1) {
527                Some(char) if char.is_ascii_digit() => {
528                    let hour = pick_part::<u32>(2, string, "hour")?;
529                    Some(ParsedPart {
530                        value: hour as i64,
531                        unit: ParseUnit::Hour,
532                    })
533                }
534                _ => {
535                    let hour = pick_part::<u32>(1, string, "hour")?;
536                    Some(ParsedPart {
537                        value: hour as i64,
538                        unit: ParseUnit::Hour,
539                    })
540                }
541            },
542            _ => {
543                let hour = pick_part::<u32>(2, string, "hour")?;
544                Some(ParsedPart {
545                    value: hour as i64,
546                    unit: ParseUnit::Hour,
547                })
548            }
549        },
550        'K' => match chars.len() {
551            1 => match string.chars().nth(1) {
552                Some(char) if char.is_ascii_digit() => {
553                    let hour = pick_part::<u32>(2, string, "hour")?;
554                    Some(ParsedPart {
555                        value: hour as i64,
556                        unit: ParseUnit::PeriodHour,
557                    })
558                }
559                _ => {
560                    let hour = pick_part::<u32>(1, string, "hour")?;
561                    Some(ParsedPart {
562                        value: hour as i64,
563                        unit: ParseUnit::PeriodHour,
564                    })
565                }
566            },
567            _ => {
568                let hour = pick_part::<u32>(2, string, "hour")?;
569                Some(ParsedPart {
570                    value: hour as i64,
571                    unit: ParseUnit::PeriodHour,
572                })
573            }
574        },
575        'k' => match chars.len() {
576            1 => match string.chars().nth(1) {
577                Some(char) if char.is_ascii_digit() => {
578                    let hour = pick_part::<u32>(2, string, "hour")?;
579                    Some(ParsedPart {
580                        value: if hour == 24 { 0 } else { hour } as i64,
581                        unit: ParseUnit::Hour,
582                    })
583                }
584                _ => {
585                    let hour = pick_part::<u32>(1, string, "hour")?;
586                    Some(ParsedPart {
587                        // Hour cannot be 24
588                        value: hour as i64,
589                        unit: ParseUnit::Hour,
590                    })
591                }
592            },
593            _ => {
594                let hour = pick_part::<u32>(2, string, "hour")?;
595                Some(ParsedPart {
596                    value: if hour == 24 { 0 } else { hour } as i64,
597                    unit: ParseUnit::Hour,
598                })
599            }
600        },
601        'm' => match chars.len() {
602            1 => match string.chars().nth(1) {
603                Some(char) if char.is_ascii_digit() => {
604                    let minute = pick_part::<u32>(2, string, "minute")?;
605                    Some(ParsedPart {
606                        value: minute as i64,
607                        unit: ParseUnit::Minute,
608                    })
609                }
610                _ => {
611                    let minute = pick_part::<u32>(1, string, "minute")?;
612                    Some(ParsedPart {
613                        value: minute as i64,
614                        unit: ParseUnit::Minute,
615                    })
616                }
617            },
618            _ => {
619                let minute = pick_part::<u32>(2, string, "minute")?;
620                Some(ParsedPart {
621                    value: minute as i64,
622                    unit: ParseUnit::Minute,
623                })
624            }
625        },
626        's' => match chars.len() {
627            1 => match string.chars().nth(1) {
628                Some(char) if char.is_ascii_digit() => {
629                    let seconds = pick_part::<u32>(2, string, "seconds")?;
630                    Some(ParsedPart {
631                        value: seconds as i64,
632                        unit: ParseUnit::Second,
633                    })
634                }
635                _ => {
636                    let seconds = pick_part::<u32>(1, string, "seconds")?;
637                    Some(ParsedPart {
638                        value: seconds as i64,
639                        unit: ParseUnit::Second,
640                    })
641                }
642            },
643            _ => {
644                let seconds = pick_part::<u32>(2, string, "seconds")?;
645                Some(ParsedPart {
646                    value: seconds as i64,
647                    unit: ParseUnit::Second,
648                })
649            }
650        },
651        'n' => match chars.len() {
652            1 => {
653                let subsecond = pick_part::<u32>(1, string, "subseconds")?;
654
655                Some(ParsedPart {
656                    value: subsecond as i64,
657                    unit: ParseUnit::Decis,
658                })
659            }
660            2 => {
661                let subsecond = pick_part::<u32>(2, string, "subseconds")?;
662
663                Some(ParsedPart {
664                    value: subsecond as i64,
665                    unit: ParseUnit::Centis,
666                })
667            }
668            4 => {
669                let subsecond = pick_part::<u32>(6, string, "subseconds")?;
670
671                Some(ParsedPart {
672                    value: subsecond as i64,
673                    unit: ParseUnit::Micros,
674                })
675            }
676            5 => {
677                let subsecond = pick_part::<u32>(9, string, "subseconds")?;
678
679                Some(ParsedPart {
680                    value: subsecond as i64,
681                    unit: ParseUnit::Nanos,
682                })
683            }
684            _ => {
685                let subsecond = pick_part::<u32>(3, string, "subseconds")?;
686
687                Some(ParsedPart {
688                    value: subsecond as i64,
689                    unit: ParseUnit::Millis,
690                })
691            }
692        },
693        'X' => parse_zone(chars.len(), string, true)?,
694        'x' => parse_zone(chars.len(), string, false)?,
695        _ => {
696            remove_part(chars.len(), string)?;
697            None
698        }
699    })
700}
701
702/// Parses the month of a date based on https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-month
703fn parse_month(length: usize, string: &mut String) -> Result<Option<ParsedPart>, AstrolabeError> {
704    Ok(match length {
705        1 | 2 => {
706            let month = pick_part::<u32>(length, string, "month")?;
707
708            Some(ParsedPart {
709                value: month as i64,
710                unit: ParseUnit::Month,
711            })
712        }
713        3 => {
714            for (n, month) in MONTH_ABBREVIATED.iter().enumerate() {
715                if string.starts_with(month) {
716                    // Using unwrap because it's safe to assume that the string is long enough
717                    remove_part(month.len(), string).unwrap();
718                    return Ok(Some(ParsedPart {
719                        value: (n + 1) as i64,
720                        unit: ParseUnit::Month,
721                    }));
722                };
723            }
724            return Err(create_invalid_format(
725                "Could not parse month from given string.".to_string(),
726            ));
727        }
728        // Narrow month parsing doesn't work as there are multiple months with the same letter
729        5 => {
730            remove_part(1, string)?;
731            None
732        }
733        _ => {
734            for (n, month) in MONTH_WIDE.iter().enumerate() {
735                if string.starts_with(month) {
736                    // Using unwrap because it's safe to assume that the string is long enough
737                    remove_part(month.len(), string).unwrap();
738                    return Ok(Some(ParsedPart {
739                        value: (n + 1) as i64,
740                        unit: ParseUnit::Month,
741                    }));
742                };
743            }
744            return Err(create_invalid_format(
745                "Could not parse month from given string.".to_string(),
746            ));
747        }
748    })
749}
750
751/// Parses the week day of a date based on https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-month
752fn parse_wday(length: usize, string: &mut String) -> Result<Option<ParsedPart>, AstrolabeError> {
753    Ok(match length {
754        2 | 3 => {
755            remove_part(length, string)?;
756            None
757        }
758        4 => {
759            for wday in WDAY_WIDE {
760                if string.starts_with(wday) {
761                    // Using unwrap because it's safe to assume that the string is long enough
762                    remove_part(wday.len(), string).unwrap();
763                    return Ok(None);
764                };
765            }
766            return Err(create_invalid_format(
767                "Could not parse week day from given string.".to_string(),
768            ));
769        }
770        6 | 8 => {
771            remove_part(2, string)?;
772            None
773        }
774        // 1, 5, 7 and 9+ all consist of 1 char
775        _ => {
776            remove_part(1, string)?;
777            None
778        }
779    })
780}
781
782/// Parses the time zone
783fn parse_zone(
784    length: usize,
785    string: &mut String,
786    with_z: bool,
787) -> Result<Option<ParsedPart>, AstrolabeError> {
788    let prefix = pick_part::<String>(1, string, "timezone prefix")?;
789
790    let multiplier = match prefix.as_str() {
791        "Z" if with_z => {
792            return Ok(Some(ParsedPart {
793                value: 0,
794                unit: ParseUnit::Offset,
795            }));
796        }
797        "+" => 1,
798        "-" => -1,
799        _ => {
800            return Err(create_invalid_format(
801                "Couldn't parse prefix of timezone offset. Prefix has to be either '+' or '-'."
802                    .to_string(),
803            ))
804        }
805    };
806
807    let hour = pick_part::<u32>(2, string, "timezone hour")?;
808
809    Ok(match length {
810        1 => match string.chars().next() {
811            Some(char) if char.is_ascii_digit() => {
812                let minute = pick_part::<u32>(2, string, "timezone minute")?;
813
814                let offset = (hour * SECS_PER_HOUR_U64 as u32 + minute * SECS_PER_MINUTE_U64 as u32)
815                    as i64
816                    * multiplier;
817
818                Some(ParsedPart {
819                    value: offset,
820                    unit: ParseUnit::Offset,
821                })
822            }
823            _ => {
824                let offset = (hour * SECS_PER_HOUR_U64 as u32) as i64 * multiplier;
825
826                Some(ParsedPart {
827                    value: offset,
828                    unit: ParseUnit::Offset,
829                })
830            }
831        },
832        2 => {
833            let minute = pick_part::<u32>(2, string, "timezone minute")?;
834
835            let offset = (hour * SECS_PER_HOUR_U64 as u32 + minute * SECS_PER_MINUTE_U64 as u32)
836                as i64
837                * multiplier;
838
839            Some(ParsedPart {
840                value: offset,
841                unit: ParseUnit::Offset,
842            })
843        }
844        4 => match string.chars().nth(2) {
845            Some(char) if char.is_ascii_digit() => {
846                let minute = pick_part::<u32>(2, string, "timezone minute")?;
847                let second = pick_part::<u32>(2, string, "timezone second")?;
848
849                let offset = (hour * SECS_PER_HOUR_U64 as u32
850                    + minute * SECS_PER_MINUTE_U64 as u32
851                    + second) as i64
852                    * multiplier;
853
854                Some(ParsedPart {
855                    value: offset,
856                    unit: ParseUnit::Offset,
857                })
858            }
859            _ => {
860                let minute = pick_part::<u32>(2, string, "timezone minute")?;
861
862                let offset = (hour * SECS_PER_HOUR_U64 as u32 + minute * SECS_PER_MINUTE_U64 as u32)
863                    as i64
864                    * multiplier;
865
866                Some(ParsedPart {
867                    value: offset,
868                    unit: ParseUnit::Offset,
869                })
870            }
871        },
872        5 => match string.chars().nth(4) {
873            Some(char) if char.is_ascii_digit() => {
874                // Using unwrap because it's safe to assume that the string is long enough
875                remove_part(1, string).unwrap();
876                let minute = pick_part::<u32>(2, string, "timezone minute")?;
877                // Using unwrap because it's safe to assume that the string is long enough
878                remove_part(1, string).unwrap();
879                let second = pick_part::<u32>(2, string, "timezone second")?;
880
881                let offset = (hour * SECS_PER_HOUR_U64 as u32
882                    + minute * SECS_PER_MINUTE_U64 as u32
883                    + second) as i64
884                    * multiplier;
885
886                Some(ParsedPart {
887                    value: offset,
888                    unit: ParseUnit::Offset,
889                })
890            }
891            _ => {
892                remove_part(1, string)?;
893                let minute = pick_part::<u32>(2, string, "timezone minute")?;
894
895                let offset = (hour * SECS_PER_HOUR_U64 as u32 + minute * SECS_PER_MINUTE_U64 as u32)
896                    as i64
897                    * multiplier;
898
899                Some(ParsedPart {
900                    value: offset,
901                    unit: ParseUnit::Offset,
902                })
903            }
904        },
905        _ => {
906            remove_part(1, string)?;
907            let minute = pick_part::<u32>(2, string, "timezone minute")?;
908
909            let offset = (hour * SECS_PER_HOUR_U64 as u32 + minute * SECS_PER_MINUTE_U64 as u32)
910                as i64
911                * multiplier;
912
913            Some(ParsedPart {
914                value: offset,
915                unit: ParseUnit::Offset,
916            })
917        }
918    })
919}
920
921fn remove_part(length: usize, string: &mut String) -> Result<(), AstrolabeError> {
922    if string.chars().count() < length {
923        Err(create_invalid_format(
924            "String to parse is too short. Please check your format string.".to_string(),
925        ))
926    } else {
927        string.replace_range(0..length, "");
928        Ok(())
929    }
930}
931
932fn pick_part<T: std::str::FromStr>(
933    length: usize,
934    string: &mut String,
935    part_name: &str,
936) -> Result<T, AstrolabeError> {
937    if string.chars().count() < length {
938        Err(create_invalid_format(
939            "String to parse is too short. Please check your format string.".to_string(),
940        ))
941    } else {
942        let part = string[0..length].parse::<T>().map_err(|_| {
943            create_invalid_format(format!(
944                "Failed parsing {} from given string. Value is '{}'.",
945                part_name,
946                &string[0..length]
947            ))
948        })?;
949        string.replace_range(0..length, "");
950        Ok(part)
951    }
952}