• 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

82.5
/src/types/vcard/partial_date.rs
1
use crate::{
2
    parser::{ParseProp, ParserError},
3
    types::Value,
4
};
5
use chrono::{Datelike, NaiveDate};
6
use std::{collections::HashMap, sync::OnceLock};
7

8
static RE_DATE: OnceLock<[regex::Regex; 4]> = OnceLock::new();
9

10
#[inline]
11
fn re_date() -> &'static [regex::Regex] {
3✔
12
    RE_DATE.get_or_init(|| {
6✔
13
        [
14
            // Reduced precision basic notation
15
            regex::Regex::new(r"^(?<year>\d{4})(((?<month>\d{2})(?<day>\d{2}))?)?$").unwrap(),
2✔
16
            // Reduced precision notation notation
17
            regex::Regex::new(r"^(?<year>\d{4})(((?:-(?<month>\d{2}))(?:-(?<day>\d{2}))?)?)?$")
2✔
18
                .unwrap(),
2✔
19
            // Truncated basic notation
20
            regex::Regex::new(r"^-(?:(?<year>\d{4})|-)((?:(?<month>\d{2})|-)(?<day>\d{2})?)?$")
2✔
21
                .unwrap(),
2✔
22
            // Truncated extended notation
23
            regex::Regex::new(r"^(?:(?<year>\d{4})|-)-(?<month>\d{2})-(?<day>\d{2})$").unwrap(),
4✔
24
        ]
25
    })
26
}
27

28
#[derive(Debug, Clone, PartialEq, Eq, Default)]
29
pub struct PartialDate {
30
    pub(crate) year: Option<i32>,
31
    pub(crate) month: Option<u32>,
32
    pub(crate) day: Option<u32>,
33
}
34

35
impl From<NaiveDate> for PartialDate {
NEW
36
    fn from(value: NaiveDate) -> Self {
×
37
        Self {
NEW
38
            year: Some(value.year()),
×
NEW
39
            month: Some(value.month()),
×
NEW
40
            day: Some(value.day()),
×
41
        }
42
    }
43
}
44

45
impl PartialDate {
46
    pub fn parse(value: &str) -> Result<Self, ParserError> {
2✔
47
        // A calendar date as specified in [ISO.8601.2004], Section 4.1.2.
48
        // https://dotat.at/tmp/ISO_8601-2004_E.pdf
49
        // Basic format: YYYYMMDD Example: 19850412
50
        // Extended format: YYYY-MM-DD Example: 1985-04-12
51
        // Reduced accuracy, as specified in [ISO.8601.2004], Sections 4.1.2.3
52
        // a) and b), but not c), is permitted.
53
        //
54
        // Expanded representation, as specified in [ISO.8601.2004], Section
55
        // 4.1.4, is forbidden.
56
        //
57
        // Truncated representation, as specified in [ISO.8601.2000], Sections
58
        // 5.2.1.3 d), e), and f), is permitted.
59
        // d) A specific day of a month in the implied year
60
        // Basic format: --MMDD EXAMPLE --0412
61
        // Extended format: --MM-DD EXAMPLE --04-12
62
        // e) A specific month in the implied year
63
        // Basic format: --MM EXAMPLE --04
64
        // Extended format: not applicable
65
        // f) A specific day in the implied month
66
        // Basic format: ---DD EXAMPLE ---12
67
        // Extended format: not applicable
68
        if let Some(captures) = re_date().iter().find_map(|pattern| pattern.captures(value)) {
12✔
69
            let year = captures.name("year").map(|y| y.as_str().parse().unwrap());
20✔
70
            let month = captures.name("month").map(|m| m.as_str().parse().unwrap());
13✔
71
            let day = captures.name("day").map(|d| d.as_str().parse().unwrap());
8✔
72
            if let Some(month) = month
2✔
73
                && month > 12
2✔
74
            {
75
                return Err(ParserError::InvalidPropertyValue(value.to_owned()));
1✔
76
            }
77
            if let Some(day) = day
5✔
78
                && day > 31
3✔
79
            {
80
                return Err(ParserError::InvalidPropertyValue(value.to_owned()));
1✔
81
            }
82
            return Ok(Self { year, month, day });
3✔
83
        }
84
        Err(ParserError::InvalidPropertyValue(value.to_owned()))
1✔
85
    }
86
}
87

88
impl Value for PartialDate {
NEW
89
    fn value_type(&self) -> Option<&'static str> {
×
90
        Some("DATE")
91
    }
92

93
    fn value(&self) -> String {
2✔
94
        if let Some(year) = &self.year {
2✔
95
            assert!(!(self.month.is_none() && self.day.is_some()));
5✔
96
            let month = self
3✔
97
                .month
98
                .map(|month| format!("-{month:02}"))
10✔
99
                .unwrap_or_default();
100
            let day = self.day.map(|day| format!("-{day:02}")).unwrap_or_default();
6✔
101
            format!("{year:04}{month}{day}")
4✔
102
        } else {
103
            let month = self
5✔
104
                .month
105
                .map(|month| format!("{month:02}"))
8✔
106
                .unwrap_or("-".to_owned());
5✔
107
            let day = self.day.map(|day| format!("{day:02}")).unwrap_or_default();
11✔
108
            format!("--{month}{day}")
5✔
109
        }
110
    }
111
}
112

113
impl ParseProp for PartialDate {
NEW
114
    fn parse_prop(
×
115
        prop: &crate::property::ContentLine,
116
        _timezones: Option<&HashMap<String, Option<chrono_tz::Tz>>>,
117
        _default_type: &str,
118
    ) -> Result<Self, ParserError> {
NEW
119
        Self::parse(prop.value.as_deref().unwrap_or_default())
×
120
    }
121
}
122

123
#[cfg(test)]
124
mod tests {
125
    use crate::types::{PartialDate, Value};
126
    use rstest::rstest;
127

128
    #[rstest]
129
    // DATE
130
    #[case("19850412", PartialDate{year: Some(1985), month: Some(4), day: Some(12)})]
131
    #[case("1985-04-12", PartialDate{year: Some(1985), month: Some(4), day: Some(12)})]
132
    #[case("1985-04", PartialDate{year: Some(1985), month: Some(4), ..Default::default()})]
133
    #[case("1985", PartialDate{year: Some(1985), ..Default::default()})]
134
    #[case("--0412", PartialDate{month: Some(4), day: Some(12), ..Default::default()})]
135
    #[case("--04-12", PartialDate{month: Some(4), day: Some(12), ..Default::default()})]
136
    #[case("--04", PartialDate{month: Some(4), ..Default::default()})]
137
    #[case("---12", PartialDate{day: Some(12), ..Default::default()})]
138
    fn test_parse_date(#[case] input: &str, #[case] value: PartialDate) {
139
        let parsed = PartialDate::parse(input).unwrap();
140
        assert!(!parsed.value().ends_with('-'));
141
        let roundtrip = PartialDate::parse(&parsed.value()).unwrap();
142
        assert_eq!(parsed, value);
143
        assert_eq!(roundtrip, value);
144
    }
145

146
    #[rstest]
147
    #[case("19850432")]
148
    #[case("19851422")]
149
    #[case("198514222")]
150
    fn test_parse_date_invalid(#[case] input: &str) {
151
        assert!(PartialDate::parse(input).is_err());
152
    }
153
}
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