From 6e0129130e4dd0add2323c5dbb2b38f6de9e08d8 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:03:59 +0200 Subject: [PATCH] Fix birthdays without year in birthday calendar Fixes #79 --- crates/ical/src/address_object.rs | 112 ++++++++++++++++-------------- crates/ical/src/timestamp.rs | 22 ++++-- 2 files changed, 76 insertions(+), 58 deletions(-) diff --git a/crates/ical/src/address_object.rs b/crates/ical/src/address_object.rs index 7e28888..64ee409 100644 --- a/crates/ical/src/address_object.rs +++ b/crates/ical/src/address_object.rs @@ -62,14 +62,14 @@ impl AddressObject { &self.vcf } - pub fn get_anniversary(&self) -> Option { - let prop = self.vcard.get_property("ANNIVERSARY")?; - CalDateTime::parse_prop(prop, &HashMap::default()).ok() + pub fn get_anniversary(&self) -> Option<(CalDateTime, bool)> { + let prop = self.vcard.get_property("ANNIVERSARY")?.value.as_deref()?; + CalDateTime::parse_vcard(prop).ok() } - pub fn get_birthday(&self) -> Option { - let prop = self.vcard.get_property("BDAY")?; - CalDateTime::parse_prop(prop, &HashMap::default()).ok() + pub fn get_birthday(&self) -> Option<(CalDateTime, bool)> { + let prop = self.vcard.get_property("BDAY")?.value.as_deref()?; + CalDateTime::parse_vcard(prop).ok() } pub fn get_full_name(&self) -> Option<&str> { @@ -78,25 +78,27 @@ impl AddressObject { } pub fn get_anniversary_object(&self) -> Result, Error> { - Ok(if let Some(anniversary) = self.get_anniversary() { - let fullname = if let Some(name) = self.get_full_name() { - name - } else { - return Ok(None); - }; - let anniversary = anniversary.date(); - let year = anniversary.year(); - let anniversary_start = anniversary.format(LOCAL_DATE); - let anniversary_end = anniversary - .succ_opt() - .unwrap_or(anniversary) - .format(LOCAL_DATE); - let uid = format!("{}-anniversary", self.get_id()); + Ok( + if let Some((anniversary, contains_year)) = self.get_anniversary() { + let fullname = if let Some(name) = self.get_full_name() { + name + } else { + return Ok(None); + }; + let anniversary = anniversary.date(); + let year = contains_year.then_some(anniversary.year()); + let anniversary_start = anniversary.format(LOCAL_DATE); + let anniversary_end = anniversary + .succ_opt() + .unwrap_or(anniversary) + .format(LOCAL_DATE); + let uid = format!("{}-anniversary", self.get_id()); - Some(CalendarObject::from_ics( - uid.clone(), - format!( - r#"BEGIN:VCALENDAR + let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default(); + Some(CalendarObject::from_ics( + uid.clone(), + format!( + r#"BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//github.com/lennart-k/rustical birthday calendar//EN @@ -105,39 +107,42 @@ DTSTART;VALUE=DATE:{anniversary_start} DTEND;VALUE=DATE:{anniversary_end} UID:{uid} RRULE:FREQ=YEARLY -SUMMARY:💍 {fullname} ({year}) +SUMMARY:💍 {fullname}{year_suffix} TRANSP:TRANSPARENT BEGIN:VALARM TRIGGER;VALUE=DURATION:-PT0M ACTION:DISPLAY -DESCRIPTION:💍 {fullname} ({year}) +DESCRIPTION:💍 {fullname}{year_suffix} END:VALARM END:VEVENT END:VCALENDAR"#, - ), - )?) - } else { - None - }) + ), + )?) + } else { + None + }, + ) } pub fn get_birthday_object(&self) -> Result, Error> { - Ok(if let Some(birthday) = self.get_birthday() { - let fullname = if let Some(name) = self.get_full_name() { - name - } else { - return Ok(None); - }; - let birthday = birthday.date(); - let year = birthday.year(); - let birthday_start = birthday.format(LOCAL_DATE); - let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE); - let uid = format!("{}-birthday", self.get_id()); + Ok( + if let Some((birthday, contains_year)) = self.get_birthday() { + let fullname = if let Some(name) = self.get_full_name() { + name + } else { + return Ok(None); + }; + let birthday = birthday.date(); + let year = contains_year.then_some(birthday.year()); + let birthday_start = birthday.format(LOCAL_DATE); + let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE); + let uid = format!("{}-birthday", self.get_id()); - Some(CalendarObject::from_ics( - uid.clone(), - format!( - r#"BEGIN:VCALENDAR + let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default(); + Some(CalendarObject::from_ics( + uid.clone(), + format!( + r#"BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//github.com/lennart-k/rustical birthday calendar//EN @@ -146,20 +151,21 @@ DTSTART;VALUE=DATE:{birthday_start} DTEND;VALUE=DATE:{birthday_end} UID:{uid} RRULE:FREQ=YEARLY -SUMMARY:🎂 {fullname} ({year}) +SUMMARY:🎂 {fullname}{year_suffix} TRANSP:TRANSPARENT BEGIN:VALARM TRIGGER;VALUE=DURATION:-PT0M ACTION:DISPLAY -DESCRIPTION:🎂 {fullname} ({year}) +DESCRIPTION:🎂 {fullname}{year_suffix} END:VALARM END:VEVENT END:VCALENDAR"#, - ), - )?) - } else { - None - }) + ), + )?) + } else { + None + }, + ) } /// Get significant dates associated with this address object diff --git a/crates/ical/src/timestamp.rs b/crates/ical/src/timestamp.rs index d48cf34..4078d16 100644 --- a/crates/ical/src/timestamp.rs +++ b/crates/ical/src/timestamp.rs @@ -254,6 +254,16 @@ impl CalDateTime { if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") { return Ok(CalDateTime::Date(date, timezone)); } + + Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string())) + } + + // Also returns whether the date contains a year + pub fn parse_vcard(value: &str) -> Result<(Self, bool), CalDateTimeError> { + if let Ok(datetime) = Self::parse(value, None) { + return Ok((datetime, true)); + } + if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) { // Because 1972 is a leap year let year = 1972; @@ -261,13 +271,15 @@ impl CalDateTime { let month = captures.name("m").unwrap().as_str().parse().ok().unwrap(); let day = captures.name("d").unwrap().as_str().parse().ok().unwrap(); - return Ok(CalDateTime::Date( - NaiveDate::from_ymd_opt(year, month, day) - .ok_or(CalDateTimeError::ParseError(value.to_string()))?, - timezone, + return Ok(( + CalDateTime::Date( + NaiveDate::from_ymd_opt(year, month, day) + .ok_or(CalDateTimeError::ParseError(value.to_string()))?, + CalTimezone::Local, + ), + false, )); } - Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string())) }