• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lennart-k / caldata-rs / #9

31 Jan 2026 12:45PM UTC coverage: 81.981% (+3.7%) from 78.261%
#9

push

lennart-k
fix doctests

2789 of 3402 relevant lines covered (81.98%)

2.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.82
/src/types/datetime.rs
1
use crate::parser::{ContentLine, ParserError};
2
use crate::types::CalDateTimeError;
3
use crate::types::{Tz, Value};
4
use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, Utc};
5
use std::{collections::HashMap, ops::Add};
6

7
const LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S";
8
const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ";
9

10
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
11
// Form 1, example: 19980118T230000 -> Local
12
// Form 2, example: 19980119T070000Z -> UTC
13
// Form 3, example: TZID=America/New_York:19980119T020000 -> Olson
14
// https://en.wikipedia.org/wiki/Tz_database
15
pub struct CalDateTime(pub(crate) DateTime<Tz>);
16

17
impl From<DateTime<Tz>> for CalDateTime {
18
    fn from(value: DateTime<Tz>) -> Self {
2✔
19
        Self(value)
2✔
20
    }
21
}
22

23
impl From<DateTime<Local>> for CalDateTime {
24
    fn from(value: DateTime<Local>) -> Self {
×
25
        Self(value.with_timezone(&Tz::Local))
×
26
    }
27
}
28

29
impl From<DateTime<Utc>> for CalDateTime {
30
    fn from(value: DateTime<Utc>) -> Self {
2✔
31
        Self(value.with_timezone(&Tz::Olson(chrono_tz::UTC)))
2✔
32
    }
33
}
34

35
impl Add<Duration> for CalDateTime {
36
    type Output = Self;
37

38
    fn add(self, duration: Duration) -> Self::Output {
1✔
39
        Self(self.0 + duration)
1✔
40
    }
41
}
42

43
impl CalDateTime {
44
    pub fn parse_prop(
4✔
45
        prop: &ContentLine,
46
        timezones: Option<&HashMap<String, Option<chrono_tz::Tz>>>,
47
    ) -> Result<Self, ParserError> {
48
        let timezone = if let Some(tzid) = prop.params.get_tzid() {
4✔
49
            if let Some(timezone) = timezones.and_then(|timezones| timezones.get(tzid)) {
8✔
50
                timezone.to_owned()
2✔
51
            } else {
52
                // TZID refers to timezone that does not exist
53
                return Err(CalDateTimeError::InvalidTZID(tzid.to_string()).into());
2✔
54
            }
55
        } else {
56
            // No explicit timezone specified.
57
            // This is valid and will be localtime or UTC depending on the value
58
            // We will stick to this default as documented in https://github.com/lennart-k/rustical/issues/102
59
            None
4✔
60
        };
61

62
        Ok(Self::parse(&prop.value, timezone)?)
6✔
63
    }
64

65
    #[must_use]
66
    pub fn format(&self) -> String {
2✔
67
        match self.timezone() {
2✔
68
            Tz::Olson(chrono_tz::UTC) => self.0.format(UTC_DATE_TIME).to_string(),
2✔
69
            _ => self.0.format(LOCAL_DATE_TIME).to_string(),
1✔
70
        }
71
    }
72

73
    pub fn parse(value: &str, timezone: Option<chrono_tz::Tz>) -> Result<Self, CalDateTimeError> {
2✔
74
        let utc = value.ends_with('Z');
6✔
75
        // Remove Z suffix
76
        // Stripping the suffix manually and only running parse_from_str improves worst-case
77
        // performance by around 40%
78
        let value = value.rsplit_once('Z').map(|(v, _)| v).unwrap_or(value);
4✔
79

80
        let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) else {
6✔
81
            return Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()));
1✔
82
        };
83

