1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

//! A collection of temporary structs and utilities to input data for tests, benchmarks,
//! and examples.

use core::str::FromStr;
use either::Either;
use icu_calendar::{DateTime, DateTimeError, Gregorian};
use icu_timezone::{CustomTimeZone, TimeZoneError};

/// Temporary function for parsing a `DateTime<Gregorian>`
///
/// This utility is for easily creating dates, not a complete robust solution. The
/// string must take a specific form of the ISO-8601 format: `YYYY-MM-DDThh:mm:ss`.
///
/// ```
/// use icu::datetime::mock::parse_gregorian_from_str;
/// use icu_calendar::{DateTime, Gregorian};
///
/// let date: DateTime<Gregorian> =
///     parse_gregorian_from_str("2020-10-14T13:21:00").expect("Failed to parse a datetime.");
/// ```
///
/// Optionally, fractional seconds can be specified: `YYYY-MM-DDThh:mm:ss.SSS`.
///
/// ```
/// use icu::datetime::mock::parse_gregorian_from_str;
/// use icu_calendar::{DateTime, Gregorian};
///
/// let date: DateTime<Gregorian> =
///     parse_gregorian_from_str("2020-10-14T13:21:00.101").expect("Failed to parse a datetime.");
/// assert_eq!(u32::from(date.time.nanosecond), 101_000_000);
/// ```
pub fn parse_gregorian_from_str(input: &str) -> Result<DateTime<Gregorian>, DateTimeError> {
    #[allow(clippy::indexing_slicing)] // TODO(#1668) Clippy exceptions need docs or fixing.
    let year: i32 = input[0..4].parse()?;
    #[allow(clippy::indexing_slicing)] // TODO(#1668) Clippy exceptions need docs or fixing.
    let month: u8 = input[5..7].parse()?;
    #[allow(clippy::indexing_slicing)] // TODO(#1668) Clippy exceptions need docs or fixing.
    let day: u8 = input[8..10].parse()?;
    #[allow(clippy::indexing_slicing)] // TODO(#1668) Clippy exceptions need docs or fixing.
    let hour: u8 = input[11..13].parse()?;
    #[allow(clippy::indexing_slicing)] // TODO(#1668) Clippy exceptions need docs or fixing.
    let minute: u8 = input[14..16].parse()?;
    #[allow(clippy::indexing_slicing)] // TODO(#1668) Clippy exceptions need docs or fixing.
    let second: u8 = input[17..19].parse()?;
    #[allow(clippy::indexing_slicing)] // TODO(#1668) Clippy exceptions need docs or fixing.
    let fraction: u32 = if input.len() > 20 {
        // Extract fractional input and trim any trailing zeros
        let mut fraction = input[20..].trim_end_matches('0');
        // Truncate to at most 9 digits
        if fraction.len() > 9 {
            fraction = &fraction[0..9];
        }
        let as_int: u32 = fraction.parse().unwrap_or(0);
        // Convert fraction to nanoseconds
        as_int * (10u32.pow(9 - fraction.len() as u32))
    } else {
        0
    };
    let mut datetime = DateTime::new_gregorian_datetime(year, month, day, hour, minute, second)?;
    datetime.time = icu_calendar::types::Time::try_new(hour, minute, second, fraction)?;

    Ok(datetime)
}

/// Parse a [`DateTime`] and [`CustomTimeZone`] from a string.
///
/// This utility is for easily creating dates, not a complete robust solution. The
/// string must take a specific form of the ISO 8601 format:
/// `YYYY-MM-DDThh:mm:ssZ`,
/// `YYYY-MM-DDThh:mm:ss±hh`,
/// `YYYY-MM-DDThh:mm:ss±hhmm`,
/// `YYYY-MM-DDThh:mm:ss±hh:mm`,
///
/// # Examples
///
/// ```
/// use icu::datetime::mock;
///
/// let (date, zone) = mock::parse_zoned_gregorian_from_str("2020-10-14T13:21:00+05:30")
///     .expect("Failed to parse a zoned datetime.");
/// ```
pub fn parse_zoned_gregorian_from_str(
    input: &str,
) -> Result<(DateTime<Gregorian>, CustomTimeZone), Either<DateTimeError, TimeZoneError>> {
    let datetime = parse_gregorian_from_str(input).map_err(Either::Left)?;
    let time_zone = match input
        .rfind(|c| c == '+' || /* ASCII */ c == '-' || /* U+2212 */ c == '−' || c == 'Z')
    {
        #[allow(clippy::indexing_slicing)] // TODO(#1668) Clippy exceptions need docs or fixing.
        Some(index) => FromStr::from_str(&input[index..]).map_err(Either::Right)?,
        None => return Err(Either::Right(TimeZoneError::InvalidOffset)),
    };

    Ok((datetime, time_zone))
}

#[test]
fn test_parsing_fractional_seconds() {
    use icu::datetime::mock::parse_gregorian_from_str;
    use icu_calendar::{DateTime, Gregorian};

    // Milliseconds
    let date: DateTime<Gregorian> =
        parse_gregorian_from_str("2020-10-14T13:21:00.123").expect("Failed to parse a datetime.");
    assert_eq!(u32::from(date.time.nanosecond), 123_000_000);

    // All zeros
    let date: DateTime<Gregorian> =
        parse_gregorian_from_str("2020-10-14T13:21:00.000").expect("Failed to parse a datetime.");
    assert_eq!(u32::from(date.time.nanosecond), 0);

    // Leading zeros
    let date: DateTime<Gregorian> = parse_gregorian_from_str("2020-10-14T13:21:00.000123")
        .expect("Failed to parse a datetime.");
    assert_eq!(u32::from(date.time.nanosecond), 123_000);

    // Trailing zeros
    let date: DateTime<Gregorian> = parse_gregorian_from_str("2020-10-14T13:21:00.123000000000000")
        .expect("Failed to parse a datetime.");
    assert_eq!(u32::from(date.time.nanosecond), 123_000_000);

    // Too much precision, should truncate to nanoseconds
    let date: DateTime<Gregorian> = parse_gregorian_from_str("2020-10-14T13:21:00.123456789999999")
        .expect("Failed to parse a datetime.");
    assert_eq!(u32::from(date.time.nanosecond), 123_456_789);
}