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

lennart-k / ical-rs / #77

13 Jan 2026 10:50AM UTC coverage: 77.638% (-0.2%) from 77.802%
#77

Pull #2

lennart-k
line parser: Support multi-octet sequences wrapped across lines
Pull Request #2: Major overhaul

930 of 1215 new or added lines in 31 files covered. (76.54%)

32 existing lines in 10 files now uncovered.

1236 of 1592 relevant lines covered (77.64%)

2.63 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::parser::ParserError;
2
use crate::types::{Timezone, Value};
3
use crate::{property::ContentLine, types::CalDateTimeError};
4
use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, Utc};
5
use chrono_tz::Tz;
6
use std::{collections::HashMap, ops::Add};
7

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

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

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

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

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

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

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

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

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

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

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

80
        Ok(Self::parse(prop_value, timezone)?)
3✔
81
    }
82

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

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

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

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

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

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

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

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

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

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

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

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