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

lennart-k / ical-rs / #73

12 Jan 2026 01:50PM UTC coverage: 77.771% (-0.03%) from 77.802%
#73

Pull #2

lennart-k
IcalCalendarObjectBuilder: Make attributes public
Pull Request #2: Major overhaul

908 of 1184 new or added lines in 30 files covered. (76.69%)

32 existing lines in 10 files now uncovered.

1221 of 1570 relevant lines covered (77.77%)

2.77 hits per line

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

78.69
/src/types/datetime.rs
1
use crate::types::{Timezone, Value};
2
use crate::{property::ContentLine, types::CalDateTimeError};
3
use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, Utc};
4
use chrono_tz::Tz;
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<Timezone>);
16

17
impl From<CalDateTime> for DateTime<rrule::Tz> {
NEW
18
    fn from(value: CalDateTime) -> Self {
×
NEW
19
        value.0.with_timezone(&value.timezone().into())
×
20
    }
21
}
22

23
impl From<DateTime<rrule::Tz>> for CalDateTime {
24
    fn from(value: DateTime<rrule::Tz>) -> Self {
1✔
25
        Self(value.with_timezone(&value.timezone().into()))
1✔
26
    }
27
}
28

29
impl From<DateTime<Timezone>> for CalDateTime {
30
    fn from(value: DateTime<Timezone>) -> Self {
1✔
31
        Self(value)
1✔
32
    }
33
}
34

35
impl From<DateTime<Local>> for CalDateTime {
NEW
36
    fn from(value: DateTime<Local>) -> Self {
×
NEW
37
        Self(value.with_timezone(&Timezone::Local))
×
38
    }
39
}
40

41
impl From<DateTime<Utc>> for CalDateTime {
42
    fn from(value: DateTime<Utc>) -> Self {
3✔
43
        Self(value.with_timezone(&Timezone::Olson(chrono_tz::UTC)))
3✔
44
    }
45
}
46

47
impl Add<Duration> for CalDateTime {
48
    type Output = Self;
49

50
    fn add(self, duration: Duration) -> Self::Output {
1✔
51
        Self(self.0 + duration)
1✔
52
    }
53
}
54

55
impl CalDateTime {
56
    pub fn parse_prop(
3✔
57
        prop: &ContentLine,
58
        timezones: Option<&HashMap<String, Option<chrono_tz::Tz>>>,
59
    ) -> Result<Self, CalDateTimeError> {
60
        let prop_value = prop
7✔
61
            .value
62
            .as_ref()
63
            .ok_or_else(|| CalDateTimeError::InvalidDatetimeFormat("empty property".into()))?;
3✔
64

65
        let timezone = if let Some(tzid) = prop.params.get_tzid() {
5✔
66
            if let Some(timezone) = timezones.and_then(|timezones| timezones.get(tzid)) {
17✔
67
                timezone.to_owned()
4✔
68
            } else {
69
                // TZID refers to timezone that does not exist
NEW
70
                return Err(CalDateTimeError::InvalidTZID(tzid.to_string()));
×
71
            }
72
        } else {
73
            // No explicit timezone specified.
74
            // This is valid and will be localtime or UTC depending on the value
75
            // We will stick to this default as documented in https://github.com/lennart-k/rustical/issues/102
76
            None
3✔
77
        };
78

79
        Self::parse(prop_value, timezone)
2✔
80
    }
81

82
    #[must_use]
83
    pub fn format(&self) -> String {
2✔
84
        match self.timezone() {
4✔
85
            Timezone::Olson(chrono_tz::UTC) => self.0.format(UTC_DATE_TIME).to_string(),
3✔
86
            _ => self.0.format(LOCAL_DATE_TIME).to_string(),
1✔
87
        }
88
    }
89

90
    pub fn parse(value: &str, timezone: Option<Tz>) -> Result<Self, CalDateTimeError> {
2✔
91
        let utc = value.ends_with('Z');
2✔
92
        // Remove Z suffix
93
        // Stripping the suffix manually and only running parse_from_str improves worst-case
94
        // performance by around 40%
95
        let value = value.rsplit_once('Z').map(|(v, _)| v).unwrap_or(value);
4✔
96

97
        let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) else {
4✔
98
            return Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()));
1✔
99
        };
100

101
        if utc {
7✔
102
            Ok(datetime.and_utc().into())
3✔
103
        } else {
104
            if let Some(timezone) = timezone {
4✔
105
                return Ok(Self(
1✔
106
                    datetime
2✔
107
                        .and_local_timezone(timezone.into())
3✔
108
                        .earliest()
3✔
109
                        .ok_or(CalDateTimeError::LocalTimeGap)?,
3✔
110
                ));
111
            }
112
            Ok(Self(
1✔
113
                datetime
1✔
114
                    .and_local_timezone(Timezone::Local)
1✔
115
                    .earliest()
1✔
116
                    .ok_or(CalDateTimeError::LocalTimeGap)?,
1✔
117
            ))
118
        }
119
    }
120

121
    #[must_use]
122
    pub fn utc(&self) -> DateTime<Utc> {
3✔
123
        self.0.to_utc()
3✔
124
    }
125

126
    #[must_use]
127
    pub fn timezone(&self) -> Timezone {
2✔
128
        self.0.timezone()
4✔
129
    }
130

131
    #[must_use]
NEW
132
    pub fn date_floor(&self) -> NaiveDate {
×
NEW
133
        self.0.date_naive()
×
134
    }
135
    #[must_use]
NEW
136
    pub fn date_ceil(&self) -> NaiveDate {
×
NEW
137
        let date = self.0.date_naive();
×
NEW
138
        date.succ_opt().unwrap_or(date)
×
139
    }
140
}
141

142
impl From<CalDateTime> for DateTime<Utc> {
NEW
143
    fn from(value: CalDateTime) -> Self {
×
NEW
144
        value.utc()
×
145
    }
146
}
147

148
#[cfg(not(tarpaulin_include))]
149
impl Datelike for CalDateTime {
150
    fn year(&self) -> i32 {
151
        self.0.year()
152
    }
153
    fn month(&self) -> u32 {
154
        self.0.month()
155
    }
156

157
    fn month0(&self) -> u32 {
158
        self.0.month0()
159
    }
160
    fn day(&self) -> u32 {
161
        self.0.day()
162
    }
163
    fn day0(&self) -> u32 {
164
        self.0.day0()
165
    }
166
    fn ordinal(&self) -> u32 {
167
        self.0.ordinal()
168
    }
169
    fn ordinal0(&self) -> u32 {
170
        self.0.ordinal0()
171
    }
172
    fn weekday(&self) -> chrono::Weekday {
173
        self.0.weekday()
174
    }
175
    fn iso_week(&self) -> chrono::IsoWeek {
176
        self.0.iso_week()
177
    }
178
    fn with_year(&self, year: i32) -> Option<Self> {
179
        Some(Self(self.0.with_year(year)?))
180
    }
181
    fn with_month(&self, month: u32) -> Option<Self> {
182
        Some(Self(self.0.with_month(month)?))
183
    }
184
    fn with_month0(&self, month0: u32) -> Option<Self> {
185
        Some(Self(self.0.with_month0(month0)?))
186
    }
187
    fn with_day(&self, day: u32) -> Option<Self> {
188
        Some(Self(self.0.with_day(day)?))
189
    }
190
    fn with_day0(&self, day0: u32) -> Option<Self> {
191
        Some(Self(self.0.with_day0(day0)?))
192
    }
193
    fn with_ordinal(&self, ordinal: u32) -> Option<Self> {
194
        Some(Self(self.0.with_ordinal(ordinal)?))
195
    }
196
    fn with_ordinal0(&self, ordinal0: u32) -> Option<Self> {
197
        Some(Self(self.0.with_ordinal0(ordinal0)?))
198
    }
199
}
200

201
impl Value for CalDateTime {
202
    fn value_type(&self) -> Option<&'static str> {
4✔
203
        Some("DATE-TIME")
204
    }
205
    fn value(&self) -> String {
2✔
206
        self.format()
4✔
207
    }
208

209
    fn utc_or_local(self) -> Self {
1✔
210
        match self.timezone() {
1✔
NEW
211
            Timezone::Local => self.clone(),
×
212
            Timezone::Olson(_) => Self(self.0.with_timezone(&Timezone::utc())),
1✔
213
        }
214
    }
215
}
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