• 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

93.1
/src/types/duration.rs
1
use crate::{property::ContentLine, types::Value};
2
use chrono::Duration;
3
use lazy_static::lazy_static;
4

5
lazy_static! {
6
    static ref RE_DURATION: regex::Regex = regex::Regex::new(
1✔
7
        r"(?x)
8
        ^(?<sign>[+-])?
9
        P (
10
            (
11
                ((?P<D>\d+)D)?  # days
12
                (
13
                    T
14
                    ((?P<H>\d+)H)?
15
                    ((?P<M>\d+)M)?
16
                    ((?P<S>\d+)S)?
17
                )?
18
            )  # dur-date,dur-time
19
            | (
20
                ((?P<W>\d+)W)?
21
            )  # dur-week
22
        )
23
        $"
24
    )
25
    .unwrap();
1✔
26
}
27

28
impl TryFrom<&ContentLine> for Option<chrono::Duration> {
29
    type Error = InvalidDuration;
30

NEW
31
    fn try_from(value: &ContentLine) -> Result<Self, Self::Error> {
×
UNCOV
32
        if let Some(value) = &value.value {
×
UNCOV
33
            Ok(Some(parse_duration(value)?))
×
34
        } else {
35
            Ok(None)
×
36
        }
37
    }
38
}
39

40
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
41
#[error("Invalid duration: {0}")]
42
pub struct InvalidDuration(String);
43

44
pub fn parse_duration(string: &str) -> Result<Duration, InvalidDuration> {
2✔
45
    let captures = RE_DURATION
7✔
46
        .captures(string)
2✔
47
        .ok_or(InvalidDuration(string.to_owned()))?;
5✔
48

49
    let mut duration = Duration::zero();
4✔
50
    if let Some(weeks) = captures.name("W") {
2✔
51
        duration += Duration::weeks(weeks.as_str().parse().unwrap());
2✔
52
    }
53
    if let Some(days) = captures.name("D") {
4✔
54
        duration += Duration::days(days.as_str().parse().unwrap());
2✔
55
    }
56
    if let Some(hours) = captures.name("H") {
4✔
57
        duration += Duration::hours(hours.as_str().parse().unwrap());
2✔
58
    }
59
    if let Some(minutes) = captures.name("M") {
2✔
60
        duration += Duration::minutes(minutes.as_str().parse().unwrap());
2✔
61
    }
62
    if let Some(seconds) = captures.name("S") {
2✔
63
        duration += Duration::seconds(seconds.as_str().parse().unwrap());
2✔
64
    }
65
    if let Some(sign) = captures.name("sign")
5✔
66
        && sign.as_str() == "-"
2✔
67
    {
68
        duration = -duration;
1✔
69
    }
70

71
    Ok(duration)
2✔
72
}
73

74
impl Value for Duration {
75
    fn value_type(&self) -> Option<&'static str> {
3✔
76
        Some("DURATION")
77
    }
78

79
    fn value(&self) -> String {
3✔
80
        if self.is_zero() {
3✔
81
            return "PT0S".to_owned();
1✔
82
        }
83
        let mut abs_duration = self.abs();
3✔
84
        let mut out = String::new();
3✔
85
        if self < &abs_duration {
6✔
86
            out.push('-');
1✔
87
        }
88
        out.push('P');
3✔
89

90
        // Return weeks if duration can be expressed exactly by number of weeks
91
        let weeks = abs_duration.num_weeks();
3✔
92
        if weeks > 0 && abs_duration == Duration::weeks(weeks) {
4✔
93
            out.push_str(&format!("{weeks}W"));
2✔
94
            return out;
1✔
95
        }
96

97
        let days = abs_duration.num_days();
6✔
98
        if days > 0 {
3✔
99
            out.push_str(&format!("{days}D"));
1✔
100
            abs_duration -= Duration::days(days);
1✔
101
        }
102
        if abs_duration.is_zero() {
6✔
103
            return out;
1✔
104
        }
105

106
        out.push('T');
3✔
107

108
        let hours = abs_duration.num_hours();
3✔
109
        if hours > 0 {
3✔
110
            out.push_str(&format!("{hours}H"));
2✔
111
            abs_duration -= Duration::hours(hours);
2✔
112
        }
113
        let minutes = abs_duration.num_minutes();
4✔
114
        if minutes > 0 {
3✔
115
            out.push_str(&format!("{minutes}M"));
2✔
116
            abs_duration -= Duration::minutes(minutes);
2✔
117
        }
118
        let seconds = abs_duration.num_seconds();
4✔
119
        if seconds > 0 {
3✔
120
            out.push_str(&format!("{seconds}S"));
1✔
121
            abs_duration -= Duration::seconds(seconds);
2✔
122
        }
123

124
        out
3✔
125
    }
126
}
127

128
#[cfg(test)]
129
mod tests {
130
    use crate::types::Value;
131

132
    use super::parse_duration;
133
    use chrono::Duration;
134
    use rstest::rstest;
135

136
    #[test]
137
    fn test_parse_duration() {
138
        assert!(parse_duration("P1D12W").is_err());
139
        assert!(parse_duration("P1W12D").is_err());
140
        assert!(parse_duration("PT10S12M").is_err());
141
        // This should yield an error but it's easier to just let it slip through as 0s
142
        assert_eq!(parse_duration("P").unwrap(), Duration::zero());
143
    }
144

145
    #[rstest]
146
    #[case("P12W", Duration::weeks(12))]
147
    #[case("-P12W", -Duration::weeks(12))]
148
    #[case("P12D", Duration::days(12))]
149
    #[case("PT12H", Duration::hours(12))]
150
    #[case("PT12M", Duration::minutes(12))]
151
    #[case("PT12S", Duration::seconds(12))]
152
    #[case("-PT12S", -Duration::seconds(12))]
153
    #[case("P2DT10M12S",
154
            Duration::days(2) + Duration::minutes(10) + Duration::seconds(12))]
155
    #[case(
156
            "PT10M12S",
157
            Duration::minutes(10) + Duration::seconds(12)
158
    )]
159
    // On a roundtrip, P should not be serialised to P
160
    #[case("PT0S", Duration::zero())]
161
    fn test_duration_roundtrip(#[case] value: &str, #[case] ref_duration: Duration) {
162
        let duration = parse_duration(value).unwrap();
163
        assert_eq!(duration, ref_duration);
164
        assert_eq!(duration.value(), value);
165
    }
166
}
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