84
        if utc {
11✔
85
            Ok(datetime.and_utc().into())
2✔
86
        } else {
87
            if let Some(timezone) = timezone {
6✔
88
                return Ok(Self(
1✔
89
                    datetime
1✔
90
                        .and_local_timezone(timezone.into())
1✔
91
                        .earliest()
1✔
92
                        .ok_or(CalDateTimeError::LocalTimeGap)?,
1✔
93
                ));
94
            }
95
            Ok(Self(
3✔
96
                datetime
3✔
97
                    .and_local_timezone(Tz::Local)
6✔
98
                    .earliest()
3✔
99
                    .ok_or(CalDateTimeError::LocalTimeGap)?,
4✔
100
            ))
101
        }
102
    }
103

104
    #[must_use]
105
    pub fn utc(&self) -> DateTime<Utc> {
2✔
106
        self.0.to_utc()
3✔
107
    }
108

109
    #[must_use]
110
    pub fn timezone(&self) -> Tz {
2✔
111
        self.0.timezone()
2✔
112
    }
113

114
    #[must_use]
115
    pub fn date_floor(&self) -> NaiveDate {
×
116
        self.0.date_naive()
×
117
    }
118
    #[must_use]
119
    pub fn date_ceil(&self) -> NaiveDate {
×
120
        let date = self.0.date_naive();
×
121
        date.succ_opt().unwrap_or(date)
×
122
    }
123
}
124

125
impl From<CalDateTime> for DateTime<Utc> {
126
    fn from(value: CalDateTime) -> Self {
×
127
        value.utc()
×
128
    }
129
}
130

131
#[cfg(not(tarpaulin_include))]
132
impl Datelike for CalDateTime {
133
    fn year(&self) -> i32 {
134
        self.0.year()
135
    }
136
    fn month(&self) -> u32 {
137
        self.0.month()
138
    }
139

140
    fn month0(&self) -> u32 {
141
        self.0.month0()
142
    }
143
    fn day(&self) -> u32 {
144
        self.0.day()
145
    }
146
    fn day0(&self) -> u32 {
147
        self.0.day0()
148
    }
149
    fn ordinal(&self) -> u32 {
150
        self.0.ordinal()
151
    }
152
    fn ordinal0(&self) -> u32 {
153
        self.0.ordinal0()
154
    }
155
    fn weekday(&self) -> chrono::Weekday {
156
        self.0.weekday()
157
    }
158
    fn iso_week(&self) -> chrono::IsoWeek {
159
        self.0.iso_week()
160
    }
161
    fn with_year(&self, year: i32) -> Option<Self> {
162
        Some(Self(self.0.with_year(year)?))
163
    }
164
    fn with_month(&self, month: u32) -> Option<Self> {
165
        Some(Self(self.0.with_month(month)?))
166
    }
167
    fn with_month0(&self, month0: u32) -> Option<Self> {
168
        Some(Self(self.0.with_month0(month0)?))
169
    }
170
    fn with_day(&self, day: u32) -> Option<Self> {
171
        Some(Self(self.0.with_day(day)?))
172
    }
173
    fn with_day0(&self, day0: u32) -> Option<Self> {
174
        Some(Self(self.0.with_day0(day0)?))
175
    }
176
    fn with_ordinal(&self, ordinal: u32) -> Option<Self> {
177
        Some(Self(self.0.with_ordinal(ordinal)?))
178
    }
179
    fn with_ordinal0(&self, ordinal0: u32) -> Option<Self> {
180
        Some(Self(self.0.with_ordinal0(ordinal0)?))
181
    }
182
}
183

184
impl Value for CalDateTime {
185
    fn value_type(&self) -> Option<&'static str> {
2✔
186
        Some("DATE-TIME")
187
    }
188
    fn value(&self) -> String {
2✔
189
        self.format()
2✔
190
    }
191

192
    fn utc_or_local(self) -> Self {
1✔
193
        match self.timezone() {
1✔
194
            Tz::Local => self.clone(),
×
195
            Tz::Olson(_) => Self(self.0.with_timezone(&Tz::utc())),
1✔
196
        }
197
    }
198
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc