From 98bef5ed17f2f1b83a341c9760c52e86ca10a591 Mon Sep 17 00:00:00 2001 From: and Date: Thu, 28 Nov 2024 17:58:16 +0100 Subject: [PATCH 01/29] Prototype event generation using ByRules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Dias --- src/common/calendar/date/CalendarUtils.ts | 109 ++++++++++++++---- .../calendar/import/ImportExportUtils.ts | 40 +++++-- 2 files changed, 117 insertions(+), 32 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 1026a15c3f21..4db178687782 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -11,6 +11,7 @@ import { getStartOfDay, incrementDate, insertIntoSortedArray, + isNotEmpty, isNotNull, isSameDayOfDate, isValidDate, @@ -38,7 +39,7 @@ import { UserSettingsGroupRoot, } from "../../api/entities/tutanota/TypeRefs.js" import { CalendarEventTimes, DAYS_SHIFTED_MS, generateEventElementId, isAllDayEvent, isAllDayEventByTimes } from "../../api/common/utils/CommonCalendarUtils" -import { createDateWrapper, DateWrapper, GroupInfo, RepeatRule, User } from "../../api/entities/sys/TypeRefs.js" +import { CalendarAdvancedRepeatRule, createDateWrapper, DateWrapper, GroupInfo, RepeatRule, User } from "../../api/entities/sys/TypeRefs.js" import { isSameId } from "../../api/common/utils/EntityUtils" import type { Time } from "./Time.js" import { CalendarInfo } from "../../../calendar-app/calendar/model/CalendarModel" @@ -48,6 +49,7 @@ import { CalendarEventUidIndexEntry } from "../../api/worker/facades/lazy/Calend import { ParserError } from "../../misc/parsing/ParserCombinator.js" import { LoginController } from "../../api/main/LoginController.js" import { BirthdayEventRegistry } from "./CalendarEventsRepository.js" +import { ByRule, BYRULE_MAP } from "../import/ImportExportUtils.js" export type CalendarTimeRange = { start: number @@ -154,55 +156,105 @@ export function calculateAlarmTime(date: Date, interval: AlarmInterval, ianaTime return DateTime.fromJSDate(date, { zone: ianaTimeZone, }) - .minus(diff) - .toJSDate() + .minus(diff) + .toJSDate() } /** takes a date which encodes the day in UTC and produces a date that encodes the same date but in local time zone. All times must be 0. */ export function getAllDayDateForTimezone(utcDate: Date, zone: string): Date { return DateTime.fromJSDate(utcDate, { zone: "utc" }) - .setZone(zone, { keepLocalTime: true }) - .set({ hour: 0, minute: 0, second: 0, millisecond: 0 }) - .toJSDate() + .setZone(zone, { keepLocalTime: true }) + .set({ hour: 0, minute: 0, second: 0, millisecond: 0 }) + .toJSDate() +} + +export function incrementByAdvancedRepeatRule(date: Date, advancedRepeatRule: CalendarAdvancedRepeatRule, ianaTimeZone: string): Date { + // FIXME implement switch to handle different ByRules + switch (advancedRepeatRule.ruleType) { + case String(ByRule.BYDAY): + const interval = // FIXME Calculate the interval taking into account advancedRepeatRule.interval(in this case the next weekday) - date + return DateTime.fromJSDate(date, { + zone: ianaTimeZone, + }). + .plus({ + days: interval, + }) + .toJSDate() + } + return date } +function* incrementAndGenerateByDayRule(date: Date, rule: ByRule, value: string) { + +} + +//FIXME I want to try using recursive functions, but no clear way to do it yet +function* generateAdvancedRules(date: Date, max: Date, rules: CalendarAdvancedRepeatRule[], zone: string) { + const parsedRules = new Map + let byRulesIndex = new Map + + for (const rule of rules) { + const ruleList = parsedRules.get(rule.ruleType as ByRule) ?? [] + parsedRules.set(rule.ruleType as ByRule, [...ruleList, rule]) + } + + for (const rule of BYRULE_MAP.values()) { + byRulesIndex.set(rule, undefined) + } + + const luxonDate = DateTime.fromJSDate(date, { zone }) + + if (parsedRules.has(ByRule.BYMINUTE)) { + const index = byRulesIndex.get(ByRule.BYMINUTE) ?? 0 + const rr = parsedRules.get(ByRule.BYMINUTE) ?? [] + + luxonDate.set({ minute: Number.parseInt(rr[index].interval) }) + byRulesIndex.set(ByRule.BYMINUTE, index + 1) + + yield luxonDate + } + + +} + +//FIXME Might be worth checking where this func is being used and start using the new function that considers advanced repeat rules export function incrementByRepeatPeriod(date: Date, repeatPeriod: RepeatPeriod, interval: number, ianaTimeZone: string): Date { switch (repeatPeriod) { case RepeatPeriod.DAILY: return DateTime.fromJSDate(date, { zone: ianaTimeZone, }) - .plus({ - days: interval, - }) - .toJSDate() + .plus({ + days: interval, + }) + .toJSDate() case RepeatPeriod.WEEKLY: return DateTime.fromJSDate(date, { zone: ianaTimeZone, }) - .plus({ - weeks: interval, - }) - .toJSDate() + .plus({ + weeks: interval, + }) + .toJSDate() case RepeatPeriod.MONTHLY: return DateTime.fromJSDate(date, { zone: ianaTimeZone, }) - .plus({ - months: interval, - }) - .toJSDate() + .plus({ + months: interval, + }) + .toJSDate() case RepeatPeriod.ANNUALLY: return DateTime.fromJSDate(date, { zone: ianaTimeZone, }) - .plus({ - years: interval, - }) - .toJSDate() + .plus({ + years: interval, + }) + .toJSDate() default: throw new Error("Unknown repeat period") @@ -660,12 +712,23 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string): Gene let calcEndTime = eventEndTime let iteration = 1 + const advancedRepeatRules = [...repeatRule.advancedRules] + while ((endOccurrences == null || iteration <= endOccurrences) && (repeatEndTime == null || calcStartTime.getTime() < repeatEndTime.getTime())) { assertDateIsValid(calcStartTime) assertDateIsValid(calcEndTime) yield { startTime: calcStartTime, endTime: calcEndTime } - calcStartTime = incrementByRepeatPeriod(eventStartTime, frequency, interval * iteration, repeatTimeZone) + if (isNotEmpty(advancedRepeatRules)) { + if (frequency === RepeatPeriod.WEEKLY) { + // FIXME order advancedRepeatRules considering weekstart order, should we considered the wkst from the event or from the user settings? + } + const byRule = advancedRepeatRules.shift() + calcStartTime = incrementByAdvancedRepeatRule(eventStartTime, byRule!, interval * iteration, repeatTimeZone) + } else { + calcStartTime = incrementByRepeatPeriod(eventStartTime, downcast(repeatRule.frequency), interval * iteration, repeatTimeZone) + } + calcEndTime = allDay ? incrementByRepeatPeriod(calcStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) : DateTime.fromJSDate(calcStartTime).plus(calcDuration).toJSDate() diff --git a/src/common/calendar/import/ImportExportUtils.ts b/src/common/calendar/import/ImportExportUtils.ts index bccad53ebb5d..53f41b381c4d 100644 --- a/src/common/calendar/import/ImportExportUtils.ts +++ b/src/common/calendar/import/ImportExportUtils.ts @@ -154,15 +154,15 @@ export function hasValidProtocol(url: URL, validProtocols: string[]) { } export const enum ByRule { - BYMINUTE, - BYHOUR, - BYDAY, - BYMONTHDAY, - BYYEARDAY, - BYWEEKNO, - BYMONTH, - BYSETPOS, - WKST, + BYMINUTE = "0", + BYHOUR = "1", + BYDAY = "2", + BYMONTHDAY = "3", + BYYEARDAY = "4", + BYWEEKNO = "5", + BYMONTH = "6", + BYSETPOS = "7", + WKST = "8", } export const BYRULE_MAP = freezeMap( @@ -178,3 +178,25 @@ export const BYRULE_MAP = freezeMap( ["WKST", ByRule.WKST], ]), ) + +export const enum WeekDaysJsValue { + SU, + MO, + TU, + WE, + TH, + FR, + SA, +} + +export const BYRULE_WEEKDAYS_JS_VALUE = freezeMap( + new Map([ + ["SU", WeekDaysJsValue.SU], + ["MO", WeekDaysJsValue.MO], + ["TU", WeekDaysJsValue.TU], + ["WE", WeekDaysJsValue.WE], + ["TH", WeekDaysJsValue.TH], + ["FR", WeekDaysJsValue.FR], + ["SA", WeekDaysJsValue.SA], + ]), +) From 86fedd5138dbe75f1a6ef2e24bf394091b67c5ab Mon Sep 17 00:00:00 2001 From: mup Date: Mon, 2 Dec 2024 15:24:11 +0100 Subject: [PATCH 02/29] Implements event expansion for daily BYDAY and BYMONTH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Dias --- .../api/worker/facades/lazy/CalendarFacade.ts | 6 +- src/common/calendar/date/CalendarUtils.ts | 361 +++++++++++++++--- 2 files changed, 301 insertions(+), 66 deletions(-) diff --git a/src/common/api/worker/facades/lazy/CalendarFacade.ts b/src/common/api/worker/facades/lazy/CalendarFacade.ts index 70306f9ae88c..fec9493ec25d 100644 --- a/src/common/api/worker/facades/lazy/CalendarFacade.ts +++ b/src/common/api/worker/facades/lazy/CalendarFacade.ts @@ -156,14 +156,14 @@ export class CalendarFacade { // Generate events occurrences per calendar to avoid calendars flashing in the screen for (const calendar of calendars) { - this.generateEventOccurences(newEvents, calendar.short, month, zone, true) - this.generateEventOccurences(newEvents, calendar.long, month, zone, false) + this.generateEventOccurrences(newEvents, calendar.short, month, zone, true) + this.generateEventOccurrences(newEvents, calendar.long, month, zone, false) } return newEvents } - private generateEventOccurences( + private generateEventOccurrences( eventMap: Map, events: CalendarEvent[], range: CalendarTimeRange, diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 4db178687782..2d7ef3373c63 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -11,7 +11,6 @@ import { getStartOfDay, incrementDate, insertIntoSortedArray, - isNotEmpty, isNotNull, isSameDayOfDate, isValidDate, @@ -28,7 +27,7 @@ import { TimeFormat, WeekStart, } from "../../api/common/TutanotaConstants" -import { DateTime, DurationLikeObject, FixedOffsetZone, IANAZone } from "luxon" +import { DateTime, DurationLikeObject, FixedOffsetZone, IANAZone, WeekdayNumbers } from "luxon" import { CalendarEvent, CalendarEventTypeRef, @@ -156,42 +155,268 @@ export function calculateAlarmTime(date: Date, interval: AlarmInterval, ianaTime return DateTime.fromJSDate(date, { zone: ianaTimeZone, }) - .minus(diff) - .toJSDate() + .minus(diff) + .toJSDate() } /** takes a date which encodes the day in UTC and produces a date that encodes the same date but in local time zone. All times must be 0. */ export function getAllDayDateForTimezone(utcDate: Date, zone: string): Date { return DateTime.fromJSDate(utcDate, { zone: "utc" }) - .setZone(zone, { keepLocalTime: true }) - .set({ hour: 0, minute: 0, second: 0, millisecond: 0 }) - .toJSDate() + .setZone(zone, { keepLocalTime: true }) + .set({ hour: 0, minute: 0, second: 0, millisecond: 0 }) + .toJSDate() } -export function incrementByAdvancedRepeatRule(date: Date, advancedRepeatRule: CalendarAdvancedRepeatRule, ianaTimeZone: string): Date { - // FIXME implement switch to handle different ByRules - switch (advancedRepeatRule.ruleType) { - case String(ByRule.BYDAY): - const interval = // FIXME Calculate the interval taking into account advancedRepeatRule.interval(in this case the next weekday) - date - return DateTime.fromJSDate(date, { - zone: ianaTimeZone, - }). - .plus({ - days: interval, - }) - .toJSDate() +const WEEKDAY_TO_NUMBER = { + SU: 1, + MO: 2, + TU: 3, + WE: 4, + TH: 5, + FR: 6, + SA: 7, +} as Record + +function applyMinuteRules(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[]): DateTime[] { + if (parsedRules.length === 0) { + return dates } - return date + + const newDates: DateTime[] = [] + for (const date of dates) { + for (const rule of parsedRules) { + newDates.push( + date.set({ + //FIXME Check if rule accepts negative values + minute: Number.parseInt(rule.interval), + }), + ) + } + } + + return newDates } -function* incrementAndGenerateByDayRule(date: Date, rule: ByRule, value: string) { +function applyHourRules(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[]) { + if (parsedRules.length === 0) { + return dates + } + + const newDates: DateTime[] = [] + for (const date of dates) { + for (const rule of parsedRules) { + newDates.push( + date.set({ + hour: Number.parseInt(rule.interval), + }), + ) + } + } + return newDates +} + +function applyByDayRules( + dates: DateTime[], + parsedRules: CalendarAdvancedRepeatRule[], + frequency: RepeatPeriod, + validMonths: number[], + wkst: WeekdayNumbers, + hasWeekNo?: boolean, +) { + if (parsedRules.length === 0) { + return dates + } + + const ruleRegex = /^([-+]?\d{0,3})([a-zA-Z]{2})?$/g + + const newDates: DateTime[] = [] + for (const rule of parsedRules) { + for (const date of dates) { + if (!date.isValid) { + console.warn("Invalid date", date) + continue + } + const parsedRuleValue = rule.interval.match(ruleRegex) + + if (!parsedRuleValue) { + console.error(`Invalid interval ${rule.interval}`) + continue + } + + const targetWeekDay = parsedRuleValue[2] !== "" ? WEEKDAY_TO_NUMBER[parsedRuleValue[2]] : null + const leadingValue = parsedRuleValue[1] !== "" ? Number.parseInt(parsedRuleValue[1]) : null + + if (frequency === RepeatPeriod.WEEKLY) { + if (!targetWeekDay) { + continue + } + const dt = date.set({ weekday: targetWeekDay }) + const intervalStart = date.set({ weekday: wkst }) + if (dt.toMillis() > intervalStart.plus({ week: 1 }).toMillis()) { + // Do nothing + } else if (dt.toMillis() < intervalStart.toMillis()) { + newDates.push(intervalStart.plus({ week: 1 })) + } else { + newDates.push(dt) + } + } else if (frequency === RepeatPeriod.MONTHLY) { + if (!targetWeekDay) { + continue + } + + const weekChange = leadingValue ?? 0 + const stopCondition = date.plus({ month: 1 }).set({ day: 1 }) + + if (weekChange != 0) { + const absWeeks = weekChange > 0 ? weekChange : Math.ceil(date.daysInMonth! / 7) - Math.abs(weekChange) + 1 + const dt = date.set({ day: 1 }).set({ weekday: targetWeekDay }).plus({ week: absWeeks }) + + if (dt.toMillis() >= date.toMillis() && dt.toMillis() < stopCondition.toMillis()) { + newDates.push(dt) + } + } else { + let currentDate = date + while (currentDate < stopCondition) { + const dt = currentDate.set({ weekday: targetWeekDay }) + if (dt.toMillis() >= date.toMillis()) { + if (validMonths.length > 0 && validMonths.includes(dt.month)) { + newDates.push(dt) + } + } + currentDate = dt.plus({ week: 1 }) + } + } + } else if (frequency === RepeatPeriod.ANNUALLY) { + const weekChange = leadingValue ?? 0 + if (hasWeekNo && weekChange > 0) { + console.warn("Invalid repeat rule, can't use BYWEEKNO with Week Offset on BYDAY") + continue + } + + if (weekChange != 0 && !hasWeekNo) { + if (!targetWeekDay) { + continue + } + + const absWeeks = weekChange > 0 ? weekChange : Math.ceil(date.daysInMonth! / 7) - Math.abs(weekChange) + 1 + const dt = date.set({ day: 1 }).set({ weekday: targetWeekDay }).plus({ week: absWeeks }) + if (dt.toMillis() >= date.toMillis()) { + newDates.push(dt) + } + } else if (hasWeekNo) { + // Handle WKST + if (!targetWeekDay) { + continue + } + const dt = date.set({ weekday: targetWeekDay }) + const intervalStart = date.set({ weekday: wkst }) + if (dt.toMillis() > intervalStart.plus({ week: 1 }).toMillis()) { + // Do nothing + } else if (dt.toMillis() < intervalStart.toMillis()) { + newDates.push(intervalStart.plus({ week: 1 })) + } else { + newDates.push(dt) + } + } else if (!hasWeekNo && weekChange === 0) { + if (!targetWeekDay) { + continue + } + + const stopCondition = date.plus({ year: 1 }) + let currentDate = date.set({ weekday: targetWeekDay }) + + if (currentDate.toMillis() >= date.toMillis()) { + newDates.push(currentDate) + } + + currentDate = currentDate.plus({ week: 1 }) + + while (currentDate.toMillis() < stopCondition.toMillis()) { + newDates.push(currentDate) + currentDate = currentDate.plus({ week: 1 }) + } + } else { + if (!targetWeekDay && weekChange > 0) { + const dt = date.set({ day: 1, month: 1 }).plus({ day: weekChange }) + if (dt.toMillis() < date.toMillis()) { + newDates.push(dt.plus({ year: 1 })) + } else { + newDates.push(dt) + } + } + } + } + } + } + + return newDates +} + +function applyByMonth(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[], maxDate: Date) { + if (parsedRules.length === 0) { + return dates + } + + const newDates: DateTime[] = [] + for (const rule of parsedRules) { + console.log({ dates, rule }) + for (const date of dates) { + if (!date.isValid) { + console.warn("Invalid date", date) + continue + } + + const targetMonth = Number.parseInt(rule.interval) + + if (date.month === targetMonth) { + newDates.push(date) + } + } + } + + return newDates +} + +function applyWeekNo(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[], validMonths: number[]): DateTime[] { + if (parsedRules.length === 0) { + return dates + } + + const newDates: DateTime[] = [] + for (const rule of parsedRules) { + for (const date of dates) { + if (!date.isValid) { + console.warn("Invalid date", date) + continue + } + + const parsedWeekNumber = Number.parseInt(rule.interval) + let newDt: DateTime + if (parsedWeekNumber < 0) { + newDt = date.set({ weekNumber: date.weeksInWeekYear - parsedWeekNumber }) + } else { + newDt = date.set({ weekNumber: parsedWeekNumber }) + } + + // Check if we didn't mess with any BYMONTH rule + if (validMonths.length > 0 && !validMonths.includes(newDt.month)) { + continue + } + + if (!newDates.some((dt) => dt.toMillis() === newDt.toMillis())) { + newDates.push(newDt) + } + } + } + + return newDates } //FIXME I want to try using recursive functions, but no clear way to do it yet -function* generateAdvancedRules(date: Date, max: Date, rules: CalendarAdvancedRepeatRule[], zone: string) { - const parsedRules = new Map - let byRulesIndex = new Map +function* generateAdvancedRules(date: Date, max: Date, rules: CalendarAdvancedRepeatRule[], repeatPeriods: RepeatPeriod[], zone: string) { + const parsedRules = new Map() + let byRulesIndex = new Map() for (const rule of rules) { const ruleList = parsedRules.get(rule.ruleType as ByRule) ?? [] @@ -213,8 +438,6 @@ function* generateAdvancedRules(date: Date, max: Date, rules: CalendarAdvancedRe yield luxonDate } - - } //FIXME Might be worth checking where this func is being used and start using the new function that considers advanced repeat rules @@ -224,37 +447,37 @@ export function incrementByRepeatPeriod(date: Date, repeatPeriod: RepeatPeriod, return DateTime.fromJSDate(date, { zone: ianaTimeZone, }) - .plus({ - days: interval, - }) - .toJSDate() + .plus({ + days: interval, + }) + .toJSDate() case RepeatPeriod.WEEKLY: return DateTime.fromJSDate(date, { zone: ianaTimeZone, }) - .plus({ - weeks: interval, - }) - .toJSDate() + .plus({ + weeks: interval, + }) + .toJSDate() case RepeatPeriod.MONTHLY: return DateTime.fromJSDate(date, { zone: ianaTimeZone, }) - .plus({ - months: interval, - }) - .toJSDate() + .plus({ + months: interval, + }) + .toJSDate() case RepeatPeriod.ANNUALLY: return DateTime.fromJSDate(date, { zone: ianaTimeZone, }) - .plus({ - years: interval, - }) - .toJSDate() + .plus({ + years: interval, + }) + .toJSDate() default: throw new Error("Unknown repeat period") @@ -527,8 +750,8 @@ export function addDaysForRecurringEvent( const exclusions = allDay ? repeatRule.excludedDates.map(({ date }) => createDateWrapper({ date: getAllDayDateForTimezone(date, timeZone) })) : repeatRule.excludedDates - - for (const { startTime, endTime } of generateEventOccurrences(event, timeZone)) { + const generatedEvents = generateEventOccurrences(event, timeZone, new Date(range.end)) + for (const { startTime, endTime } of generatedEvents) { if (startTime.getTime() > range.end) break if (endTime.getTime() < range.start) continue if (isExcludedDate(startTime, exclusions)) { @@ -611,7 +834,7 @@ export function generateCalendarInstancesInRange( nextCandidate: CalendarEvent }> = progenitors .map((p) => { - const generator = generateEventOccurrences(p, timeZone) + const generator = generateEventOccurrences(p, timeZone, new Date(range.end)) const excludedDates = p.repeatRule?.excludedDates ?? [] const nextCandidate = getNextCandidate(p, generator, excludedDates) if (nextCandidate == null) return null @@ -674,8 +897,9 @@ export function getRepeatEndTimeForDisplay(repeatRule: RepeatRule, isAllDay: boo * terminates once the end condition of the repeat rule is hit. * @param event the event to iterate occurrences on. * @param timeZone + * @param maxDate */ -function* generateEventOccurrences(event: CalendarEvent, timeZone: string): Generator<{ startTime: Date; endTime: Date }> { +function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDate: Date): Generator<{ startTime: Date; endTime: Date }> { const { repeatRule } = event if (repeatRule == null) { @@ -709,29 +933,29 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string): Gene let calcStartTime = eventStartTime const calcDuration = allDay ? getDiffIn24hIntervals(eventStartTime, eventEndTime, timeZone) : eventEndTime.getTime() - eventStartTime.getTime() - let calcEndTime = eventEndTime let iteration = 1 - const advancedRepeatRules = [...repeatRule.advancedRules] - while ((endOccurrences == null || iteration <= endOccurrences) && (repeatEndTime == null || calcStartTime.getTime() < repeatEndTime.getTime())) { - assertDateIsValid(calcStartTime) - assertDateIsValid(calcEndTime) - yield { startTime: calcStartTime, endTime: calcEndTime } + if (frequency === RepeatPeriod.DAILY) { + const events = applyByMonth( + [DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], + repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH), + maxDate, + ) - if (isNotEmpty(advancedRepeatRules)) { - if (frequency === RepeatPeriod.WEEKLY) { - // FIXME order advancedRepeatRules considering weekstart order, should we considered the wkst from the event or from the user settings? + for (const event of events) { + const newStartTime = event.toJSDate() + const newEndTime = allDay + ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) + : DateTime.fromJSDate(newStartTime).plus(calcDuration).toJSDate() + + assertDateIsValid(newStartTime) + assertDateIsValid(newEndTime) + yield { startTime: newStartTime, endTime: newEndTime } } - const byRule = advancedRepeatRules.shift() - calcStartTime = incrementByAdvancedRepeatRule(eventStartTime, byRule!, interval * iteration, repeatTimeZone) - } else { - calcStartTime = incrementByRepeatPeriod(eventStartTime, downcast(repeatRule.frequency), interval * iteration, repeatTimeZone) } - calcEndTime = allDay - ? incrementByRepeatPeriod(calcStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) - : DateTime.fromJSDate(calcStartTime).plus(calcDuration).toJSDate() + calcStartTime = incrementByRepeatPeriod(eventStartTime, downcast(repeatRule.frequency), interval * iteration, repeatTimeZone) iteration++ } } @@ -769,8 +993,19 @@ export function calendarEventHasMoreThanOneOccurrencesLeft({ progenitor, altered // in our model, we have an extra exclusion for each altered instance. this code // assumes that this invariant is upheld here and does not match each recurrenceId // against an exclusion, but only tallies them up. + + // The only two possible endTypes here are EndType === Count || EndType === Date + let maxDate: Date + if (endType === EndType.Count) { + maxDate = new Date(progenitor.startTime.getTime() * Number(endValue ?? 1)) + } else { + const millis = endValue && Number(endValue) > 0 ? endValue : progenitor.startTime.getTime() + maxDate = new Date(millis) + } + let occurrencesFound = alteredInstances.length - for (const { startTime } of generateEventOccurrences(progenitor, getTimeZone())) { + + for (const { startTime } of generateEventOccurrences(progenitor, getTimeZone(), maxDate)) { const startTimestamp = startTime.getTime() while (i < excludedTimestamps.length && startTimestamp > excludedTimestamps[i]) { // exclusions are sorted From 6fb655c8a0eb17339f2b3f5ada80b06b09f87875 Mon Sep 17 00:00:00 2001 From: mup Date: Tue, 3 Dec 2024 12:44:47 +0100 Subject: [PATCH 03/29] Implements event expansion for Weekly interval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Dias --- src/common/calendar/date/CalendarUtils.ts | 74 +++++++++++++++++++---- 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 2d7ef3373c63..734ad4ddef7c 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -168,13 +168,13 @@ export function getAllDayDateForTimezone(utcDate: Date, zone: string): Date { } const WEEKDAY_TO_NUMBER = { - SU: 1, - MO: 2, - TU: 3, - WE: 4, - TH: 5, - FR: 6, - SA: 7, + MO: 1, + TU: 2, + WE: 3, + TH: 4, + FR: 5, + SA: 6, + SU: 7, } as Record function applyMinuteRules(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[]): DateTime[] { @@ -237,7 +237,7 @@ function applyByDayRules( console.warn("Invalid date", date) continue } - const parsedRuleValue = rule.interval.match(ruleRegex) + const parsedRuleValue = Array.from(rule.interval.matchAll(ruleRegex)).flat() if (!parsedRuleValue) { console.error(`Invalid interval ${rule.interval}`) @@ -251,13 +251,18 @@ function applyByDayRules( if (!targetWeekDay) { continue } - const dt = date.set({ weekday: targetWeekDay }) + + let dt = date.set({ weekday: targetWeekDay }) const intervalStart = date.set({ weekday: wkst }) if (dt.toMillis() > intervalStart.plus({ week: 1 }).toMillis()) { // Do nothing + continue } else if (dt.toMillis() < intervalStart.toMillis()) { - newDates.push(intervalStart.plus({ week: 1 })) - } else { + dt = dt.plus({ week: 1 }) + } + + debugger + if (validMonths.length === 0 || validMonths.includes(dt.month)) { newDates.push(dt) } } else if (frequency === RepeatPeriod.MONTHLY) { @@ -353,14 +358,13 @@ function applyByDayRules( return newDates } -function applyByMonth(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[], maxDate: Date) { +function applyByMonth(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[], maxDate: Date, repeatPeriod: RepeatPeriod) { if (parsedRules.length === 0) { return dates } const newDates: DateTime[] = [] for (const rule of parsedRules) { - console.log({ dates, rule }) for (const date of dates) { if (!date.isValid) { console.warn("Invalid date", date) @@ -369,6 +373,19 @@ function applyByMonth(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule const targetMonth = Number.parseInt(rule.interval) + if (repeatPeriod === RepeatPeriod.WEEKLY) { + const weekStart = date.set({ weekday: 1 }) + const weekEnd = date.set({ weekday: 7 }) + + if (weekStart.year === weekEnd.year && weekStart.month < weekEnd.month && (weekEnd.month === targetMonth || weekStart.month === targetMonth)) { + newDates.push(date) + continue + } else if (weekStart.year < weekEnd.year && (weekEnd.month === targetMonth || weekStart.month === targetMonth)) { + newDates.push(date) + continue + } + } + if (date.month === targetMonth) { newDates.push(date) } @@ -941,9 +958,40 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa [DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH), maxDate, + RepeatPeriod.DAILY, + ) + + for (const event of events) { + const newStartTime = event.toJSDate() + const newEndTime = allDay + ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) + : DateTime.fromJSDate(newStartTime).plus(calcDuration).toJSDate() + + assertDateIsValid(newStartTime) + assertDateIsValid(newEndTime) + yield { startTime: newStartTime, endTime: newEndTime } + } + } else if (frequency === RepeatPeriod.WEEKLY) { + const byMonthRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH) + const weekStartRule = repeatRule.advancedRules.find((rule) => rule.ruleType === ByRule.WKST)?.interval + const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) + + const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, maxDate, RepeatPeriod.WEEKLY) + + const events = applyByDayRules( + monthAppliedEvents, + repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYDAY), + RepeatPeriod.WEEKLY, + validMonths, + weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, ) for (const event of events) { + // An Edge date allowed by BYMONTH, we shouldn't accept it + if (validMonths.length > 0 && !validMonths.includes(event.month)) { + continue + } + const newStartTime = event.toJSDate() const newEndTime = allDay ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) From 23ac945362c852c89333886eb628c5d21ca7e381 Mon Sep 17 00:00:00 2001 From: mup Date: Tue, 3 Dec 2024 15:25:28 +0100 Subject: [PATCH 04/29] Implements event expansion for Monthly interval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Dias --- src/common/calendar/date/CalendarUtils.ts | 117 +++++++++++++++------- 1 file changed, 83 insertions(+), 34 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 734ad4ddef7c..88df9b417b3d 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -48,7 +48,7 @@ import { CalendarEventUidIndexEntry } from "../../api/worker/facades/lazy/Calend import { ParserError } from "../../misc/parsing/ParserCombinator.js" import { LoginController } from "../../api/main/LoginController.js" import { BirthdayEventRegistry } from "./CalendarEventsRepository.js" -import { ByRule, BYRULE_MAP } from "../import/ImportExportUtils.js" +import { ByRule } from "../import/ImportExportUtils.js" export type CalendarTimeRange = { start: number @@ -272,19 +272,26 @@ function applyByDayRules( const weekChange = leadingValue ?? 0 const stopCondition = date.plus({ month: 1 }).set({ day: 1 }) + const baseDate = date.set({ day: 1 }) if (weekChange != 0) { - const absWeeks = weekChange > 0 ? weekChange : Math.ceil(date.daysInMonth! / 7) - Math.abs(weekChange) + 1 - const dt = date.set({ day: 1 }).set({ weekday: targetWeekDay }).plus({ week: absWeeks }) + const absWeeks = weekChange > 0 ? weekChange : Math.ceil(baseDate.daysInMonth! / 7) - Math.abs(weekChange) + let dt = baseDate.set({ day: 1 }) + + while (dt.weekday != targetWeekDay) { + dt = dt.plus({ day: 1 }) + } + + dt = dt.plus({ week: absWeeks - 1 }) - if (dt.toMillis() >= date.toMillis() && dt.toMillis() < stopCondition.toMillis()) { + if (dt.toMillis() >= baseDate.toMillis() && dt.toMillis() < stopCondition.toMillis()) { newDates.push(dt) } } else { - let currentDate = date + let currentDate = baseDate while (currentDate < stopCondition) { const dt = currentDate.set({ weekday: targetWeekDay }) - if (dt.toMillis() >= date.toMillis()) { + if (dt.toMillis() >= baseDate.toMillis()) { if (validMonths.length > 0 && validMonths.includes(dt.month)) { newDates.push(dt) } @@ -395,6 +402,42 @@ function applyByMonth(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule return newDates } +function applyByMonthDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[]) { + if (parsedRules.length === 0) { + return dates + } + + const newDates: DateTime[] = [] + for (const rule of parsedRules) { + for (const date of dates) { + if (!date.isValid) { + console.warn("Invalid event date", date) + continue + } + + const targetDay = Number.parseInt(rule.interval) + + if (Number.isNaN(targetDay)) { + console.warn("Invalid BYMONTHDAY rule for date", date) + continue + } + + if (targetDay >= 0) { + newDates.push(date.set({ day: targetDay })) + continue + } + + const daysDiff = date.daysInMonth! - Math.abs(targetDay) + + if (daysDiff > 0) { + newDates.push(date.set({ day: daysDiff })) + } + } + } + + return newDates +} + function applyWeekNo(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[], validMonths: number[]): DateTime[] { if (parsedRules.length === 0) { return dates @@ -430,33 +473,6 @@ function applyWeekNo(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[ return newDates } -//FIXME I want to try using recursive functions, but no clear way to do it yet -function* generateAdvancedRules(date: Date, max: Date, rules: CalendarAdvancedRepeatRule[], repeatPeriods: RepeatPeriod[], zone: string) { - const parsedRules = new Map() - let byRulesIndex = new Map() - - for (const rule of rules) { - const ruleList = parsedRules.get(rule.ruleType as ByRule) ?? [] - parsedRules.set(rule.ruleType as ByRule, [...ruleList, rule]) - } - - for (const rule of BYRULE_MAP.values()) { - byRulesIndex.set(rule, undefined) - } - - const luxonDate = DateTime.fromJSDate(date, { zone }) - - if (parsedRules.has(ByRule.BYMINUTE)) { - const index = byRulesIndex.get(ByRule.BYMINUTE) ?? 0 - const rr = parsedRules.get(ByRule.BYMINUTE) ?? [] - - luxonDate.set({ minute: Number.parseInt(rr[index].interval) }) - byRulesIndex.set(ByRule.BYMINUTE, index + 1) - - yield luxonDate - } -} - //FIXME Might be worth checking where this func is being used and start using the new function that considers advanced repeat rules export function incrementByRepeatPeriod(date: Date, repeatPeriod: RepeatPeriod, interval: number, ianaTimeZone: string): Date { switch (repeatPeriod) { @@ -973,6 +989,7 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa } } else if (frequency === RepeatPeriod.WEEKLY) { const byMonthRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH) + const byDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYDAY) const weekStartRule = repeatRule.advancedRules.find((rule) => rule.ruleType === ByRule.WKST)?.interval const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) @@ -980,12 +997,44 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa const events = applyByDayRules( monthAppliedEvents, - repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYDAY), + byDayRules, RepeatPeriod.WEEKLY, validMonths, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, ) + for (const event of events) { + // An Edge date allowed by BYMONTH, we shouldn't accept it + if (validMonths.length > 0 && !validMonths.includes(event.month)) { + continue + } + + const newStartTime = event.toJSDate() + const newEndTime = allDay + ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) + : DateTime.fromJSDate(newStartTime).plus(calcDuration).toJSDate() + + assertDateIsValid(newStartTime) + assertDateIsValid(newEndTime) + yield { startTime: newStartTime, endTime: newEndTime } + } + } else if (frequency === RepeatPeriod.MONTHLY) { + const byMonthRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH) + const byDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYDAY) + const byMonthDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTHDAY) + const weekStartRule = repeatRule.advancedRules.find((rule) => rule.ruleType === ByRule.WKST)?.interval + const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) + + const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, maxDate, RepeatPeriod.MONTHLY) + const monthDayAppliedEvents = applyByMonthDay(monthAppliedEvents, byMonthDayRules) + const events = applyByDayRules( + monthDayAppliedEvents, + byDayRules, + RepeatPeriod.MONTHLY, + validMonths, + weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, + ) + for (const event of events) { // An Edge date allowed by BYMONTH, we shouldn't accept it if (validMonths.length > 0 && !validMonths.includes(event.month)) { From ed889730465a719f93f120418e319cd96e2583ef Mon Sep 17 00:00:00 2001 From: mup Date: Mon, 9 Dec 2024 08:38:54 +0100 Subject: [PATCH 05/29] Implements expansion for Yearly interval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Dias --- src/common/calendar/date/CalendarUtils.ts | 154 ++++++++++++++++++---- 1 file changed, 125 insertions(+), 29 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 88df9b417b3d..de772858c494 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -27,7 +27,7 @@ import { TimeFormat, WeekStart, } from "../../api/common/TutanotaConstants" -import { DateTime, DurationLikeObject, FixedOffsetZone, IANAZone, WeekdayNumbers } from "luxon" +import { DateTime, DurationLikeObject, FixedOffsetZone, IANAZone, MonthNumbers, WeekdayNumbers } from "luxon" import { CalendarEvent, CalendarEventTypeRef, @@ -391,6 +391,12 @@ function applyByMonth(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule newDates.push(date) continue } + } else if (repeatPeriod === RepeatPeriod.ANNUALLY) { + const dt = date.set({ month: targetMonth }) + const yearOffset: number = date.year === dt.year && date.month > dt.month ? 1 : 0 + + newDates.push(date.set({ month: targetMonth }).plus({ year: yearOffset })) + continue } if (date.month === targetMonth) { @@ -438,7 +444,7 @@ function applyByMonthDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatR return newDates } -function applyWeekNo(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[], validMonths: number[]): DateTime[] { +function applyWeekNo(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[], wkst: WeekdayNumbers): DateTime[] { if (parsedRules.length === 0) { return dates } @@ -453,26 +459,68 @@ function applyWeekNo(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[ const parsedWeekNumber = Number.parseInt(rule.interval) let newDt: DateTime + if (parsedWeekNumber < 0) { - newDt = date.set({ weekNumber: date.weeksInWeekYear - parsedWeekNumber }) + newDt = date.set({ weekNumber: date.weeksInWeekYear - Math.abs(parsedWeekNumber) }).set({ weekday: wkst }) + for (let i = 0; i < 7; i++) { + newDates.push(newDt.plus({ day: i })) + } } else { - newDt = date.set({ weekNumber: parsedWeekNumber }) + newDt = date.set({ weekNumber: parsedWeekNumber }).set({ weekday: wkst }) + + for (let i = 0; i < 7; i++) { + newDates.push(newDt.plus({ day: i })) + } } + } + } + + return newDates +} + +function applyYearDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[]) { + if (parsedRules.length === 0) { + return dates + } - // Check if we didn't mess with any BYMONTH rule - if (validMonths.length > 0 && !validMonths.includes(newDt.month)) { + const newDates: DateTime[] = [] + for (const rule of parsedRules) { + for (const date of dates) { + if (!date.isValid) { + console.warn("Invalid date", date) continue } - if (!newDates.some((dt) => dt.toMillis() === newDt.toMillis())) { - newDates.push(newDt) + const targetDay = Number.parseInt(rule.interval) + + let dt: DateTime + if (targetDay < 0) { + dt = date.set({ day: 31, month: 12 }).minus({ day: Math.abs(targetDay) }) + } else { + dt = date.set({ day: 1, month: 1 }).plus({ day: targetDay - 1 }) } + + const yearOffset = dt.toMillis() < date.toMillis() ? 1 : 0 + dt = dt.plus({ year: yearOffset }) + + newDates.push(dt) } } return newDates } +/* + * Order generated events by date, avoiding an early stop and filter out events that doesn't respect BYMONTH rule + * @param dates Generated DateTime objects from BYRULEs + * @param validMonths List of months allowed by BYMONTH rules, should be empty in case no BYMONTH is specified + */ +function finishByRules(dates: DateTime[], validMonths: MonthNumbers[]) { + const cleanDates = validMonths.length > 0 ? dates.filter((dt) => validMonths.includes(dt.month as MonthNumbers)) : dates + + return cleanDates.sort((a, b) => a.toMillis() - b.toMillis()) +} + //FIXME Might be worth checking where this func is being used and start using the new function that considers advanced repeat rules export function incrementByRepeatPeriod(date: Date, repeatPeriod: RepeatPeriod, interval: number, ianaTimeZone: string): Date { switch (repeatPeriod) { @@ -966,9 +1014,19 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa let calcStartTime = eventStartTime const calcDuration = allDay ? getDiffIn24hIntervals(eventStartTime, eventEndTime, timeZone) : eventEndTime.getTime() - eventStartTime.getTime() + let calcEndTime = eventEndTime let iteration = 1 + assertDateIsValid(calcStartTime) + assertDateIsValid(calcEndTime) + yield { startTime: calcStartTime, endTime: calcEndTime } + while ((endOccurrences == null || iteration <= endOccurrences) && (repeatEndTime == null || calcStartTime.getTime() < repeatEndTime.getTime())) { + // We reached our range end, no need to continue generating/evaluating events + if (calcStartTime.getTime() > maxDate.getTime()) { + break + } + if (frequency === RepeatPeriod.DAILY) { const events = applyByMonth( [DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], @@ -995,20 +1053,18 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, maxDate, RepeatPeriod.WEEKLY) - const events = applyByDayRules( - monthAppliedEvents, - byDayRules, - RepeatPeriod.WEEKLY, - validMonths, - weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, + const events = finishByRules( + applyByDayRules( + monthAppliedEvents, + byDayRules, + RepeatPeriod.WEEKLY, + validMonths, + weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, + ), + validMonths as MonthNumbers[], ) for (const event of events) { - // An Edge date allowed by BYMONTH, we shouldn't accept it - if (validMonths.length > 0 && !validMonths.includes(event.month)) { - continue - } - const newStartTime = event.toJSDate() const newEndTime = allDay ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) @@ -1027,20 +1083,60 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, maxDate, RepeatPeriod.MONTHLY) const monthDayAppliedEvents = applyByMonthDay(monthAppliedEvents, byMonthDayRules) - const events = applyByDayRules( - monthDayAppliedEvents, - byDayRules, - RepeatPeriod.MONTHLY, - validMonths, - weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, + + const events = finishByRules( + applyByDayRules( + monthDayAppliedEvents, + byDayRules, + RepeatPeriod.MONTHLY, + validMonths, + weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, + ), + validMonths as MonthNumbers[], ) for (const event of events) { - // An Edge date allowed by BYMONTH, we shouldn't accept it - if (validMonths.length > 0 && !validMonths.includes(event.month)) { - continue - } + const newStartTime = event.toJSDate() + const newEndTime = allDay + ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) + : DateTime.fromJSDate(newStartTime).plus(calcDuration).toJSDate() + assertDateIsValid(newStartTime) + assertDateIsValid(newEndTime) + yield { startTime: newStartTime, endTime: newEndTime } + } + } else if (frequency === RepeatPeriod.ANNUALLY) { + const byMonthRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH) + const byDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYDAY) + const byMonthDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTHDAY) + const byYearDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYYEARDAY) + const byWeekNoRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYWEEKNO) + const weekStartRule = repeatRule.advancedRules.find((rule) => rule.ruleType === ByRule.WKST)?.interval + const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) + + const monthAppliedEvents = applyByMonth( + [DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], + byMonthRules, + maxDate, + RepeatPeriod.ANNUALLY, + ) + + const weekNoAppliedEvents = applyWeekNo(monthAppliedEvents, byWeekNoRules, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO) + const yearDayAppliedEvents = applyYearDay(weekNoAppliedEvents, byYearDayRules) + const monthDayAppliedEvents = applyByMonthDay(yearDayAppliedEvents, byMonthDayRules) + + const events = finishByRules( + applyByDayRules( + monthDayAppliedEvents, + byDayRules, + RepeatPeriod.ANNUALLY, + validMonths, + weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, + ), + validMonths as MonthNumbers[], + ) + + for (const event of events) { const newStartTime = event.toJSDate() const newEndTime = allDay ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) From c04c45bace400256541e6ddea122aef7d953c54b Mon Sep 17 00:00:00 2001 From: pas Date: Tue, 10 Dec 2024 16:16:44 +0100 Subject: [PATCH 06/29] Add export of advanced repeat rules --- .../calendar/export/CalendarExporter.ts | 38 ++++++++++++++++--- .../calendar/import/ImportExportUtils.ts | 2 +- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/calendar-app/calendar/export/CalendarExporter.ts b/src/calendar-app/calendar/export/CalendarExporter.ts index f14249d31fdf..82a3748d4e30 100644 --- a/src/calendar-app/calendar/export/CalendarExporter.ts +++ b/src/calendar-app/calendar/export/CalendarExporter.ts @@ -1,16 +1,24 @@ -import type { CalendarAttendeeStatus, CalendarMethod } from "../../../common/api/common/TutanotaConstants" -import { assertEnumValue, EndType, RepeatPeriod, SECOND_MS } from "../../../common/api/common/TutanotaConstants" -import { assertNotNull, downcast, incrementDate, mapAndFilterNull, neverNull, pad, stringToUtf8Uint8Array } from "@tutao/tutanota-utils" +import { + assertEnumValue, + CalendarAttendeeStatus, + CalendarMethod, + EndType, + RepeatPeriod, + reverse, + SECOND_MS, +} from "../../../common/api/common/TutanotaConstants" +import { assertNotNull, downcast, incrementDate, isNotEmpty, mapAndFilterNull, neverNull, pad, stringToUtf8Uint8Array } from "@tutao/tutanota-utils" import { calendarAttendeeStatusToParstat, iCalReplacements, repeatPeriodToIcalFrequency } from "./CalendarParser" import { getAllDayDateLocal, isAllDayEvent } from "../../../common/api/common/utils/CommonCalendarUtils" import { AlarmIntervalUnit, generateUid, getTimeZone, parseAlarmInterval } from "../../../common/calendar/date/CalendarUtils" import type { CalendarEvent } from "../../../common/api/entities/tutanota/TypeRefs.js" import { createFile } from "../../../common/api/entities/tutanota/TypeRefs.js" import { convertToDataFile, DataFile } from "../../../common/api/common/DataFile" -import type { DateWrapper, RepeatRule, UserAlarmInfo } from "../../../common/api/entities/sys/TypeRefs.js" +import type { CalendarAdvancedRepeatRule, DateWrapper, RepeatRule, UserAlarmInfo } from "../../../common/api/entities/sys/TypeRefs.js" import { DateTime } from "luxon" import { getLetId } from "../../../common/api/common/utils/EntityUtils" import { CALENDAR_MIME_TYPE } from "../../../common/file/FileController.js" +import { ByRule } from "../../../common/calendar/import/ImportExportUtils.js" /** create an ical data file that can be attached to an invitation/update/cancellation/response mail */ export function makeInvitationCalendarFile(event: CalendarEvent, method: CalendarMethod, now: Date, zone: string): DataFile { @@ -99,6 +107,24 @@ export function serializeEvent(event: CalendarEvent, alarms: Array() + const byRuleValueToKey = reverse(ByRule) + advancedRules.forEach((r) => { + const type = byRuleValueToKey[r.ruleType as ByRule] + BYRULES.set(type, BYRULES.get(type) ? `${BYRULES.get(type)},${r.interval}` : r.interval) + }) + BYRULES.forEach((interval, type) => { + advancedRepeatRules += `;${type.toUpperCase()}=${interval}` + }) + } + + return advancedRepeatRules +} + /** importer internals exported for testing */ export function serializeRepeatRule(repeatRule: RepeatRule | null, isAllDayEvent: boolean, localTimeZone: string) { if (repeatRule) { @@ -136,9 +162,11 @@ export function serializeRepeatRule(repeatRule: RepeatRule | null, isAllDayEvent } const excludedDates = serializeExcludedDates(repeatRule.excludedDates, repeatRule.timeZone) + const advancedRepeatRules = serializeAdvancedRepeatRules(repeatRule.advancedRules) + return [ `RRULE:FREQ=${repeatPeriodToIcalFrequency(assertEnumValue(RepeatPeriod, repeatRule.frequency))}` + `;INTERVAL=${repeatRule.interval}` + endType, - ].concat(excludedDates) + ].concat(advancedRepeatRules, excludedDates) } else { return [] } diff --git a/src/common/calendar/import/ImportExportUtils.ts b/src/common/calendar/import/ImportExportUtils.ts index 53f41b381c4d..6a7f6c312829 100644 --- a/src/common/calendar/import/ImportExportUtils.ts +++ b/src/common/calendar/import/ImportExportUtils.ts @@ -153,7 +153,7 @@ export function hasValidProtocol(url: URL, validProtocols: string[]) { return validProtocols.includes(url.protocol) } -export const enum ByRule { +export enum ByRule { BYMINUTE = "0", BYHOUR = "1", BYDAY = "2", From 8647d66f9b1b2f7050caf34094fe243dd3763ba6 Mon Sep 17 00:00:00 2001 From: and Date: Thu, 12 Dec 2024 09:45:07 +0100 Subject: [PATCH 07/29] Implements BYMONTHDAY filtering --- src/common/calendar/date/CalendarUtils.ts | 53 +++++++++++++++-------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index de772858c494..2550b1a0c126 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -247,7 +247,12 @@ function applyByDayRules( const targetWeekDay = parsedRuleValue[2] !== "" ? WEEKDAY_TO_NUMBER[parsedRuleValue[2]] : null const leadingValue = parsedRuleValue[1] !== "" ? Number.parseInt(parsedRuleValue[1]) : null - if (frequency === RepeatPeriod.WEEKLY) { + if (frequency === RepeatPeriod.DAILY) { + if (date.weekday !== targetWeekDay) { + continue + } + newDates.push(date) + } else if (frequency === RepeatPeriod.WEEKLY) { if (!targetWeekDay) { continue } @@ -408,7 +413,7 @@ function applyByMonth(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule return newDates } -function applyByMonthDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[]) { +function applyByMonthDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[], isDailyEvent: boolean = false) { if (parsedRules.length === 0) { return dates } @@ -420,7 +425,6 @@ function applyByMonthDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatR console.warn("Invalid event date", date) continue } - const targetDay = Number.parseInt(rule.interval) if (Number.isNaN(targetDay)) { @@ -428,15 +432,26 @@ function applyByMonthDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatR continue } - if (targetDay >= 0) { - newDates.push(date.set({ day: targetDay })) - continue - } - - const daysDiff = date.daysInMonth! - Math.abs(targetDay) + if (isDailyEvent) { + if (targetDay > 0 && date.day === targetDay) { + newDates.push(date) + } else if (targetDay < 0) { + const daysDiff = date.daysInMonth! - Math.abs(targetDay) + 1 - if (daysDiff > 0) { - newDates.push(date.set({ day: daysDiff })) + if (daysDiff > 0 && daysDiff === date.day) { + newDates.push(date) + } + } + } else { + // Monthly or Yearly + if (targetDay >= 0) { + newDates.push(date.set({ day: targetDay })) + continue + } + const daysDiff = date.daysInMonth! - Math.abs(targetDay) + 1 + if (daysDiff > 0) { + newDates.push(date.set({ day: daysDiff })) + } } } } @@ -521,7 +536,6 @@ function finishByRules(dates: DateTime[], validMonths: MonthNumbers[]) { return cleanDates.sort((a, b) => a.toMillis() - b.toMillis()) } -//FIXME Might be worth checking where this func is being used and start using the new function that considers advanced repeat rules export function incrementByRepeatPeriod(date: Date, repeatPeriod: RepeatPeriod, interval: number, ianaTimeZone: string): Date { switch (repeatPeriod) { case RepeatPeriod.DAILY: @@ -1028,11 +1042,16 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa } if (frequency === RepeatPeriod.DAILY) { - const events = applyByMonth( - [DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], - repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH), - maxDate, - RepeatPeriod.DAILY, + const byMonthRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH) + const byMonthDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTHDAY) + const byDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYDAY) + const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) + + const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, maxDate, RepeatPeriod.DAILY) + const monthDayAppliedEvents = applyByMonthDay(monthAppliedEvents, byMonthDayRules, true) + const events = finishByRules( + applyByDayRules(monthDayAppliedEvents, byDayRules, RepeatPeriod.DAILY, validMonths, WEEKDAY_TO_NUMBER.MO), + validMonths as MonthNumbers[], ) for (const event of events) { From d4b9eaaffb3ed82d529821dbdcff9b3b935da321 Mon Sep 17 00:00:00 2001 From: and Date: Thu, 12 Dec 2024 11:05:15 +0100 Subject: [PATCH 08/29] Filter events happening before progenitor --- src/common/calendar/date/CalendarUtils.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 2550b1a0c126..ad187d6cc408 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -248,11 +248,13 @@ function applyByDayRules( const leadingValue = parsedRuleValue[1] !== "" ? Number.parseInt(parsedRuleValue[1]) : null if (frequency === RepeatPeriod.DAILY) { + // BYMONTH => BYMONTHDAY => BYDAY if (date.weekday !== targetWeekDay) { continue } newDates.push(date) } else if (frequency === RepeatPeriod.WEEKLY) { + // BYMONTH => BYDAY(expand) if (!targetWeekDay) { continue } @@ -530,8 +532,12 @@ function applyYearDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule * @param dates Generated DateTime objects from BYRULEs * @param validMonths List of months allowed by BYMONTH rules, should be empty in case no BYMONTH is specified */ -function finishByRules(dates: DateTime[], validMonths: MonthNumbers[]) { - const cleanDates = validMonths.length > 0 ? dates.filter((dt) => validMonths.includes(dt.month as MonthNumbers)) : dates +function finishByRules(dates: DateTime[], validMonths: MonthNumbers[], progenitorStartDate?: Date) { + let cleanDates = validMonths.length > 0 ? dates.filter((dt) => validMonths.includes(dt.month as MonthNumbers)) : dates + + if (progenitorStartDate) { + cleanDates = cleanDates.filter((dt) => dt.toMillis() >= progenitorStartDate.getTime()) + } return cleanDates.sort((a, b) => a.toMillis() - b.toMillis()) } @@ -1081,6 +1087,7 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, ), validMonths as MonthNumbers[], + eventStartTime, ) for (const event of events) { From 5475873f14e3913ba810ea0bf97b77b76377d3d8 Mon Sep 17 00:00:00 2001 From: and Date: Mon, 9 Dec 2024 15:19:57 +0100 Subject: [PATCH 09/29] Implements BYSETPOS filtering --- src/common/calendar/date/CalendarUtils.ts | 66 ++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index ad187d6cc408..33c29d0a6ac8 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -11,6 +11,7 @@ import { getStartOfDay, incrementDate, insertIntoSortedArray, + isNotEmpty, isNotNull, isSameDayOfDate, isValidDate, @@ -268,7 +269,6 @@ function applyByDayRules( dt = dt.plus({ week: 1 }) } - debugger if (validMonths.length === 0 || validMonths.includes(dt.month)) { newDates.push(dt) } @@ -832,6 +832,54 @@ export function addDaysForEventInstance(daysToEvents: Map Number(value) < -366) || + positiveValues.some((value) => Number(value) > 366) || + positiveValues.includes(eventCount.toString()) + ) { + return true + } + break + case RepeatPeriod.WEEKLY: + if ( + negativeValues.some((value) => Number(value) < -7) || + positiveValues.some((value) => Number(value) > 7) || + positiveValues.includes(eventCount.toString()) + ) { + return true + } + break + case RepeatPeriod.MONTHLY: + if ( + negativeValues.some((value) => Number(value) < -31) || + positiveValues.some((value) => Number(value) > 31) || + positiveValues.includes(eventCount.toString()) + ) { + return true + } + break + } + for (const negativeValue of negativeValues) { + if (allEvents.length - Math.abs(Number(negativeValue)) + 1 === eventCount) { + return true + } + } + return false +} + /** add the days a repeating {@param event} occurs on during {@param range} to {@param daysToEvents} by calling addDaysForEventInstance() for each of its * non-excluded instances. * @param timeZone @@ -852,6 +900,8 @@ export function addDaysForRecurringEvent( ? repeatRule.excludedDates.map(({ date }) => createDateWrapper({ date: getAllDayDateForTimezone(date, timeZone) })) : repeatRule.excludedDates const generatedEvents = generateEventOccurrences(event, timeZone, new Date(range.end)) + const allEvents: CalendarEvent[] = [] + for (const { startTime, endTime } of generatedEvents) { if (startTime.getTime() > range.end) break if (endTime.getTime() < range.start) continue @@ -867,8 +917,20 @@ export function addDaysForRecurringEvent( eventClone.startTime = new Date(startTime) eventClone.endTime = new Date(endTime) } - addDaysForEventInstance(daysToEvents, eventClone, range, timeZone) + allEvents.push(eventClone) + } + } + + const setPosRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYSETPOS) + const setPosRulesValues = setPosRules.map((rule) => rule.interval) + const shouldApplySetPos = isNotEmpty(setPosRules) && setPosRules.length < repeatRule.advancedRules.length + let eventCount = 0 + console.log({ allEvents }) + for (const event of allEvents) { + if (shouldApplySetPos && !bySetPosContainsEventOccurance(setPosRulesValues, event, ++eventCount, allEvents)) { + continue } + addDaysForEventInstance(daysToEvents, event, range, timeZone) } } From 3fb2590c2a36cdc832cd15a87ef0f78bdd461ffa Mon Sep 17 00:00:00 2001 From: and Date: Wed, 11 Dec 2024 17:08:03 +0100 Subject: [PATCH 10/29] Fixes BYWEEKNO expansion --- src/common/calendar/date/CalendarUtils.ts | 76 +++++++++++++++++------ 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 33c29d0a6ac8..35d33671b9a3 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -224,6 +224,7 @@ function applyByDayRules( validMonths: number[], wkst: WeekdayNumbers, hasWeekNo?: boolean, + monthDays?: number[], ) { if (parsedRules.length === 0) { return dates @@ -277,30 +278,52 @@ function applyByDayRules( continue } + const allowedDays: number[] = [] const weekChange = leadingValue ?? 0 const stopCondition = date.plus({ month: 1 }).set({ day: 1 }) const baseDate = date.set({ day: 1 }) + for (const allowedDay of monthDays ?? []) { + if (allowedDay > 0) { + allowedDays.push(allowedDay) + continue + } + + const day = baseDate.daysInMonth! - Math.abs(allowedDay) + 1 + allowedDays.push(day) + } + + const isAllowedInMonthDayRule = (day: number) => { + return allowedDays.length === 0 ? true : allowedDays.includes(day) + } + if (weekChange != 0) { - const absWeeks = weekChange > 0 ? weekChange : Math.ceil(baseDate.daysInMonth! / 7) - Math.abs(weekChange) let dt = baseDate.set({ day: 1 }) - while (dt.weekday != targetWeekDay) { - dt = dt.plus({ day: 1 }) + if (weekChange < 0) { + dt = dt + .set({ day: dt.daysInMonth }) + .set({ weekday: targetWeekDay }) + .minus({ week: Math.abs(weekChange) - 1 }) + } else { + while (dt.weekday != targetWeekDay) { + dt = dt.plus({ day: 1 }) + } + dt = dt.plus({ week: weekChange - 1 }) } - dt = dt.plus({ week: absWeeks - 1 }) - - if (dt.toMillis() >= baseDate.toMillis() && dt.toMillis() < stopCondition.toMillis()) { + if (dt.toMillis() >= baseDate.toMillis() && dt.toMillis() < stopCondition.toMillis() && isAllowedInMonthDayRule(dt.day)) { newDates.push(dt) } } else { let currentDate = baseDate while (currentDate < stopCondition) { const dt = currentDate.set({ weekday: targetWeekDay }) - if (dt.toMillis() >= baseDate.toMillis()) { + if (dt.toMillis() >= baseDate.toMillis() && isAllowedInMonthDayRule(dt.day)) { if (validMonths.length > 0 && validMonths.includes(dt.month)) { newDates.push(dt) + } else if (validMonths.length === 0) { + newDates.push(dt) } } currentDate = dt.plus({ week: 1 }) @@ -475,19 +498,30 @@ function applyWeekNo(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[ } const parsedWeekNumber = Number.parseInt(rule.interval) - let newDt: DateTime - + let newDt: DateTime = date + let weekNumber if (parsedWeekNumber < 0) { - newDt = date.set({ weekNumber: date.weeksInWeekYear - Math.abs(parsedWeekNumber) }).set({ weekday: wkst }) - for (let i = 0; i < 7; i++) { - newDates.push(newDt.plus({ day: i })) - } + weekNumber = date.weeksInWeekYear - Math.abs(parsedWeekNumber) + 1 + // I don't get why when I don't set this here it doesn't work for only YEARLY!!! + // But if I set here and re-set the week later, it works???? + // Also, it starts expanding for next week when the offset is -50? + // Is that a problem for negative only? + // newDt = date.set({ weekNumber: date.weeksInWeekYear - Math.abs(parsedWeekNumber) + 1 }) + console.log("Negative weeknumber ", { parsedWeekNumber, newDt }) } else { - newDt = date.set({ weekNumber: parsedWeekNumber }).set({ weekday: wkst }) + newDt = date.set({ weekNumber: parsedWeekNumber }) + weekNumber = parsedWeekNumber + console.log("Postive weeknumber ", { parsedWeekNumber, newDt }) + } - for (let i = 0; i < 7; i++) { - newDates.push(newDt.plus({ day: i })) + const yearOffset = newDt.toMillis() < date.toMillis() ? 1 : 0 + newDt = newDt.plus({ year: yearOffset }).set({ weekNumber }).set({ weekday: wkst }) + for (let i = 0; i < 7; i++) { + const finalDate = newDt.plus({ day: i }) + if (finalDate.year > newDt.year) { + break } + newDates.push(finalDate) } } } @@ -1179,8 +1213,11 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa RepeatPeriod.MONTHLY, validMonths, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, + false, + byMonthDayRules.map((rule) => Number.parseInt(rule.interval)), ), validMonths as MonthNumbers[], + eventStartTime, ) for (const event of events) { @@ -1209,7 +1246,11 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa RepeatPeriod.ANNUALLY, ) - const weekNoAppliedEvents = applyWeekNo(monthAppliedEvents, byWeekNoRules, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO) + const weekNoAppliedEvents = applyWeekNo( + monthAppliedEvents, + byWeekNoRules, + weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, + ) const yearDayAppliedEvents = applyYearDay(weekNoAppliedEvents, byYearDayRules) const monthDayAppliedEvents = applyByMonthDay(yearDayAppliedEvents, byMonthDayRules) @@ -1223,7 +1264,6 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa ), validMonths as MonthNumbers[], ) - for (const event of events) { const newStartTime = event.toJSDate() const newEndTime = allDay From 041938b9846238b417ecd6aa5fdcda8bdc5d80be Mon Sep 17 00:00:00 2001 From: mup Date: Mon, 16 Dec 2024 08:09:05 +0100 Subject: [PATCH 11/29] Implements BYYEARDAY filtering --- src/common/calendar/date/CalendarUtils.ts | 112 ++++++++++++++++------ 1 file changed, 81 insertions(+), 31 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 35d33671b9a3..0ed6e3c886a4 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -15,6 +15,7 @@ import { isNotNull, isSameDayOfDate, isValidDate, + memoized, neverNull, TIMESTAMP_ZERO_YEAR, } from "@tutao/tutanota-utils" @@ -225,6 +226,7 @@ function applyByDayRules( wkst: WeekdayNumbers, hasWeekNo?: boolean, monthDays?: number[], + yearDays?: number[], ) { if (parsedRules.length === 0) { return dates @@ -336,15 +338,26 @@ function applyByDayRules( continue } - if (weekChange != 0 && !hasWeekNo) { + if (weekChange !== 0 && !hasWeekNo) { if (!targetWeekDay) { - continue - } - - const absWeeks = weekChange > 0 ? weekChange : Math.ceil(date.daysInMonth! / 7) - Math.abs(weekChange) + 1 - const dt = date.set({ day: 1 }).set({ weekday: targetWeekDay }).plus({ week: absWeeks }) - if (dt.toMillis() >= date.toMillis()) { - newDates.push(dt) + let dt: DateTime + if (weekChange > 0) { + dt = date.set({ day: 1, month: 1 }).plus({ day: weekChange - 1 }) + } else { + console.log({ weekChange, day: Math.abs(weekChange) - 1 }) + dt = date.set({ day: 31, month: 12 }).minus({ day: Math.abs(weekChange) - 1 }) + } + if (dt.toMillis() < date.toMillis()) { + newDates.push(dt.plus({ year: 1 })) + } else { + newDates.push(dt) + } + } else { + const absWeeks = weekChange > 0 ? weekChange : Math.ceil(date.daysInMonth! / 7) - Math.abs(weekChange) + 1 + const dt = date.set({ day: 1 }).set({ weekday: targetWeekDay }).plus({ week: absWeeks }) + if (dt.toMillis() >= date.toMillis()) { + newDates.push(dt) + } } } else if (hasWeekNo) { // Handle WKST @@ -365,10 +378,10 @@ function applyByDayRules( continue } - const stopCondition = date.plus({ year: 1 }) - let currentDate = date.set({ weekday: targetWeekDay }) + const stopCondition = date.set({ day: 1 }).plus({ month: 1 }) + let currentDate = date.set({ day: 1, weekday: targetWeekDay }) - if (currentDate.toMillis() >= date.toMillis()) { + if (currentDate.toMillis() >= date.set({ day: 1 }).toMillis()) { newDates.push(currentDate) } @@ -378,20 +391,46 @@ function applyByDayRules( newDates.push(currentDate) currentDate = currentDate.plus({ week: 1 }) } - } else { - if (!targetWeekDay && weekChange > 0) { - const dt = date.set({ day: 1, month: 1 }).plus({ day: weekChange }) - if (dt.toMillis() < date.toMillis()) { - newDates.push(dt.plus({ year: 1 })) - } else { - newDates.push(dt) - } - } } } } } + if (frequency === RepeatPeriod.ANNUALLY) { + const getValidDaysInYear = memoized((year: number): number[] => { + const daysInYear = DateTime.fromObject({ year, month: 1, day: 1 }).daysInYear + const allowedDays: number[] = [] + for (const allowedDay of yearDays ?? []) { + if (allowedDay > 0) { + allowedDays.push(allowedDay) + continue + } + + const day = daysInYear - Math.abs(allowedDay) + 1 + } + + return allowedDays + }) + + const convertDateToDayOfYear = memoized((date: Date) => { + return (Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(date.getFullYear(), 0, 0)) / 24 / 60 / 60 / 1000 + }) + + const isValidDay = (date: DateTime) => { + const validDays = getValidDaysInYear(date.year) + + if (validDays.length === 0) { + return true + } + + const dayInYear = convertDateToDayOfYear(date.toJSDate()) + + return validDays.includes(dayInYear) + } + + return newDates.filter((date) => isValidDay(date)) + } + return newDates } @@ -470,12 +509,19 @@ function applyByMonthDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatR } else { // Monthly or Yearly if (targetDay >= 0) { - newDates.push(date.set({ day: targetDay })) + const dt = date.set({ day: targetDay }) + if (targetDay <= date.daysInMonth!) { + newDates.push(dt) + } continue } + const daysDiff = date.daysInMonth! - Math.abs(targetDay) + 1 if (daysDiff > 0) { - newDates.push(date.set({ day: daysDiff })) + const dt = date.set({ day: daysDiff }) + if (daysDiff <= date.daysInMonth!) { + newDates.push(dt) + } } } } @@ -529,7 +575,7 @@ function applyWeekNo(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[ return newDates } -function applyYearDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[]) { +function applyYearDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[], evaluateSameWeek: boolean, evaluateSameMonth: boolean) { if (parsedRules.length === 0) { return dates } @@ -546,7 +592,7 @@ function applyYearDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule let dt: DateTime if (targetDay < 0) { - dt = date.set({ day: 31, month: 12 }).minus({ day: Math.abs(targetDay) }) + dt = date.set({ day: 31, month: 12 }).minus({ day: Math.abs(targetDay) - 1 }) } else { dt = date.set({ day: 1, month: 1 }).plus({ day: targetDay - 1 }) } @@ -554,6 +600,10 @@ function applyYearDay(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule const yearOffset = dt.toMillis() < date.toMillis() ? 1 : 0 dt = dt.plus({ year: yearOffset }) + if ((evaluateSameWeek && date.weekNumber !== dt.weekNumber) || (evaluateSameMonth && date.month !== dt.month)) { + continue + } + newDates.push(dt) } } @@ -1238,7 +1288,7 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa const byWeekNoRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYWEEKNO) const weekStartRule = repeatRule.advancedRules.find((rule) => rule.ruleType === ByRule.WKST)?.interval const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) - + const validYearDays = byYearDayRules.map((rule) => Number.parseInt(rule.interval)) const monthAppliedEvents = applyByMonth( [DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, @@ -1246,12 +1296,8 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa RepeatPeriod.ANNUALLY, ) - const weekNoAppliedEvents = applyWeekNo( - monthAppliedEvents, - byWeekNoRules, - weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, - ) - const yearDayAppliedEvents = applyYearDay(weekNoAppliedEvents, byYearDayRules) + const weekNoAppliedEvents = applyWeekNo(monthAppliedEvents, byWeekNoRules, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO) + const yearDayAppliedEvents = applyYearDay(weekNoAppliedEvents, byYearDayRules, byWeekNoRules.length > 0, byMonthRules.length > 0) const monthDayAppliedEvents = applyByMonthDay(yearDayAppliedEvents, byMonthDayRules) const events = finishByRules( @@ -1261,8 +1307,12 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa RepeatPeriod.ANNUALLY, validMonths, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, + byWeekNoRules.length > 0, + [], + validYearDays, ), validMonths as MonthNumbers[], + eventStartTime, ) for (const event of events) { const newStartTime = event.toJSDate() From 2d643ae0a9d030a799f6cc36ba72f97cfe5b401b Mon Sep 17 00:00:00 2001 From: mup Date: Tue, 17 Dec 2024 12:10:52 +0100 Subject: [PATCH 12/29] Fixes SETPOS filtering --- src/common/calendar/date/CalendarUtils.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 0ed6e3c886a4..3f351e0419d2 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -928,7 +928,16 @@ function bySetPosContainsEventOccurance(posRulesValues: string[], event: Calenda } const { repeatRule } = event switch (repeatRule?.frequency) { - case RepeatPeriod.DAILY || RepeatPeriod.ANNUALLY: + case RepeatPeriod.DAILY: + if ( + negativeValues.some((value) => Number(value) < -366) || + positiveValues.some((value) => Number(value) > 366) || + positiveValues.includes(eventCount.toString()) + ) { + return true + } + break + case RepeatPeriod.ANNUALLY: if ( negativeValues.some((value) => Number(value) < -366) || positiveValues.some((value) => Number(value) > 366) || @@ -984,7 +993,7 @@ export function addDaysForRecurringEvent( ? repeatRule.excludedDates.map(({ date }) => createDateWrapper({ date: getAllDayDateForTimezone(date, timeZone) })) : repeatRule.excludedDates const generatedEvents = generateEventOccurrences(event, timeZone, new Date(range.end)) - const allEvents: CalendarEvent[] = [] + const allEvents: Map = new Map() for (const { startTime, endTime } of generatedEvents) { if (startTime.getTime() > range.end) break @@ -1001,7 +1010,7 @@ export function addDaysForRecurringEvent( eventClone.startTime = new Date(startTime) eventClone.endTime = new Date(endTime) } - allEvents.push(eventClone) + allEvents.set(eventClone.startTime.getTime(), eventClone) } } @@ -1009,9 +1018,9 @@ export function addDaysForRecurringEvent( const setPosRulesValues = setPosRules.map((rule) => rule.interval) const shouldApplySetPos = isNotEmpty(setPosRules) && setPosRules.length < repeatRule.advancedRules.length let eventCount = 0 - console.log({ allEvents }) - for (const event of allEvents) { - if (shouldApplySetPos && !bySetPosContainsEventOccurance(setPosRulesValues, event, ++eventCount, allEvents)) { + const events = Array.from(allEvents.values()) + for (const event of events) { + if (shouldApplySetPos && !bySetPosContainsEventOccurance(setPosRulesValues, event, ++eventCount, events)) { continue } addDaysForEventInstance(daysToEvents, event, range, timeZone) From f5524135b52cd057a7d230eb912ac8fec9ac8a67 Mon Sep 17 00:00:00 2001 From: mup Date: Tue, 17 Dec 2024 13:27:38 +0100 Subject: [PATCH 13/29] Simplifies generator with advanced rules This commit simplifies the Event Generator, removing duplicated code that are not needed. --- src/common/calendar/date/CalendarUtils.ts | 167 +++++----------------- 1 file changed, 36 insertions(+), 131 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 3f351e0419d2..4988af90f65f 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -1202,137 +1202,42 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa break } - if (frequency === RepeatPeriod.DAILY) { - const byMonthRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH) - const byMonthDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTHDAY) - const byDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYDAY) - const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) - - const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, maxDate, RepeatPeriod.DAILY) - const monthDayAppliedEvents = applyByMonthDay(monthAppliedEvents, byMonthDayRules, true) - const events = finishByRules( - applyByDayRules(monthDayAppliedEvents, byDayRules, RepeatPeriod.DAILY, validMonths, WEEKDAY_TO_NUMBER.MO), - validMonths as MonthNumbers[], - ) - - for (const event of events) { - const newStartTime = event.toJSDate() - const newEndTime = allDay - ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) - : DateTime.fromJSDate(newStartTime).plus(calcDuration).toJSDate() - - assertDateIsValid(newStartTime) - assertDateIsValid(newEndTime) - yield { startTime: newStartTime, endTime: newEndTime } - } - } else if (frequency === RepeatPeriod.WEEKLY) { - const byMonthRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH) - const byDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYDAY) - const weekStartRule = repeatRule.advancedRules.find((rule) => rule.ruleType === ByRule.WKST)?.interval - const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) - - const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, maxDate, RepeatPeriod.WEEKLY) - - const events = finishByRules( - applyByDayRules( - monthAppliedEvents, - byDayRules, - RepeatPeriod.WEEKLY, - validMonths, - weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, - ), - validMonths as MonthNumbers[], - eventStartTime, - ) - - for (const event of events) { - const newStartTime = event.toJSDate() - const newEndTime = allDay - ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) - : DateTime.fromJSDate(newStartTime).plus(calcDuration).toJSDate() - - assertDateIsValid(newStartTime) - assertDateIsValid(newEndTime) - yield { startTime: newStartTime, endTime: newEndTime } - } - } else if (frequency === RepeatPeriod.MONTHLY) { - const byMonthRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH) - const byDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYDAY) - const byMonthDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTHDAY) - const weekStartRule = repeatRule.advancedRules.find((rule) => rule.ruleType === ByRule.WKST)?.interval - const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) - - const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, maxDate, RepeatPeriod.MONTHLY) - const monthDayAppliedEvents = applyByMonthDay(monthAppliedEvents, byMonthDayRules) - - const events = finishByRules( - applyByDayRules( - monthDayAppliedEvents, - byDayRules, - RepeatPeriod.MONTHLY, - validMonths, - weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, - false, - byMonthDayRules.map((rule) => Number.parseInt(rule.interval)), - ), - validMonths as MonthNumbers[], - eventStartTime, - ) - - for (const event of events) { - const newStartTime = event.toJSDate() - const newEndTime = allDay - ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) - : DateTime.fromJSDate(newStartTime).plus(calcDuration).toJSDate() - - assertDateIsValid(newStartTime) - assertDateIsValid(newEndTime) - yield { startTime: newStartTime, endTime: newEndTime } - } - } else if (frequency === RepeatPeriod.ANNUALLY) { - const byMonthRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH) - const byDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYDAY) - const byMonthDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTHDAY) - const byYearDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYYEARDAY) - const byWeekNoRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYWEEKNO) - const weekStartRule = repeatRule.advancedRules.find((rule) => rule.ruleType === ByRule.WKST)?.interval - const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) - const validYearDays = byYearDayRules.map((rule) => Number.parseInt(rule.interval)) - const monthAppliedEvents = applyByMonth( - [DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], - byMonthRules, - maxDate, - RepeatPeriod.ANNUALLY, - ) - - const weekNoAppliedEvents = applyWeekNo(monthAppliedEvents, byWeekNoRules, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO) - const yearDayAppliedEvents = applyYearDay(weekNoAppliedEvents, byYearDayRules, byWeekNoRules.length > 0, byMonthRules.length > 0) - const monthDayAppliedEvents = applyByMonthDay(yearDayAppliedEvents, byMonthDayRules) - - const events = finishByRules( - applyByDayRules( - monthDayAppliedEvents, - byDayRules, - RepeatPeriod.ANNUALLY, - validMonths, - weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, - byWeekNoRules.length > 0, - [], - validYearDays, - ), - validMonths as MonthNumbers[], - eventStartTime, - ) - for (const event of events) { - const newStartTime = event.toJSDate() - const newEndTime = allDay - ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) - : DateTime.fromJSDate(newStartTime).plus(calcDuration).toJSDate() - - assertDateIsValid(newStartTime) - assertDateIsValid(newEndTime) - yield { startTime: newStartTime, endTime: newEndTime } - } + const byMonthRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTH) + const byDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYDAY) + const byMonthDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYMONTHDAY) + const byYearDayRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYYEARDAY) + const byWeekNoRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYWEEKNO) + const weekStartRule = repeatRule.advancedRules.find((rule) => rule.ruleType === ByRule.WKST)?.interval + const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) + const validYearDays = byYearDayRules.map((rule) => Number.parseInt(rule.interval)) + const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, maxDate, frequency) + const weekNoAppliedEvents = applyWeekNo(monthAppliedEvents, byWeekNoRules, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO) + const yearDayAppliedEvents = applyYearDay(weekNoAppliedEvents, byYearDayRules, byWeekNoRules.length > 0, byMonthRules.length > 0) + const monthDayAppliedEvents = applyByMonthDay(yearDayAppliedEvents, byMonthDayRules) + + const events = finishByRules( + applyByDayRules( + monthDayAppliedEvents, + byDayRules, + frequency, + validMonths, + weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, + byWeekNoRules.length > 0, + validMonths, + validYearDays, + ), + validMonths as MonthNumbers[], + eventStartTime, + ) + for (const event of events) { + const newStartTime = event.toJSDate() + const newEndTime = allDay + ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) + : DateTime.fromJSDate(newStartTime).plus(calcDuration).toJSDate() + + assertDateIsValid(newStartTime) + assertDateIsValid(newEndTime) + yield { startTime: newStartTime, endTime: newEndTime } } calcStartTime = incrementByRepeatPeriod(eventStartTime, downcast(repeatRule.frequency), interval * iteration, repeatTimeZone) From 3d46221265d1da390e26fd24a8863fc98cd99995 Mon Sep 17 00:00:00 2001 From: mup Date: Wed, 18 Dec 2024 13:16:14 +0100 Subject: [PATCH 14/29] [SDK] Implements BYMONTH expansion --- packages/node-mimimi/Cargo.lock | 2 + src/common/calendar/date/CalendarUtils.ts | 10 +- tuta-sdk/rust/sdk/Cargo.toml | 1 + tuta-sdk/rust/sdk/src/date.rs | 1 + .../rust/sdk/src/date/event_recurrence.rs | 369 ++++++++++++++++++ 5 files changed, 377 insertions(+), 6 deletions(-) create mode 100644 tuta-sdk/rust/sdk/src/date/event_recurrence.rs diff --git a/packages/node-mimimi/Cargo.lock b/packages/node-mimimi/Cargo.lock index cf5c052f8e25..62f35127025b 100644 --- a/packages/node-mimimi/Cargo.lock +++ b/packages/node-mimimi/Cargo.lock @@ -516,6 +516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -2181,6 +2182,7 @@ dependencies = [ "sha3", "simple_logger", "thiserror 2.0.11", + "time", "tokio", "uniffi", "zeroize", diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 4988af90f65f..72447011d688 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -225,7 +225,6 @@ function applyByDayRules( validMonths: number[], wkst: WeekdayNumbers, hasWeekNo?: boolean, - monthDays?: number[], yearDays?: number[], ) { if (parsedRules.length === 0) { @@ -285,7 +284,7 @@ function applyByDayRules( const stopCondition = date.plus({ month: 1 }).set({ day: 1 }) const baseDate = date.set({ day: 1 }) - for (const allowedDay of monthDays ?? []) { + for (const allowedDay of validMonths) { if (allowedDay > 0) { allowedDays.push(allowedDay) continue @@ -434,7 +433,7 @@ function applyByDayRules( return newDates } -function applyByMonth(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[], maxDate: Date, repeatPeriod: RepeatPeriod) { +function applyByMonth(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[], repeatPeriod: RepeatPeriod) { if (parsedRules.length === 0) { return dates } @@ -464,7 +463,7 @@ function applyByMonth(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule const dt = date.set({ month: targetMonth }) const yearOffset: number = date.year === dt.year && date.month > dt.month ? 1 : 0 - newDates.push(date.set({ month: targetMonth }).plus({ year: yearOffset })) + newDates.push(dt.plus({ year: yearOffset })) continue } @@ -1210,7 +1209,7 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa const weekStartRule = repeatRule.advancedRules.find((rule) => rule.ruleType === ByRule.WKST)?.interval const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) const validYearDays = byYearDayRules.map((rule) => Number.parseInt(rule.interval)) - const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, maxDate, frequency) + const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, frequency) const weekNoAppliedEvents = applyWeekNo(monthAppliedEvents, byWeekNoRules, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO) const yearDayAppliedEvents = applyYearDay(weekNoAppliedEvents, byYearDayRules, byWeekNoRules.length > 0, byMonthRules.length > 0) const monthDayAppliedEvents = applyByMonthDay(yearDayAppliedEvents, byMonthDayRules) @@ -1223,7 +1222,6 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa validMonths, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, byWeekNoRules.length > 0, - validMonths, validYearDays, ), validMonths as MonthNumbers[], diff --git a/tuta-sdk/rust/sdk/Cargo.toml b/tuta-sdk/rust/sdk/Cargo.toml index 61f70a2d4ebf..6e2271db5f92 100644 --- a/tuta-sdk/rust/sdk/Cargo.toml +++ b/tuta-sdk/rust/sdk/Cargo.toml @@ -43,6 +43,7 @@ form_urlencoded = "1" # allow initializing a simple_logger if the consuming application (or examples) want to do that. simple_logger = { version = "5.0.0", optional = true } sha3 = "0.10.8" +time = { version = "0.3.37", features = ["serde", "macros"] } [target.'cfg(target_os = "android")'.dependencies] android_log = "0.1.3" diff --git a/tuta-sdk/rust/sdk/src/date.rs b/tuta-sdk/rust/sdk/src/date.rs index b610f2591bca..16ec9c2b48c5 100644 --- a/tuta-sdk/rust/sdk/src/date.rs +++ b/tuta-sdk/rust/sdk/src/date.rs @@ -4,6 +4,7 @@ pub(crate) mod date_provider; #[cfg(not(test))] pub(crate) mod date_provider; mod date_time; +mod event_recurrence; pub use date_provider::DateProvider; pub use date_time::DateTime; diff --git a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs new file mode 100644 index 000000000000..0167ccfb17b7 --- /dev/null +++ b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs @@ -0,0 +1,369 @@ +use time::{Date, Month, PrimitiveDateTime, Weekday}; + +#[derive(PartialEq)] +enum ByRuleType { + BYMINUTE, + BYHOUR, + BYDAY, + BYMONTHDAY, + BYYEARDAY, + BYWEEKNO, + BYMONTH, + BYSETPOS, + WKST, +} + +impl ByRuleType { + fn value(&self) -> &str { + match *self { + ByRuleType::BYMINUTE => "0", + ByRuleType::BYHOUR => "1", + ByRuleType::BYDAY => "2", + ByRuleType::BYMONTHDAY => "3", + ByRuleType::BYYEARDAY => "4", + ByRuleType::BYWEEKNO => "5", + ByRuleType::BYMONTH => "6", + ByRuleType::BYSETPOS => "7", + ByRuleType::WKST => "8" + } + } + + fn from_str(value: &str) -> ByRuleType { + match value { + "0" => ByRuleType::BYMINUTE, + "1" => ByRuleType::BYHOUR, + "2" => ByRuleType::BYDAY, + "3" => ByRuleType::BYMONTHDAY, + "4" => ByRuleType::BYYEARDAY, + "5" => ByRuleType::BYWEEKNO, + "6" => ByRuleType::BYMONTH, + "7" => ByRuleType::BYSETPOS, + "8" => ByRuleType::WKST, + _ => panic!("Invalid ByRule {value}") + } + } +} + +#[derive(PartialEq)] +enum RepeatPeriod { + DAILY, + WEEKLY, + MONTHLY, + ANNUALLY, +} + +impl RepeatPeriod { + fn value(&self) -> &str { + match *self { + RepeatPeriod::DAILY => "0", + RepeatPeriod::WEEKLY => "1", + RepeatPeriod::MONTHLY => "2", + RepeatPeriod::ANNUALLY => "3" + } + } + + fn from_str(value: &str) -> RepeatPeriod { + match value { + "0" => RepeatPeriod::DAILY, + "1" => RepeatPeriod::WEEKLY, + "2" => RepeatPeriod::MONTHLY, + "3" => RepeatPeriod::ANNUALLY, + _ => panic!("Invalid RepeatPeriod {value}") + } + } +} + +pub struct ByRule { + by_rule: ByRuleType, + interval: String, +} + +pub struct RepeatRule { + frequency: RepeatPeriod, + by_rules: Vec, +} + +trait MonthNumber { + fn to_number(&self) -> i8; + fn from_number(number: i8) -> Month; +} + +impl MonthNumber for Month { + fn to_number(&self) -> i8 { + match *self { + Month::January => 1, + Month::February => 2, + Month::March => 3, + Month::April => 4, + Month::May => 5, + Month::June => 6, + Month::July => 7, + Month::August => 8, + Month::September => 9, + Month::October => 10, + Month::November => 11, + Month::December => 12, + } + } + + fn from_number(number: i8) -> Month { + match number { + 1 => Month::January, + 2 => Month::February, + 3 => Month::March, + 4 => Month::April, + 5 => Month::May, + 6 => Month::June, + 7 => Month::July, + 8 => Month::August, + 9 => Month::September, + 10 => Month::October, + 11 => Month::November, + 12 => Month::December, + _ => panic!("Invalid Month {number}") + } + } +} + +pub struct EventRecurrence; + +impl<'a> EventRecurrence { + pub fn new() -> Self { + EventRecurrence {} + } + pub fn generate_future_instances(&self, date: PrimitiveDateTime, repeat_rule: RepeatRule) -> Vec { + let by_month_rules: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYMONTH }).collect(); + let by_day_rules: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYDAY }).collect(); + let by_month_day_rules: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYMONTHDAY }).collect(); + let by_year_day_rules: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYYEARDAY }).collect(); + let by_week_no_rules: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYWEEKNO }).collect(); + let by_set_pos: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYSETPOS }).collect(); + + let week_start: i8 = match repeat_rule.by_rules.iter().find(|&x| { x.by_rule == ByRuleType::WKST }) { + Some(rule) => match rule.interval.as_str() { + "MO" => 1, + "TU" => 2, + "WE" => 3, + "TH" => 4, + "FR" => 5, + "SA" => 6, + "SU" => 7, + _ => 1 + }, + None => 1 + }; + + let valid_months: Vec = by_month_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); + let valid_year_days: Vec = by_year_day_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); + + let month_applied_events: Vec = self.apply_month_rules(&vec![date], &by_month_rules, &repeat_rule.frequency); + let week_no_applied_events: Vec = self.apply_week_no_rules(month_applied_events, &by_week_no_rules, week_start); + let year_day_applied_events: Vec = self.apply_year_day_rules(week_no_applied_events, &by_year_day_rules, by_week_no_rules.len() > 0, by_month_rules.len() > 0); + let month_day_applied_events: Vec = self.apply_month_day_rules(year_day_applied_events, &by_month_day_rules); + let day_applied_events: Vec = self.apply_day_rules(month_day_applied_events, &by_day_rules, &repeat_rule.frequency, valid_months.clone(), week_start, by_week_no_rules.len() > 0, valid_year_days); + + self.finish_rules(day_applied_events, &by_set_pos, valid_months.clone(), date.assume_utc().unix_timestamp()) + } + + fn apply_month_rules(&self, dates: &Vec, rules: &Vec<&'a ByRule>, frequency: &RepeatPeriod) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in dates { + let target_month = rule.interval.parse::().unwrap(); + + if frequency == &RepeatPeriod::WEEKLY { + let week_start = PrimitiveDateTime::new(Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Monday).unwrap(), date.time()); + let week_end = PrimitiveDateTime::new(Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Sunday).unwrap(), date.time()); + + let week_start_year = week_start.year(); + let week_end_year = week_end.year(); + + let week_start_month = week_start.month().to_number(); + let week_end_month = week_end.month().to_number(); + + let is_target_month = week_end_month == target_month || week_start_month == target_month; + + if week_start_year == week_end_year && week_start_month < week_end_month && is_target_month { + new_dates.push(date.clone()); + continue; + } else if week_start_year < week_end_year && is_target_month { + new_dates.push(date.clone()); + continue; + } + } else if frequency == &RepeatPeriod::ANNUALLY { + let new_date = date.clone().replace_month(Month::from_number(target_month)).unwrap(); + let years_to_add = if date.year() == new_date.year() && date.month().to_number() > target_month { 1 } else { 0 }; + + new_dates.push(new_date.replace_year(new_date.year() + years_to_add).unwrap()); + continue; + } + + if date.month().to_number() == target_month { + new_dates.push(date.clone()); + } + } + } + + new_dates + } + + fn apply_week_no_rules(&self, dates: Vec, rules: &Vec<&ByRule>, week_start: i8) -> Vec { + Vec::new() + } + + fn apply_year_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, evaluate_same_week: bool, evaluate_same_month: bool) -> Vec { + Vec::new() + } + + fn apply_month_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>) -> Vec { + Vec::new() + } + + fn apply_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, frequency: &RepeatPeriod, valid_months: Vec, week_start: i8, has_week_no: bool, valid_year_days: Vec) -> Vec { + Vec::new() + } + + fn finish_rules(&self, dates: Vec, set_pos_rules: &Vec<&ByRule>, valid_months: Vec, event_start_time: i64) -> Vec { + Vec::new() + } +} + +#[cfg(test)] +mod tests { + use time::{Date, Month, PrimitiveDateTime, Time}; + + use super::*; + + #[test] + fn test_parse_weekly_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::January, 23).unwrap(), time); + let invalid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::March, 11).unwrap(), time); + + assert_eq!(EventRecurrence {}.apply_month_rules(&vec![valid_date], &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], &RepeatPeriod::WEEKLY), vec![valid_date]); + + assert_eq!(EventRecurrence {}.apply_month_rules(&vec![invalid_date], &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], &RepeatPeriod::WEEKLY), vec![]); + } + + #[test] + fn test_parse_monthly_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::January, 23).unwrap(), time); + let invalid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::March, 11).unwrap(), time); + + assert_eq!(EventRecurrence {}.apply_month_rules(&vec![valid_date], &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], &RepeatPeriod::MONTHLY), vec![valid_date]); + + assert_eq!(EventRecurrence {}.apply_month_rules(&vec![invalid_date], &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], &RepeatPeriod::MONTHLY), vec![]); + } + + #[test] + fn test_parse_yearly_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::January, 23).unwrap(), time); + let to_next_year = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::March, 11).unwrap(), time); + + assert_eq!(EventRecurrence {}.apply_month_rules(&vec![valid_date], &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], &RepeatPeriod::ANNUALLY), vec![valid_date, valid_date.replace_month(Month::February).unwrap()]); + + // BYMONTH never limits on Yearly, just expands + assert_eq!( + EventRecurrence {}.apply_month_rules( + &vec![to_next_year], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + ), + vec![ + to_next_year.replace_year(2025).unwrap().replace_month(Month::January).unwrap(), + to_next_year.replace_year(2025).unwrap().replace_month(Month::February).unwrap(), + ] + ); + } + + #[test] + fn test_parse_daily_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::January, 23).unwrap(), time); + let second_valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::February, 12).unwrap(), time); + let invalid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::March, 11).unwrap(), time); + + assert_eq!(EventRecurrence {}.apply_month_rules(&vec![valid_date], &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], &RepeatPeriod::DAILY), vec![valid_date]); + + assert_eq!(EventRecurrence {}.apply_month_rules(&vec![invalid_date], &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], &RepeatPeriod::DAILY), vec![]); + } +} \ No newline at end of file From 234198da6b9d6f933ea5052f669fa1a426df2aff Mon Sep 17 00:00:00 2001 From: mup Date: Wed, 18 Dec 2024 15:07:33 +0100 Subject: [PATCH 15/29] [SDK] Implements BYWEEKNO expansion --- src/common/calendar/date/CalendarUtils.ts | 13 +- .../rust/sdk/src/date/event_recurrence.rs | 194 ++++++++++++++++-- 2 files changed, 183 insertions(+), 24 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 72447011d688..9afd0bc06921 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -1210,8 +1210,17 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) const validYearDays = byYearDayRules.map((rule) => Number.parseInt(rule.interval)) const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, frequency) - const weekNoAppliedEvents = applyWeekNo(monthAppliedEvents, byWeekNoRules, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO) - const yearDayAppliedEvents = applyYearDay(weekNoAppliedEvents, byYearDayRules, byWeekNoRules.length > 0, byMonthRules.length > 0) + + // RFC explicit says to not apply when freq != Annually + const weekNoAppliedEvents = + frequency === RepeatPeriod.ANNUALLY + ? applyWeekNo(monthAppliedEvents, byWeekNoRules, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO) + : monthAppliedEvents + const yearDayAppliedEvents = + frequency === RepeatPeriod.ANNUALLY + ? applyYearDay(weekNoAppliedEvents, byYearDayRules, byWeekNoRules.length > 0, byMonthRules.length > 0) + : weekNoAppliedEvents + const monthDayAppliedEvents = applyByMonthDay(yearDayAppliedEvents, byMonthDayRules) const events = finishByRules( diff --git a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs index 0167ccfb17b7..a8bf37a8b286 100644 --- a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs +++ b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs @@ -1,4 +1,7 @@ -use time::{Date, Month, PrimitiveDateTime, Weekday}; +use std::ops::Add; + +use time::{Date, Duration, Month, PrimitiveDateTime, Weekday}; +use time::util::weeks_in_year; #[derive(PartialEq)] enum ByRuleType { @@ -139,18 +142,18 @@ impl<'a> EventRecurrence { let by_week_no_rules: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYWEEKNO }).collect(); let by_set_pos: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYSETPOS }).collect(); - let week_start: i8 = match repeat_rule.by_rules.iter().find(|&x| { x.by_rule == ByRuleType::WKST }) { + let week_start: Weekday = match repeat_rule.by_rules.iter().find(|&x| { x.by_rule == ByRuleType::WKST }) { Some(rule) => match rule.interval.as_str() { - "MO" => 1, - "TU" => 2, - "WE" => 3, - "TH" => 4, - "FR" => 5, - "SA" => 6, - "SU" => 7, - _ => 1 + "MO" => Weekday::Monday, + "TU" => Weekday::Tuesday, + "WE" => Weekday::Wednesday, + "TH" => Weekday::Thursday, + "FR" => Weekday::Friday, + "SA" => Weekday::Saturday, + "SU" => Weekday::Sunday, + _ => Weekday::Monday }, - None => 1 + None => Weekday::Monday }; let valid_months: Vec = by_month_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); @@ -212,8 +215,42 @@ impl<'a> EventRecurrence { new_dates } - fn apply_week_no_rules(&self, dates: Vec, rules: &Vec<&ByRule>, week_start: i8) -> Vec { - Vec::new() + fn apply_week_no_rules(&self, dates: Vec, rules: &Vec<&'a ByRule>, week_start: Weekday) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in &dates { + let parsed_week = rule.interval.parse::().unwrap(); + + let mut new_date = date.clone(); + let mut week_number: u8; + + if parsed_week < 0 { + week_number = weeks_in_year(date.year()) - parsed_week.unsigned_abs() + 1 + } else { + new_date = new_date.replace_date(Date::from_iso_week_date(new_date.year(), parsed_week as u8, new_date.weekday()).unwrap()); + week_number = parsed_week as u8 + } + + let year_offset = if new_date.assume_utc().unix_timestamp() < date.assume_utc().unix_timestamp() { 1 } else { 0 }; + new_date = new_date.replace_date(Date::from_iso_week_date(new_date.year() + year_offset, week_number, week_start).unwrap()); + + for i in 0..7 { + let final_date = new_date.add(Duration::days(i)); + if final_date.year() > new_date.year() { + break; + } + + new_dates.push(final_date) + } + } + } + + new_dates } fn apply_year_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, evaluate_same_week: bool, evaluate_same_month: bool) -> Vec { @@ -224,7 +261,7 @@ impl<'a> EventRecurrence { Vec::new() } - fn apply_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, frequency: &RepeatPeriod, valid_months: Vec, week_start: i8, has_week_no: bool, valid_year_days: Vec) -> Vec { + fn apply_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, frequency: &RepeatPeriod, valid_months: Vec, week_start: Weekday, has_week_no: bool, valid_year_days: Vec) -> Vec { Vec::new() } @@ -245,7 +282,9 @@ mod tests { let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::January, 23).unwrap(), time); let invalid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::March, 11).unwrap(), time); - assert_eq!(EventRecurrence {}.apply_month_rules(&vec![valid_date], &vec![ + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_month_rules(&vec![valid_date], &vec![ &ByRule { by_rule: ByRuleType::BYMONTH, interval: "1".to_string(), @@ -256,7 +295,7 @@ mod tests { }, ], &RepeatPeriod::WEEKLY), vec![valid_date]); - assert_eq!(EventRecurrence {}.apply_month_rules(&vec![invalid_date], &vec![ + assert_eq!(event_recurrence.apply_month_rules(&vec![invalid_date], &vec![ &ByRule { by_rule: ByRuleType::BYMONTH, interval: "1".to_string(), @@ -274,7 +313,9 @@ mod tests { let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::January, 23).unwrap(), time); let invalid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::March, 11).unwrap(), time); - assert_eq!(EventRecurrence {}.apply_month_rules(&vec![valid_date], &vec![ + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_month_rules(&vec![valid_date], &vec![ &ByRule { by_rule: ByRuleType::BYMONTH, interval: "1".to_string(), @@ -285,7 +326,7 @@ mod tests { }, ], &RepeatPeriod::MONTHLY), vec![valid_date]); - assert_eq!(EventRecurrence {}.apply_month_rules(&vec![invalid_date], &vec![ + assert_eq!(event_recurrence.apply_month_rules(&vec![invalid_date], &vec![ &ByRule { by_rule: ByRuleType::BYMONTH, interval: "1".to_string(), @@ -303,7 +344,9 @@ mod tests { let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::January, 23).unwrap(), time); let to_next_year = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::March, 11).unwrap(), time); - assert_eq!(EventRecurrence {}.apply_month_rules(&vec![valid_date], &vec![ + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_month_rules(&vec![valid_date], &vec![ &ByRule { by_rule: ByRuleType::BYMONTH, interval: "1".to_string(), @@ -316,7 +359,7 @@ mod tests { // BYMONTH never limits on Yearly, just expands assert_eq!( - EventRecurrence {}.apply_month_rules( + event_recurrence.apply_month_rules( &vec![to_next_year], &vec![ &ByRule { @@ -344,7 +387,9 @@ mod tests { let second_valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::February, 12).unwrap(), time); let invalid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::March, 11).unwrap(), time); - assert_eq!(EventRecurrence {}.apply_month_rules(&vec![valid_date], &vec![ + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_month_rules(&vec![valid_date], &vec![ &ByRule { by_rule: ByRuleType::BYMONTH, interval: "1".to_string(), @@ -355,7 +400,7 @@ mod tests { }, ], &RepeatPeriod::DAILY), vec![valid_date]); - assert_eq!(EventRecurrence {}.apply_month_rules(&vec![invalid_date], &vec![ + assert_eq!(event_recurrence.apply_month_rules(&vec![invalid_date], &vec![ &ByRule { by_rule: ByRuleType::BYMONTH, interval: "1".to_string(), @@ -366,4 +411,109 @@ mod tests { }, ], &RepeatPeriod::DAILY), vec![]); } + + #[test] + fn test_parse_positive_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 31).unwrap(), time); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::January, 27).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new(base_date.add(Duration::days(i)), time)); + } + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_week_no_rules(vec![valid_date], &vec![ + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "5".to_string(), + }, + ], Weekday::Monday), valid_dates); + } + + #[test] + fn test_parse_wkst_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 31).unwrap(), time); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::January, 28).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new(base_date.add(Duration::days(i)), time)); + } + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_week_no_rules(vec![valid_date], &vec![ + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "5".to_string(), + }, + ], Weekday::Tuesday), valid_dates); + } + + #[test] + fn test_parse_negative_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::December, 4).unwrap(), time); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::November, 24).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new(base_date.add(Duration::days(i)), time)); + } + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_week_no_rules(vec![valid_date], &vec![ + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "-5".to_string(), + }, + ], Weekday::Monday), valid_dates); + } + + #[test] + fn test_parse_edge_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2026, Month::December, 29).unwrap(), time); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2026, Month::December, 28).unwrap(); + for i in 0..4 { + valid_dates.push(PrimitiveDateTime::new(base_date.add(Duration::days(i)), time)); + } + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_week_no_rules(vec![valid_date], &vec![ + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "-1".to_string(), + }, + ], Weekday::Monday), valid_dates); + } + + #[test] + fn test_parse_out_of_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 22).unwrap(), time); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2026, Month::January, 26).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new(base_date.add(Duration::days(i)), time)); + } + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_week_no_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "5".to_string(), + }, + ], Weekday::Monday), valid_dates); + } } \ No newline at end of file From 3a3d0898a09969661b6650aab8bc00a14651e96d Mon Sep 17 00:00:00 2001 From: mup Date: Thu, 19 Dec 2024 09:58:58 +0100 Subject: [PATCH 16/29] [SDK] Implements BYYEARDAY expansion --- .../rust/sdk/src/date/event_recurrence.rs | 117 +++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs index a8bf37a8b286..2e2177a5e8c7 100644 --- a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs +++ b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs @@ -1,4 +1,4 @@ -use std::ops::Add; +use std::ops::{Add, Sub}; use time::{Date, Duration, Month, PrimitiveDateTime, Weekday}; use time::util::weeks_in_year; @@ -254,7 +254,39 @@ impl<'a> EventRecurrence { } fn apply_year_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, evaluate_same_week: bool, evaluate_same_month: bool) -> Vec { - Vec::new() + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in &dates { + let parsed_day = rule.interval.parse::().unwrap(); + + let mut new_date: PrimitiveDateTime; + if parsed_day.is_negative() { + new_date = date.replace_month(Month::December).unwrap() + .replace_day(31).unwrap() + .sub(Duration::days((parsed_day.unsigned_abs() - 1) as i64)); + } else { + new_date = date.replace_month(Month::January).unwrap() + .replace_day(1).unwrap() + .add(Duration::days(parsed_day - 1)); + } + + let year_offset = if new_date.assume_utc().unix_timestamp() < date.assume_utc().unix_timestamp() { 1 } else { 0 }; + new_date = new_date.replace_year(new_date.year() + year_offset).unwrap(); + + if (evaluate_same_week && date.iso_week() != new_date.iso_week()) || (evaluate_same_month && date.month() != new_date.month()) { + continue; + } + + new_dates.push(new_date) + } + } + + new_dates } fn apply_month_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>) -> Vec { @@ -516,4 +548,85 @@ mod tests { }, ], Weekday::Monday), valid_dates); } + + #[test] + fn test_parse_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 1).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_year_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + } + ], false, false), [ + date.replace_day(9).unwrap() + ]); + } + + #[test] + fn test_parse_year_day_keep_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 1).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_year_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + } + ], true, false), []); + } + + #[test] + fn test_parse_year_day_keep_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 22).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_year_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + } + ], true, true), []); + } + + #[test] + fn test_parse_out_of_year_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 22).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_year_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + } + ], false, false), [ + date.replace_year(2026).unwrap().replace_day(9).unwrap() + ]); + } + + #[test] + fn test_parse_negative_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 22).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_year_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "-1".to_string(), + } + ], false, false), [ + date.replace_month(Month::December).unwrap().replace_day(31).unwrap() + ]); + } } \ No newline at end of file From 43756f9bca36c4a11dc178101ebd7d8bab0a490e Mon Sep 17 00:00:00 2001 From: mup Date: Thu, 19 Dec 2024 11:13:48 +0100 Subject: [PATCH 17/29] [SDK] Implements BYMONTHDAY expansion --- src/common/calendar/date/CalendarUtils.ts | 2 +- .../rust/sdk/src/date/event_recurrence.rs | 166 +++++++++++++++++- 2 files changed, 158 insertions(+), 10 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 9afd0bc06921..83347bed1292 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -1221,7 +1221,7 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa ? applyYearDay(weekNoAppliedEvents, byYearDayRules, byWeekNoRules.length > 0, byMonthRules.length > 0) : weekNoAppliedEvents - const monthDayAppliedEvents = applyByMonthDay(yearDayAppliedEvents, byMonthDayRules) + const monthDayAppliedEvents = applyByMonthDay(yearDayAppliedEvents, byMonthDayRules, frequency === RepeatPeriod.DAILY) const events = finishByRules( applyByDayRules( diff --git a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs index 2e2177a5e8c7..3f23c41d4d7f 100644 --- a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs +++ b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs @@ -162,7 +162,7 @@ impl<'a> EventRecurrence { let month_applied_events: Vec = self.apply_month_rules(&vec![date], &by_month_rules, &repeat_rule.frequency); let week_no_applied_events: Vec = self.apply_week_no_rules(month_applied_events, &by_week_no_rules, week_start); let year_day_applied_events: Vec = self.apply_year_day_rules(week_no_applied_events, &by_year_day_rules, by_week_no_rules.len() > 0, by_month_rules.len() > 0); - let month_day_applied_events: Vec = self.apply_month_day_rules(year_day_applied_events, &by_month_day_rules); + let month_day_applied_events: Vec = self.apply_month_day_rules(year_day_applied_events, &by_month_day_rules, &repeat_rule.frequency == &RepeatPeriod::DAILY); let day_applied_events: Vec = self.apply_day_rules(month_day_applied_events, &by_day_rules, &repeat_rule.frequency, valid_months.clone(), week_start, by_week_no_rules.len() > 0, valid_year_days); self.finish_rules(day_applied_events, &by_set_pos, valid_months.clone(), date.assume_utc().unix_timestamp()) @@ -177,7 +177,10 @@ impl<'a> EventRecurrence { for &rule in rules { for date in dates { - let target_month = rule.interval.parse::().unwrap(); + let target_month: i8 = match rule.interval.parse::() { + Ok(month) => month, + _ => continue + }; if frequency == &RepeatPeriod::WEEKLY { let week_start = PrimitiveDateTime::new(Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Monday).unwrap(), date.time()); @@ -199,10 +202,18 @@ impl<'a> EventRecurrence { continue; } } else if frequency == &RepeatPeriod::ANNUALLY { - let new_date = date.clone().replace_month(Month::from_number(target_month)).unwrap(); + let new_date = match date.clone().replace_month(Month::from_number(target_month)) { + Ok(dt) => dt, + _ => continue + }; + let years_to_add = if date.year() == new_date.year() && date.month().to_number() > target_month { 1 } else { 0 }; - new_dates.push(new_date.replace_year(new_date.year() + years_to_add).unwrap()); + new_dates.push(match new_date.replace_year(new_date.year() + years_to_add) { + Ok(date) => date, + _ => continue + }); + continue; } @@ -224,7 +235,10 @@ impl<'a> EventRecurrence { for &rule in rules { for date in &dates { - let parsed_week = rule.interval.parse::().unwrap(); + let parsed_week: i8 = match rule.interval.parse::() { + Ok(week) => week, + _ => continue + }; let mut new_date = date.clone(); let mut week_number: u8; @@ -262,7 +276,10 @@ impl<'a> EventRecurrence { for &rule in rules { for date in &dates { - let parsed_day = rule.interval.parse::().unwrap(); + let parsed_day: i64 = match rule.interval.parse::() { + Ok(day) => day, + _ => continue + }; let mut new_date: PrimitiveDateTime; if parsed_day.is_negative() { @@ -276,7 +293,10 @@ impl<'a> EventRecurrence { } let year_offset = if new_date.assume_utc().unix_timestamp() < date.assume_utc().unix_timestamp() { 1 } else { 0 }; - new_date = new_date.replace_year(new_date.year() + year_offset).unwrap(); + new_date = match new_date.replace_year(new_date.year() + year_offset) { + Ok(date) => date, + _ => continue + }; if (evaluate_same_week && date.iso_week() != new_date.iso_week()) || (evaluate_same_month && date.month() != new_date.month()) { continue; @@ -289,8 +309,50 @@ impl<'a> EventRecurrence { new_dates } - fn apply_month_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>) -> Vec { - Vec::new() + fn apply_month_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, is_daily_event: bool) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in &dates { + let target_day: i8 = match rule.interval.parse::() { + Ok(day) => day, + _ => continue + }; + let days_diff = date.month().length(date.year()) as i8 - target_day.unsigned_abs() as i8 + 1; + + if is_daily_event { + if target_day.is_positive() && date.day() == target_day.unsigned_abs() { + new_dates.push(date.clone()); + } else if target_day.is_negative() && days_diff == date.day() as i8 { + new_dates.push(date.clone()); + } + + continue; + } + + if target_day >= 0 && target_day.unsigned_abs() <= date.month().length(date.year()) { + let date = match date.replace_day(target_day.unsigned_abs()) { + Ok(date) => date, + _ => continue + }; + + new_dates.push(date); + } else if days_diff > 0 && target_day.unsigned_abs() <= date.month().length(date.year()) { + let date = match date.replace_day(days_diff.unsigned_abs()) { + Ok(date) => date, + _ => continue + }; + + new_dates.push(date); + } + } + } + + new_dates } fn apply_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, frequency: &RepeatPeriod, valid_months: Vec, week_start: Weekday, has_week_no: bool, valid_year_days: Vec) -> Vec { @@ -629,4 +691,90 @@ mod tests { date.replace_month(Month::December).unwrap().replace_day(31).unwrap() ]); } + + #[test] + fn test_parse_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 22).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_month_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "10".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "20".to_string(), + }, + ], false), [ + date.replace_day(10).unwrap(), + date.replace_day(20).unwrap() + ]); + } + + #[test] + fn test_parse_invalid_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 22).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_month_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "30".to_string(), + }, + ], false), []); + } + + #[test] + fn test_parse_daily_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 20).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_month_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "20".to_string(), + } + ], false), [ + date.replace_day(20).unwrap() + ]); + } + + #[test] + fn test_parse_negative_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_month_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "-1".to_string(), + }, + ], false), [ + date.replace_day(31).unwrap(), + ]); + } + + #[test] + fn test_parse_invalid_date_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_month_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "32".to_string(), + }, + ], false), []); + } } \ No newline at end of file From 8f83e7fa11457e4980de2bd71c586cda3133c5d7 Mon Sep 17 00:00:00 2001 From: mup Date: Fri, 20 Dec 2024 07:20:03 +0100 Subject: [PATCH 18/29] [SDK] Implements BYDAY expansion --- src/common/calendar/date/CalendarUtils.ts | 15 +- tuta-sdk/rust/Cargo.lock | 1 + tuta-sdk/rust/sdk/Cargo.toml | 1 + .../rust/sdk/src/date/event_recurrence.rs | 543 +++++++++++++++++- 4 files changed, 542 insertions(+), 18 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 83347bed1292..2bc2bf6845a8 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -225,6 +225,7 @@ function applyByDayRules( validMonths: number[], wkst: WeekdayNumbers, hasWeekNo?: boolean, + monthDays?: number[], yearDays?: number[], ) { if (parsedRules.length === 0) { @@ -284,7 +285,7 @@ function applyByDayRules( const stopCondition = date.plus({ month: 1 }).set({ day: 1 }) const baseDate = date.set({ day: 1 }) - for (const allowedDay of validMonths) { + for (const allowedDay of monthDays ?? []) { if (allowedDay > 0) { allowedDays.push(allowedDay) continue @@ -299,7 +300,7 @@ function applyByDayRules( } if (weekChange != 0) { - let dt = baseDate.set({ day: 1 }) + let dt = baseDate if (weekChange < 0) { dt = dt @@ -332,7 +333,7 @@ function applyByDayRules( } } else if (frequency === RepeatPeriod.ANNUALLY) { const weekChange = leadingValue ?? 0 - if (hasWeekNo && weekChange > 0) { + if (hasWeekNo && weekChange !== 0) { console.warn("Invalid repeat rule, can't use BYWEEKNO with Week Offset on BYDAY") continue } @@ -343,7 +344,6 @@ function applyByDayRules( if (weekChange > 0) { dt = date.set({ day: 1, month: 1 }).plus({ day: weekChange - 1 }) } else { - console.log({ weekChange, day: Math.abs(weekChange) - 1 }) dt = date.set({ day: 31, month: 12 }).minus({ day: Math.abs(weekChange) - 1 }) } if (dt.toMillis() < date.toMillis()) { @@ -365,7 +365,7 @@ function applyByDayRules( } const dt = date.set({ weekday: targetWeekDay }) const intervalStart = date.set({ weekday: wkst }) - if (dt.toMillis() > intervalStart.plus({ week: 1 }).toMillis()) { + if (dt.toMillis() > intervalStart.plus({ week: 1 }).toMillis() || dt.toMillis() < date.toMillis()) { // Do nothing } else if (dt.toMillis() < intervalStart.toMillis()) { newDates.push(intervalStart.plus({ week: 1 })) @@ -377,7 +377,7 @@ function applyByDayRules( continue } - const stopCondition = date.set({ day: 1 }).plus({ month: 1 }) + const stopCondition = date.set({ day: 1 }).plus({ year: 1 }) let currentDate = date.set({ day: 1, weekday: targetWeekDay }) if (currentDate.toMillis() >= date.set({ day: 1 }).toMillis()) { @@ -406,6 +406,7 @@ function applyByDayRules( } const day = daysInYear - Math.abs(allowedDay) + 1 + allowedDays.push(day) } return allowedDays @@ -1210,6 +1211,7 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa const validMonths = byMonthRules.map((rule) => Number.parseInt(rule.interval)) const validYearDays = byYearDayRules.map((rule) => Number.parseInt(rule.interval)) const monthAppliedEvents = applyByMonth([DateTime.fromJSDate(calcStartTime, { zone: repeatTimeZone })], byMonthRules, frequency) + const validMonthDays = byMonthDayRules.map((rule) => Number.parseInt(rule.interval)) // RFC explicit says to not apply when freq != Annually const weekNoAppliedEvents = @@ -1231,6 +1233,7 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa validMonths, weekStartRule ? WEEKDAY_TO_NUMBER[weekStartRule] : WEEKDAY_TO_NUMBER.MO, byWeekNoRules.length > 0, + validMonthDays, validYearDays, ), validMonths as MonthNumbers[], diff --git a/tuta-sdk/rust/Cargo.lock b/tuta-sdk/rust/Cargo.lock index de345d56c0de..80f0642b5cc1 100644 --- a/tuta-sdk/rust/Cargo.lock +++ b/tuta-sdk/rust/Cargo.lock @@ -2230,6 +2230,7 @@ dependencies = [ "pqcrypto-traits", "rand", "rand_core", + "regex", "rsa", "rustls", "serde", diff --git a/tuta-sdk/rust/sdk/Cargo.toml b/tuta-sdk/rust/sdk/Cargo.toml index 6e2271db5f92..63ded061baa8 100644 --- a/tuta-sdk/rust/sdk/Cargo.toml +++ b/tuta-sdk/rust/sdk/Cargo.toml @@ -44,6 +44,7 @@ form_urlencoded = "1" simple_logger = { version = "5.0.0", optional = true } sha3 = "0.10.8" time = { version = "0.3.37", features = ["serde", "macros"] } +regex = "1.11.1" [target.'cfg(target_os = "android")'.dependencies] android_log = "0.1.3" diff --git a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs index 3f23c41d4d7f..2e253cff13ff 100644 --- a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs +++ b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs @@ -1,6 +1,7 @@ use std::ops::{Add, Sub}; -use time::{Date, Duration, Month, PrimitiveDateTime, Weekday}; +use regex::Regex; +use time::{Date, Duration, Month, PrimitiveDateTime, Time, Weekday}; use time::util::weeks_in_year; #[derive(PartialEq)] @@ -87,12 +88,16 @@ pub struct RepeatRule { } trait MonthNumber { - fn to_number(&self) -> i8; - fn from_number(number: i8) -> Month; + fn to_number(&self) -> u8; + fn from_number(number: u8) -> Month; +} + +trait WeekdayString { + fn from_short(short_weekday: &str) -> Weekday; } impl MonthNumber for Month { - fn to_number(&self) -> i8 { + fn to_number(&self) -> u8 { match *self { Month::January => 1, Month::February => 2, @@ -109,7 +114,7 @@ impl MonthNumber for Month { } } - fn from_number(number: i8) -> Month { + fn from_number(number: u8) -> Month { match number { 1 => Month::January, 2 => Month::February, @@ -128,6 +133,31 @@ impl MonthNumber for Month { } } +impl WeekdayString for Weekday { + fn from_short(short_weekday: &str) -> Weekday { + match short_weekday { + "MO" => Weekday::Monday, + "TU" => Weekday::Tuesday, + "WE" => Weekday::Wednesday, + "TH" => Weekday::Thursday, + "FR" => Weekday::Friday, + "SA" => Weekday::Saturday, + "SU" => Weekday::Sunday, + _ => panic!("Invalid Weekday {short_weekday}") + } + } +} + +trait DateExpansion { + fn add_month(&self) -> Date; +} + +impl DateExpansion for Date { + fn add_month(&self) -> Date { + self.add(Duration::days(i64::from(self.month().length(self.year())))) + } +} + pub struct EventRecurrence; impl<'a> EventRecurrence { @@ -156,14 +186,15 @@ impl<'a> EventRecurrence { None => Weekday::Monday }; - let valid_months: Vec = by_month_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); - let valid_year_days: Vec = by_year_day_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); + let valid_months: Vec = by_month_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); + let valid_month_days: Vec = by_month_day_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); + let valid_year_days: Vec = by_year_day_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); let month_applied_events: Vec = self.apply_month_rules(&vec![date], &by_month_rules, &repeat_rule.frequency); let week_no_applied_events: Vec = self.apply_week_no_rules(month_applied_events, &by_week_no_rules, week_start); let year_day_applied_events: Vec = self.apply_year_day_rules(week_no_applied_events, &by_year_day_rules, by_week_no_rules.len() > 0, by_month_rules.len() > 0); let month_day_applied_events: Vec = self.apply_month_day_rules(year_day_applied_events, &by_month_day_rules, &repeat_rule.frequency == &RepeatPeriod::DAILY); - let day_applied_events: Vec = self.apply_day_rules(month_day_applied_events, &by_day_rules, &repeat_rule.frequency, valid_months.clone(), week_start, by_week_no_rules.len() > 0, valid_year_days); + let day_applied_events: Vec = self.apply_day_rules(month_day_applied_events, &by_day_rules, &repeat_rule.frequency, valid_months.clone(), week_start, by_week_no_rules.len() > 0, valid_month_days, valid_year_days); self.finish_rules(day_applied_events, &by_set_pos, valid_months.clone(), date.assume_utc().unix_timestamp()) } @@ -177,7 +208,7 @@ impl<'a> EventRecurrence { for &rule in rules { for date in dates { - let target_month: i8 = match rule.interval.parse::() { + let target_month: u8 = match rule.interval.parse::() { Ok(month) => month, _ => continue }; @@ -355,11 +386,237 @@ impl<'a> EventRecurrence { new_dates } - fn apply_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, frequency: &RepeatPeriod, valid_months: Vec, week_start: Weekday, has_week_no: bool, valid_year_days: Vec) -> Vec { - Vec::new() + fn apply_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, frequency: &RepeatPeriod, valid_months: Vec, week_start: Weekday, has_week_no: bool, valid_month_days: Vec, valid_year_days: Vec) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + let regex = Regex::new(r"^([-+]?\d{0,3})([a-zA-Z]{2})?$").unwrap(); + + for &rule in rules { + for date in &dates { + let Some(parsed_rule) = regex.captures(rule.interval.as_str()) else { continue }; + let target_week_day = parsed_rule.get(2); + let leading_value = parsed_rule.get(1); + date.add(Duration::days(i64::from(Month::December.length(2025)))); + if frequency == &RepeatPeriod::DAILY && target_week_day.is_some() && date.weekday() == Weekday::from_short(target_week_day.unwrap().as_str()) { + new_dates.push(date.clone()) + } else if frequency == &RepeatPeriod::WEEKLY && target_week_day.is_some() { + let mut new_date = date.replace_date(Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::from_short(target_week_day.unwrap().as_str())).unwrap()); + let interval_start = date.replace_date(Date::from_iso_week_date(date.year(), date.iso_week(), week_start).unwrap()); + + if new_date.assume_utc().unix_timestamp() > interval_start.add(Duration::weeks(1)).assume_utc().unix_timestamp() { + continue; + } else if new_date.assume_utc().unix_timestamp() < interval_start.assume_utc().unix_timestamp() { + new_date = new_date.add(Duration::weeks(1)); + } + + if valid_months.len() == 0 || valid_months.contains(&new_date.month().to_number()) { + new_dates.push(new_date) + } + } else if frequency == &RepeatPeriod::MONTHLY && target_week_day.is_some() { + let mut allowed_days: Vec = Vec::new(); + + let week_change = match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { + Ok(val) => { val } + _ => { 0 } + }; + + let base_date = date.replace_day(1).unwrap(); + let stop_condition = PrimitiveDateTime::new(base_date.date().add_month(), base_date.time()); + + for allowed_day in &valid_month_days { + if allowed_day.is_positive() { + allowed_days.push(allowed_day.unsigned_abs()); + continue; + } + + let day = base_date.month().length(date.year()) - allowed_day.unsigned_abs() + 1; + allowed_days.push(day); + } + + let is_allowed_in_month_day = |day: u8| -> bool { + if allowed_days.len() == 0 { + return true; + } + + allowed_days.contains(&day) + }; + + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + + if week_change != 0 { + let mut new_date = base_date; + if week_change.is_negative() { + new_date = new_date.replace_day(new_date.month().length(new_date.year())).unwrap(); + new_date = new_date.replace_date(Date::from_iso_week_date(new_date.year(), new_date.iso_week(), parsed_weekday).unwrap()); + + let new_week = new_date.iso_week() - week_change.unsigned_abs() - 1; + new_date = new_date.replace_date(Date::from_iso_week_date(new_date.year(), new_week, new_date.weekday()).unwrap()) + } else { + while new_date.weekday() != parsed_weekday { + new_date = new_date.add(Duration::days(1)); + } + + new_date = new_date.replace_date(Date::from_iso_week_date(new_date.year(), new_date.iso_week() + week_change.unsigned_abs() - 1, new_date.weekday()).unwrap()) + } + + if new_date.assume_utc().unix_timestamp() >= base_date.assume_utc().unix_timestamp() + && new_date.assume_utc().unix_timestamp() <= stop_condition.assume_utc().unix_timestamp() + && is_allowed_in_month_day(new_date.day()) { + new_dates.push(new_date) + } + } else { + let mut current_date = base_date; + while current_date.assume_utc().unix_timestamp() < stop_condition.assume_utc().unix_timestamp() { + let new_date = current_date.replace_date(Date::from_iso_week_date(current_date.year(), current_date.iso_week(), parsed_weekday).unwrap()); + if new_date.assume_utc().unix_timestamp() >= base_date.assume_utc().unix_timestamp() && is_allowed_in_month_day(new_date.day()) { + if valid_months.len() > 0 && valid_months.contains(&new_date.month().to_number()) { + new_dates.push(new_date) + } else if valid_months.len() == 0 { + new_dates.push(new_date) + } + } + + current_date = new_date.add(Duration::days(7)); + } + } + } else if frequency == &RepeatPeriod::ANNUALLY { + let week_change = match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { + Ok(val) => { val } + _ => { 0 } + }; + + if has_week_no && week_change != 0 { + println!("Invalid repeat rule, can't use BYWEEKNO with Week Offset on BYDAY"); + continue; + } + + if week_change != 0 && !has_week_no { + let mut new_date: PrimitiveDateTime; + + if !target_week_day.is_some() { + if week_change > 0 { + new_date = date.replace_day(1).unwrap().replace_month(Month::January).unwrap().add(Duration::days(week_change - 1)) + } else { + new_date = date.replace_month(Month::December).unwrap().replace_day(31).unwrap().sub(Duration::days(week_change.abs() - 1)) + } + } else { + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + + if week_change > 0 { + new_date = date.replace_day(1).unwrap().replace_month(Month::January).unwrap().add(Duration::weeks(week_change - 1)); + + while new_date.weekday() != parsed_weekday { + new_date = new_date.add(Duration::days(1)); + } + } else { + new_date = date.replace_month(Month::December).unwrap().replace_day(31).unwrap().sub(Duration::weeks(week_change.abs() - 1)); + while new_date.weekday() != parsed_weekday { + new_date = new_date.sub(Duration::days(1)); + } + } + } + + if new_date.assume_utc().unix_timestamp() < date.assume_utc().unix_timestamp() { + match new_date.replace_year(new_date.year() + 1) { + Ok(dt) => new_dates.push(dt), + _ => continue + } + } else { + new_dates.push(new_date) + } + } else if has_week_no { + if !target_week_day.is_some() { + continue; + } + + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + let new_date = date.replace_date(Date::from_iso_week_date(date.year(), date.iso_week(), parsed_weekday).unwrap()); + let interval_start = date.replace_date(Date::from_iso_week_date(date.year(), date.iso_week(), week_start).unwrap()); + let week_ahead = interval_start.add(Duration::days(7)); + + if new_date.assume_utc().unix_timestamp() > week_ahead.assume_utc().unix_timestamp() || new_date.assume_utc().unix_timestamp() < date.assume_utc().unix_timestamp() {} else if new_date.assume_utc().unix_timestamp() < interval_start.assume_utc().unix_timestamp() { + new_dates.push(interval_start.add(Duration::days(7))); + } else { + new_dates.push(new_date); + } + } else { + if !target_week_day.is_some() { + continue; + } + + let day_one = date.replace_day(1).unwrap(); + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + + let stop_date = match Date::from_calendar_date(date.year() + 1, date.month(), date.day()) { + Ok(date) => date, + _ => continue + }; + + let stop_condition = date.replace_date(stop_date); + let mut current_date = date.replace_date(Date::from_iso_week_date(date.year(), day_one.iso_week(), parsed_weekday).unwrap()); + + if current_date.assume_utc().unix_timestamp() >= day_one.assume_utc().unix_timestamp() { + new_dates.push(current_date); + } + + current_date = current_date.add(Duration::days(7)); + + while current_date.assume_utc().unix_timestamp() < stop_condition.assume_utc().unix_timestamp() { + new_dates.push(current_date); + current_date = current_date.add(Duration::days(7)); + } + } + } + } + } + + if frequency == &RepeatPeriod::ANNUALLY { + return new_dates.iter().filter(|date| { self.is_valid_day_in_year(**date, valid_year_days.clone()) }).map(|date| { *date }).collect(); + } + + new_dates + } + + fn convert_date_to_day_of_year(&self, date: PrimitiveDateTime) -> u16 { + let day = (date.replace_time(Time::from_hms(0, 0, 0).unwrap()).assume_utc().unix_timestamp() - PrimitiveDateTime::new(Date::from_calendar_date(date.year() - 1, Month::December, 31).unwrap(), date.time()).assume_utc().unix_timestamp()) / 24 / 60 / 60 / 1000; + return day as u16; + } + + fn get_valid_days_in_year(&self, year: i32, valid_year_days: &Vec) -> Vec { + let days_in_year = Date::from_calendar_date(year, Month::December, 31).unwrap().ordinal(); + let mut allowed_days: Vec = Vec::new(); + + for allowed_day in valid_year_days { + if allowed_day > &0 { + allowed_days.push(allowed_day.abs() as u16); + continue; + } + + let day = days_in_year - allowed_day.unsigned_abs() + 1; + allowed_days.push(day); + } + + allowed_days + } + + fn is_valid_day_in_year(&self, date: PrimitiveDateTime, valid_year_days: Vec) -> bool { + let valid_days = self.get_valid_days_in_year(date.year(), &valid_year_days); + + if valid_days.len() == 0 { + return true; + } + + let day_in_year = self.convert_date_to_day_of_year(date); + + let is_valid = valid_days.contains(&day_in_year); + + return is_valid; } - fn finish_rules(&self, dates: Vec, set_pos_rules: &Vec<&ByRule>, valid_months: Vec, event_start_time: i64) -> Vec { + fn finish_rules(&self, dates: Vec, set_pos_rules: &Vec<&ByRule>, valid_months: Vec, event_start_time: i64) -> Vec { Vec::new() } } @@ -777,4 +1034,266 @@ mod tests { }, ], false), []); } + + #[test] + fn test_parse_by_day_daily() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + } + ], &RepeatPeriod::DAILY, vec![], Weekday::Monday, false, vec![], vec![]), [date]); + } + + #[test] + fn test_parse_by_day_daily_invalid() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 8).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + } + ], &RepeatPeriod::DAILY, vec![], Weekday::Monday, false, vec![], vec![]), []); + } + + #[test] + fn test_parse_by_day_weekly() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 9).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "SA".to_string(), + }, + ], &RepeatPeriod::WEEKLY, vec![], Weekday::Monday, false, vec![], vec![]), [ + date.replace_day(10).unwrap(), + date.replace_day(11).unwrap() + ]); + } + + #[test] + fn test_parse_by_day_monthly() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 6).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + // Can be WEEKDAY + WEEK + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + }, + ], &RepeatPeriod::MONTHLY, vec![], Weekday::Monday, false, vec![], vec![]), [ + date, + date.replace_day(13).unwrap(), + date.replace_day(20).unwrap(), + date.replace_day(27).unwrap() + ]); + } + + #[test] + fn test_parse_by_day_monthly_with_monthday() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 6).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + let rules = vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "7".to_string(), + }, + ]; + let by_day_rules: Vec<&ByRule> = rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYDAY }).collect(); + let by_month_day_rules: Vec<&ByRule> = rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYMONTHDAY }).collect(); + + let valid_month_days: Vec = by_month_day_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &by_day_rules, &RepeatPeriod::MONTHLY, vec![], Weekday::Monday, false, valid_month_days, vec![]), []); + } + + #[test] + fn test_parse_by_day_monthly_with_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + // Can be WEEKDAY + WEEK + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + }, + ], &RepeatPeriod::MONTHLY, vec![], Weekday::Monday, false, vec![], vec![]), [ + date.replace_day(13).unwrap() + ]); + } + + #[test] + fn test_parse_by_day_monthly_with_monthday_and_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 6).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + let rules = vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "7".to_string(), + }, + ]; + let by_day_rules: Vec<&ByRule> = rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYDAY }).collect(); + let by_month_day_rules: Vec<&ByRule> = rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYMONTHDAY }).collect(); + + let valid_month_days: Vec = by_month_day_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &by_day_rules, &RepeatPeriod::MONTHLY, vec![], Weekday::Monday, false, valid_month_days, vec![]), []); + } + + #[test] + fn test_parse_by_day_yearly() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 6).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + let end_date = date.replace_year(2026).unwrap(); + let mut current_date = date; + let mut expected_dates: Vec = Vec::new(); + + while current_date.assume_utc().unix_timestamp() < end_date.assume_utc().unix_timestamp() { + expected_dates.push(current_date); + current_date = current_date.add(Duration::days(7)) + } + + // Can be WEEKDAY + WEEK + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + }, + ], &RepeatPeriod::ANNUALLY, vec![], Weekday::Monday, false, vec![], vec![]), expected_dates); + } + + #[test] + fn test_parse_by_day_yearly_with_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + // Can be WEEKDAY + WEEK + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + }, + ], &RepeatPeriod::ANNUALLY, vec![], Weekday::Monday, false, vec![], vec![]), [ + date.replace_day(13).unwrap(), + ]); + } + + #[test] + fn test_parse_by_day_yearly_with_ordinal_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + // Can be WEEKDAY + WEEK + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "35".to_string(), + }, + ], &RepeatPeriod::ANNUALLY, vec![], Weekday::Monday, false, vec![], vec![]), [ + date.replace_month(Month::February).unwrap().replace_day(4).unwrap(), + ]); + } + + #[test] + fn test_parse_by_day_yearly_with_weekno() { + //FIXME + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 6).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "6".to_string(), + }, + ], &RepeatPeriod::ANNUALLY, vec![], Weekday::Monday, true, vec![], vec![]), [date]); + } + + #[test] + fn test_parse_by_day_yearly_with_unmatch_weekno() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "35".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "7".to_string(), + }, + ], &RepeatPeriod::ANNUALLY, vec![], Weekday::Monday, true, vec![], vec![]), []); + } + + #[test] + fn test_parse_by_day_yearly_with_invalid_rule() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); + + let event_recurrence = EventRecurrence {}; + // Can be WEEKDAY + WEEK + + assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "6".to_string(), + }, + ], &RepeatPeriod::ANNUALLY, vec![], Weekday::Monday, true, vec![], vec![]), []); + } } \ No newline at end of file From a8d9330889af92b6628c53a008ccb04ec8521974 Mon Sep 17 00:00:00 2001 From: mup Date: Mon, 6 Jan 2025 15:04:41 +0100 Subject: [PATCH 19/29] [SDK] Implements tests for the complete recurrence generation flow --- .../rust/sdk/src/date/event_recurrence.rs | 3940 +++++++++++------ 1 file changed, 2689 insertions(+), 1251 deletions(-) diff --git a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs index 2e253cff13ff..d37a0ad5c62b 100644 --- a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs +++ b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs @@ -1,1299 +1,2737 @@ use std::ops::{Add, Sub}; use regex::Regex; -use time::{Date, Duration, Month, PrimitiveDateTime, Time, Weekday}; use time::util::weeks_in_year; +use time::{Date, Duration, Month, PrimitiveDateTime, Weekday}; -#[derive(PartialEq)] +#[derive(PartialEq, Clone)] enum ByRuleType { - BYMINUTE, - BYHOUR, - BYDAY, - BYMONTHDAY, - BYYEARDAY, - BYWEEKNO, - BYMONTH, - BYSETPOS, - WKST, + BYMINUTE, + BYHOUR, + BYDAY, + BYMONTHDAY, + BYYEARDAY, + BYWEEKNO, + BYMONTH, + BYSETPOS, + WKST, } impl ByRuleType { - fn value(&self) -> &str { - match *self { - ByRuleType::BYMINUTE => "0", - ByRuleType::BYHOUR => "1", - ByRuleType::BYDAY => "2", - ByRuleType::BYMONTHDAY => "3", - ByRuleType::BYYEARDAY => "4", - ByRuleType::BYWEEKNO => "5", - ByRuleType::BYMONTH => "6", - ByRuleType::BYSETPOS => "7", - ByRuleType::WKST => "8" - } - } - - fn from_str(value: &str) -> ByRuleType { - match value { - "0" => ByRuleType::BYMINUTE, - "1" => ByRuleType::BYHOUR, - "2" => ByRuleType::BYDAY, - "3" => ByRuleType::BYMONTHDAY, - "4" => ByRuleType::BYYEARDAY, - "5" => ByRuleType::BYWEEKNO, - "6" => ByRuleType::BYMONTH, - "7" => ByRuleType::BYSETPOS, - "8" => ByRuleType::WKST, - _ => panic!("Invalid ByRule {value}") - } - } + fn value(&self) -> &str { + match *self { + ByRuleType::BYMINUTE => "0", + ByRuleType::BYHOUR => "1", + ByRuleType::BYDAY => "2", + ByRuleType::BYMONTHDAY => "3", + ByRuleType::BYYEARDAY => "4", + ByRuleType::BYWEEKNO => "5", + ByRuleType::BYMONTH => "6", + ByRuleType::BYSETPOS => "7", + ByRuleType::WKST => "8", + } + } + + fn from_str(value: &str) -> ByRuleType { + match value { + "0" => ByRuleType::BYMINUTE, + "1" => ByRuleType::BYHOUR, + "2" => ByRuleType::BYDAY, + "3" => ByRuleType::BYMONTHDAY, + "4" => ByRuleType::BYYEARDAY, + "5" => ByRuleType::BYWEEKNO, + "6" => ByRuleType::BYMONTH, + "7" => ByRuleType::BYSETPOS, + "8" => ByRuleType::WKST, + _ => panic!("Invalid ByRule {value}"), + } + } } -#[derive(PartialEq)] +#[derive(PartialEq, Clone)] enum RepeatPeriod { - DAILY, - WEEKLY, - MONTHLY, - ANNUALLY, + DAILY, + WEEKLY, + MONTHLY, + ANNUALLY, } impl RepeatPeriod { - fn value(&self) -> &str { - match *self { - RepeatPeriod::DAILY => "0", - RepeatPeriod::WEEKLY => "1", - RepeatPeriod::MONTHLY => "2", - RepeatPeriod::ANNUALLY => "3" - } - } - - fn from_str(value: &str) -> RepeatPeriod { - match value { - "0" => RepeatPeriod::DAILY, - "1" => RepeatPeriod::WEEKLY, - "2" => RepeatPeriod::MONTHLY, - "3" => RepeatPeriod::ANNUALLY, - _ => panic!("Invalid RepeatPeriod {value}") - } - } + fn value(&self) -> &str { + match *self { + RepeatPeriod::DAILY => "0", + RepeatPeriod::WEEKLY => "1", + RepeatPeriod::MONTHLY => "2", + RepeatPeriod::ANNUALLY => "3", + } + } + + fn from_str(value: &str) -> RepeatPeriod { + match value { + "0" => RepeatPeriod::DAILY, + "1" => RepeatPeriod::WEEKLY, + "2" => RepeatPeriod::MONTHLY, + "3" => RepeatPeriod::ANNUALLY, + _ => panic!("Invalid RepeatPeriod {value}"), + } + } } +#[derive(Clone)] pub struct ByRule { - by_rule: ByRuleType, - interval: String, + by_rule: ByRuleType, + interval: String, } +#[derive(Clone)] pub struct RepeatRule { - frequency: RepeatPeriod, - by_rules: Vec, + frequency: RepeatPeriod, + by_rules: Vec, } trait MonthNumber { - fn to_number(&self) -> u8; - fn from_number(number: u8) -> Month; + fn to_number(&self) -> u8; + fn from_number(number: u8) -> Month; } trait WeekdayString { - fn from_short(short_weekday: &str) -> Weekday; + fn from_short(short_weekday: &str) -> Weekday; } impl MonthNumber for Month { - fn to_number(&self) -> u8 { - match *self { - Month::January => 1, - Month::February => 2, - Month::March => 3, - Month::April => 4, - Month::May => 5, - Month::June => 6, - Month::July => 7, - Month::August => 8, - Month::September => 9, - Month::October => 10, - Month::November => 11, - Month::December => 12, - } - } - - fn from_number(number: u8) -> Month { - match number { - 1 => Month::January, - 2 => Month::February, - 3 => Month::March, - 4 => Month::April, - 5 => Month::May, - 6 => Month::June, - 7 => Month::July, - 8 => Month::August, - 9 => Month::September, - 10 => Month::October, - 11 => Month::November, - 12 => Month::December, - _ => panic!("Invalid Month {number}") - } - } + fn to_number(&self) -> u8 { + match *self { + Month::January => 1, + Month::February => 2, + Month::March => 3, + Month::April => 4, + Month::May => 5, + Month::June => 6, + Month::July => 7, + Month::August => 8, + Month::September => 9, + Month::October => 10, + Month::November => 11, + Month::December => 12, + } + } + + fn from_number(number: u8) -> Month { + match number { + 1 => Month::January, + 2 => Month::February, + 3 => Month::March, + 4 => Month::April, + 5 => Month::May, + 6 => Month::June, + 7 => Month::July, + 8 => Month::August, + 9 => Month::September, + 10 => Month::October, + 11 => Month::November, + 12 => Month::December, + _ => panic!("Invalid Month {number}"), + } + } } impl WeekdayString for Weekday { - fn from_short(short_weekday: &str) -> Weekday { - match short_weekday { - "MO" => Weekday::Monday, - "TU" => Weekday::Tuesday, - "WE" => Weekday::Wednesday, - "TH" => Weekday::Thursday, - "FR" => Weekday::Friday, - "SA" => Weekday::Saturday, - "SU" => Weekday::Sunday, - _ => panic!("Invalid Weekday {short_weekday}") - } - } + fn from_short(short_weekday: &str) -> Weekday { + match short_weekday { + "MO" => Weekday::Monday, + "TU" => Weekday::Tuesday, + "WE" => Weekday::Wednesday, + "TH" => Weekday::Thursday, + "FR" => Weekday::Friday, + "SA" => Weekday::Saturday, + "SU" => Weekday::Sunday, + _ => panic!("Invalid Weekday {short_weekday}"), + } + } } trait DateExpansion { - fn add_month(&self) -> Date; + fn add_month(&self) -> Date; } impl DateExpansion for Date { - fn add_month(&self) -> Date { - self.add(Duration::days(i64::from(self.month().length(self.year())))) - } + fn add_month(&self) -> Date { + self.add(Duration::days(i64::from(self.month().length(self.year())))) + } } pub struct EventRecurrence; impl<'a> EventRecurrence { - pub fn new() -> Self { - EventRecurrence {} - } - pub fn generate_future_instances(&self, date: PrimitiveDateTime, repeat_rule: RepeatRule) -> Vec { - let by_month_rules: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYMONTH }).collect(); - let by_day_rules: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYDAY }).collect(); - let by_month_day_rules: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYMONTHDAY }).collect(); - let by_year_day_rules: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYYEARDAY }).collect(); - let by_week_no_rules: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYWEEKNO }).collect(); - let by_set_pos: Vec<&ByRule> = repeat_rule.by_rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYSETPOS }).collect(); - - let week_start: Weekday = match repeat_rule.by_rules.iter().find(|&x| { x.by_rule == ByRuleType::WKST }) { - Some(rule) => match rule.interval.as_str() { - "MO" => Weekday::Monday, - "TU" => Weekday::Tuesday, - "WE" => Weekday::Wednesday, - "TH" => Weekday::Thursday, - "FR" => Weekday::Friday, - "SA" => Weekday::Saturday, - "SU" => Weekday::Sunday, - _ => Weekday::Monday - }, - None => Weekday::Monday - }; - - let valid_months: Vec = by_month_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); - let valid_month_days: Vec = by_month_day_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); - let valid_year_days: Vec = by_year_day_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); - - let month_applied_events: Vec = self.apply_month_rules(&vec![date], &by_month_rules, &repeat_rule.frequency); - let week_no_applied_events: Vec = self.apply_week_no_rules(month_applied_events, &by_week_no_rules, week_start); - let year_day_applied_events: Vec = self.apply_year_day_rules(week_no_applied_events, &by_year_day_rules, by_week_no_rules.len() > 0, by_month_rules.len() > 0); - let month_day_applied_events: Vec = self.apply_month_day_rules(year_day_applied_events, &by_month_day_rules, &repeat_rule.frequency == &RepeatPeriod::DAILY); - let day_applied_events: Vec = self.apply_day_rules(month_day_applied_events, &by_day_rules, &repeat_rule.frequency, valid_months.clone(), week_start, by_week_no_rules.len() > 0, valid_month_days, valid_year_days); - - self.finish_rules(day_applied_events, &by_set_pos, valid_months.clone(), date.assume_utc().unix_timestamp()) - } - - fn apply_month_rules(&self, dates: &Vec, rules: &Vec<&'a ByRule>, frequency: &RepeatPeriod) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - - for &rule in rules { - for date in dates { - let target_month: u8 = match rule.interval.parse::() { - Ok(month) => month, - _ => continue - }; - - if frequency == &RepeatPeriod::WEEKLY { - let week_start = PrimitiveDateTime::new(Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Monday).unwrap(), date.time()); - let week_end = PrimitiveDateTime::new(Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Sunday).unwrap(), date.time()); - - let week_start_year = week_start.year(); - let week_end_year = week_end.year(); - - let week_start_month = week_start.month().to_number(); - let week_end_month = week_end.month().to_number(); - - let is_target_month = week_end_month == target_month || week_start_month == target_month; - - if week_start_year == week_end_year && week_start_month < week_end_month && is_target_month { - new_dates.push(date.clone()); - continue; - } else if week_start_year < week_end_year && is_target_month { - new_dates.push(date.clone()); - continue; - } - } else if frequency == &RepeatPeriod::ANNUALLY { - let new_date = match date.clone().replace_month(Month::from_number(target_month)) { - Ok(dt) => dt, - _ => continue - }; - - let years_to_add = if date.year() == new_date.year() && date.month().to_number() > target_month { 1 } else { 0 }; - - new_dates.push(match new_date.replace_year(new_date.year() + years_to_add) { - Ok(date) => date, - _ => continue - }); - - continue; - } - - if date.month().to_number() == target_month { - new_dates.push(date.clone()); - } - } - } - - new_dates - } - - fn apply_week_no_rules(&self, dates: Vec, rules: &Vec<&'a ByRule>, week_start: Weekday) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - - for &rule in rules { - for date in &dates { - let parsed_week: i8 = match rule.interval.parse::() { - Ok(week) => week, - _ => continue - }; - - let mut new_date = date.clone(); - let mut week_number: u8; - - if parsed_week < 0 { - week_number = weeks_in_year(date.year()) - parsed_week.unsigned_abs() + 1 - } else { - new_date = new_date.replace_date(Date::from_iso_week_date(new_date.year(), parsed_week as u8, new_date.weekday()).unwrap()); - week_number = parsed_week as u8 - } - - let year_offset = if new_date.assume_utc().unix_timestamp() < date.assume_utc().unix_timestamp() { 1 } else { 0 }; - new_date = new_date.replace_date(Date::from_iso_week_date(new_date.year() + year_offset, week_number, week_start).unwrap()); - - for i in 0..7 { - let final_date = new_date.add(Duration::days(i)); - if final_date.year() > new_date.year() { - break; - } - - new_dates.push(final_date) - } - } - } - - new_dates - } - - fn apply_year_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, evaluate_same_week: bool, evaluate_same_month: bool) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - - for &rule in rules { - for date in &dates { - let parsed_day: i64 = match rule.interval.parse::() { - Ok(day) => day, - _ => continue - }; - - let mut new_date: PrimitiveDateTime; - if parsed_day.is_negative() { - new_date = date.replace_month(Month::December).unwrap() - .replace_day(31).unwrap() - .sub(Duration::days((parsed_day.unsigned_abs() - 1) as i64)); - } else { - new_date = date.replace_month(Month::January).unwrap() - .replace_day(1).unwrap() - .add(Duration::days(parsed_day - 1)); - } - - let year_offset = if new_date.assume_utc().unix_timestamp() < date.assume_utc().unix_timestamp() { 1 } else { 0 }; - new_date = match new_date.replace_year(new_date.year() + year_offset) { - Ok(date) => date, - _ => continue - }; - - if (evaluate_same_week && date.iso_week() != new_date.iso_week()) || (evaluate_same_month && date.month() != new_date.month()) { - continue; - } - - new_dates.push(new_date) - } - } - - new_dates - } - - fn apply_month_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, is_daily_event: bool) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - - for &rule in rules { - for date in &dates { - let target_day: i8 = match rule.interval.parse::() { - Ok(day) => day, - _ => continue - }; - let days_diff = date.month().length(date.year()) as i8 - target_day.unsigned_abs() as i8 + 1; - - if is_daily_event { - if target_day.is_positive() && date.day() == target_day.unsigned_abs() { - new_dates.push(date.clone()); - } else if target_day.is_negative() && days_diff == date.day() as i8 { - new_dates.push(date.clone()); - } - - continue; - } - - if target_day >= 0 && target_day.unsigned_abs() <= date.month().length(date.year()) { - let date = match date.replace_day(target_day.unsigned_abs()) { - Ok(date) => date, - _ => continue - }; - - new_dates.push(date); - } else if days_diff > 0 && target_day.unsigned_abs() <= date.month().length(date.year()) { - let date = match date.replace_day(days_diff.unsigned_abs()) { - Ok(date) => date, - _ => continue - }; - - new_dates.push(date); - } - } - } - - new_dates - } - - fn apply_day_rules(&self, dates: Vec, rules: &Vec<&ByRule>, frequency: &RepeatPeriod, valid_months: Vec, week_start: Weekday, has_week_no: bool, valid_month_days: Vec, valid_year_days: Vec) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - let regex = Regex::new(r"^([-+]?\d{0,3})([a-zA-Z]{2})?$").unwrap(); - - for &rule in rules { - for date in &dates { - let Some(parsed_rule) = regex.captures(rule.interval.as_str()) else { continue }; - let target_week_day = parsed_rule.get(2); - let leading_value = parsed_rule.get(1); - date.add(Duration::days(i64::from(Month::December.length(2025)))); - if frequency == &RepeatPeriod::DAILY && target_week_day.is_some() && date.weekday() == Weekday::from_short(target_week_day.unwrap().as_str()) { - new_dates.push(date.clone()) - } else if frequency == &RepeatPeriod::WEEKLY && target_week_day.is_some() { - let mut new_date = date.replace_date(Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::from_short(target_week_day.unwrap().as_str())).unwrap()); - let interval_start = date.replace_date(Date::from_iso_week_date(date.year(), date.iso_week(), week_start).unwrap()); - - if new_date.assume_utc().unix_timestamp() > interval_start.add(Duration::weeks(1)).assume_utc().unix_timestamp() { - continue; - } else if new_date.assume_utc().unix_timestamp() < interval_start.assume_utc().unix_timestamp() { - new_date = new_date.add(Duration::weeks(1)); - } - - if valid_months.len() == 0 || valid_months.contains(&new_date.month().to_number()) { - new_dates.push(new_date) - } - } else if frequency == &RepeatPeriod::MONTHLY && target_week_day.is_some() { - let mut allowed_days: Vec = Vec::new(); - - let week_change = match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { - Ok(val) => { val } - _ => { 0 } - }; - - let base_date = date.replace_day(1).unwrap(); - let stop_condition = PrimitiveDateTime::new(base_date.date().add_month(), base_date.time()); - - for allowed_day in &valid_month_days { - if allowed_day.is_positive() { - allowed_days.push(allowed_day.unsigned_abs()); - continue; - } - - let day = base_date.month().length(date.year()) - allowed_day.unsigned_abs() + 1; - allowed_days.push(day); - } - - let is_allowed_in_month_day = |day: u8| -> bool { - if allowed_days.len() == 0 { - return true; - } - - allowed_days.contains(&day) - }; - - let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); - - if week_change != 0 { - let mut new_date = base_date; - if week_change.is_negative() { - new_date = new_date.replace_day(new_date.month().length(new_date.year())).unwrap(); - new_date = new_date.replace_date(Date::from_iso_week_date(new_date.year(), new_date.iso_week(), parsed_weekday).unwrap()); - - let new_week = new_date.iso_week() - week_change.unsigned_abs() - 1; - new_date = new_date.replace_date(Date::from_iso_week_date(new_date.year(), new_week, new_date.weekday()).unwrap()) - } else { - while new_date.weekday() != parsed_weekday { - new_date = new_date.add(Duration::days(1)); - } - - new_date = new_date.replace_date(Date::from_iso_week_date(new_date.year(), new_date.iso_week() + week_change.unsigned_abs() - 1, new_date.weekday()).unwrap()) - } - - if new_date.assume_utc().unix_timestamp() >= base_date.assume_utc().unix_timestamp() - && new_date.assume_utc().unix_timestamp() <= stop_condition.assume_utc().unix_timestamp() - && is_allowed_in_month_day(new_date.day()) { - new_dates.push(new_date) - } - } else { - let mut current_date = base_date; - while current_date.assume_utc().unix_timestamp() < stop_condition.assume_utc().unix_timestamp() { - let new_date = current_date.replace_date(Date::from_iso_week_date(current_date.year(), current_date.iso_week(), parsed_weekday).unwrap()); - if new_date.assume_utc().unix_timestamp() >= base_date.assume_utc().unix_timestamp() && is_allowed_in_month_day(new_date.day()) { - if valid_months.len() > 0 && valid_months.contains(&new_date.month().to_number()) { - new_dates.push(new_date) - } else if valid_months.len() == 0 { - new_dates.push(new_date) - } - } - - current_date = new_date.add(Duration::days(7)); - } - } - } else if frequency == &RepeatPeriod::ANNUALLY { - let week_change = match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { - Ok(val) => { val } - _ => { 0 } - }; - - if has_week_no && week_change != 0 { - println!("Invalid repeat rule, can't use BYWEEKNO with Week Offset on BYDAY"); - continue; - } - - if week_change != 0 && !has_week_no { - let mut new_date: PrimitiveDateTime; - - if !target_week_day.is_some() { - if week_change > 0 { - new_date = date.replace_day(1).unwrap().replace_month(Month::January).unwrap().add(Duration::days(week_change - 1)) - } else { - new_date = date.replace_month(Month::December).unwrap().replace_day(31).unwrap().sub(Duration::days(week_change.abs() - 1)) - } - } else { - let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); - - if week_change > 0 { - new_date = date.replace_day(1).unwrap().replace_month(Month::January).unwrap().add(Duration::weeks(week_change - 1)); - - while new_date.weekday() != parsed_weekday { - new_date = new_date.add(Duration::days(1)); - } - } else { - new_date = date.replace_month(Month::December).unwrap().replace_day(31).unwrap().sub(Duration::weeks(week_change.abs() - 1)); - while new_date.weekday() != parsed_weekday { - new_date = new_date.sub(Duration::days(1)); - } - } - } - - if new_date.assume_utc().unix_timestamp() < date.assume_utc().unix_timestamp() { - match new_date.replace_year(new_date.year() + 1) { - Ok(dt) => new_dates.push(dt), - _ => continue - } - } else { - new_dates.push(new_date) - } - } else if has_week_no { - if !target_week_day.is_some() { - continue; - } - - let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); - let new_date = date.replace_date(Date::from_iso_week_date(date.year(), date.iso_week(), parsed_weekday).unwrap()); - let interval_start = date.replace_date(Date::from_iso_week_date(date.year(), date.iso_week(), week_start).unwrap()); - let week_ahead = interval_start.add(Duration::days(7)); - - if new_date.assume_utc().unix_timestamp() > week_ahead.assume_utc().unix_timestamp() || new_date.assume_utc().unix_timestamp() < date.assume_utc().unix_timestamp() {} else if new_date.assume_utc().unix_timestamp() < interval_start.assume_utc().unix_timestamp() { - new_dates.push(interval_start.add(Duration::days(7))); - } else { - new_dates.push(new_date); - } - } else { - if !target_week_day.is_some() { - continue; - } - - let day_one = date.replace_day(1).unwrap(); - let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); - - let stop_date = match Date::from_calendar_date(date.year() + 1, date.month(), date.day()) { - Ok(date) => date, - _ => continue - }; - - let stop_condition = date.replace_date(stop_date); - let mut current_date = date.replace_date(Date::from_iso_week_date(date.year(), day_one.iso_week(), parsed_weekday).unwrap()); - - if current_date.assume_utc().unix_timestamp() >= day_one.assume_utc().unix_timestamp() { - new_dates.push(current_date); - } - - current_date = current_date.add(Duration::days(7)); - - while current_date.assume_utc().unix_timestamp() < stop_condition.assume_utc().unix_timestamp() { - new_dates.push(current_date); - current_date = current_date.add(Duration::days(7)); - } - } - } - } - } - - if frequency == &RepeatPeriod::ANNUALLY { - return new_dates.iter().filter(|date| { self.is_valid_day_in_year(**date, valid_year_days.clone()) }).map(|date| { *date }).collect(); - } - - new_dates - } - - fn convert_date_to_day_of_year(&self, date: PrimitiveDateTime) -> u16 { - let day = (date.replace_time(Time::from_hms(0, 0, 0).unwrap()).assume_utc().unix_timestamp() - PrimitiveDateTime::new(Date::from_calendar_date(date.year() - 1, Month::December, 31).unwrap(), date.time()).assume_utc().unix_timestamp()) / 24 / 60 / 60 / 1000; - return day as u16; - } - - fn get_valid_days_in_year(&self, year: i32, valid_year_days: &Vec) -> Vec { - let days_in_year = Date::from_calendar_date(year, Month::December, 31).unwrap().ordinal(); - let mut allowed_days: Vec = Vec::new(); - - for allowed_day in valid_year_days { - if allowed_day > &0 { - allowed_days.push(allowed_day.abs() as u16); - continue; - } - - let day = days_in_year - allowed_day.unsigned_abs() + 1; - allowed_days.push(day); - } - - allowed_days - } - - fn is_valid_day_in_year(&self, date: PrimitiveDateTime, valid_year_days: Vec) -> bool { - let valid_days = self.get_valid_days_in_year(date.year(), &valid_year_days); - - if valid_days.len() == 0 { - return true; - } - - let day_in_year = self.convert_date_to_day_of_year(date); - - let is_valid = valid_days.contains(&day_in_year); - - return is_valid; - } - - fn finish_rules(&self, dates: Vec, set_pos_rules: &Vec<&ByRule>, valid_months: Vec, event_start_time: i64) -> Vec { - Vec::new() - } + pub fn new() -> Self { + EventRecurrence {} + } + pub fn generate_future_instances( + &self, + date: PrimitiveDateTime, + repeat_rule: RepeatRule, + ) -> Vec { + let by_month_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYMONTH) + .collect(); + let by_day_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYDAY) + .collect(); + let by_month_day_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) + .collect(); + let by_year_day_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYYEARDAY) + .collect(); + let by_week_no_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYWEEKNO) + .collect(); + + let week_start: Weekday; + + if repeat_rule.frequency == RepeatPeriod::ANNUALLY + || repeat_rule.frequency == RepeatPeriod::WEEKLY + { + week_start = match repeat_rule + .by_rules + .iter() + .find(|&x| x.by_rule == ByRuleType::WKST) + { + Some(rule) => match rule.interval.as_str() { + "MO" => Weekday::Monday, + "TU" => Weekday::Tuesday, + "WE" => Weekday::Wednesday, + "TH" => Weekday::Thursday, + "FR" => Weekday::Friday, + "SA" => Weekday::Saturday, + "SU" => Weekday::Sunday, + _ => Weekday::Monday, + }, + None => Weekday::Monday, + }; + } else { + week_start = Weekday::Monday + } + + let valid_months: Vec = by_month_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + let valid_month_days: Vec = by_month_day_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + let valid_year_days: Vec = by_year_day_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + + let month_applied_events: Vec = + self.apply_month_rules(&vec![date], &by_month_rules, &repeat_rule.frequency); + + let week_no_applied_events: Vec = + if repeat_rule.frequency == RepeatPeriod::ANNUALLY { + self.apply_week_no_rules(month_applied_events, &by_week_no_rules, week_start) + } else { + month_applied_events + }; + + let year_day_applied_events: Vec = + if repeat_rule.frequency == RepeatPeriod::ANNUALLY { + self.apply_year_day_rules( + week_no_applied_events, + &by_year_day_rules, + by_week_no_rules.len() > 0, + by_month_rules.len() > 0, + ) + } else { + week_no_applied_events + }; + + let month_day_applied_events: Vec = self.apply_month_day_rules( + year_day_applied_events, + &by_month_day_rules, + &repeat_rule.frequency == &RepeatPeriod::DAILY, + ); + let day_applied_events: Vec = self.apply_day_rules( + month_day_applied_events, + &by_day_rules, + &repeat_rule.frequency, + valid_months.clone(), + week_start, + by_week_no_rules.len() > 0, + valid_month_days, + valid_year_days, + ); + + let date_timestamp = date.assume_utc().unix_timestamp(); + self.finish_rules( + day_applied_events, + valid_months.clone(), + Some(date_timestamp), + ) + } + + fn apply_month_rules( + &self, + dates: &Vec, + rules: &Vec<&'a ByRule>, + frequency: &RepeatPeriod, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in dates { + let target_month: u8 = match rule.interval.parse::() { + Ok(month) => month, + _ => continue, + }; + + if frequency == &RepeatPeriod::WEEKLY { + let week_start = PrimitiveDateTime::new( + Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Monday) + .unwrap(), + date.time(), + ); + let week_end = PrimitiveDateTime::new( + Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Sunday) + .unwrap(), + date.time(), + ); + + let week_start_year = week_start.year(); + let week_end_year = week_end.year(); + + let week_start_month = week_start.month().to_number(); + let week_end_month = week_end.month().to_number(); + + let is_target_month = + week_end_month == target_month || week_start_month == target_month; + + if week_start_year == week_end_year + && week_start_month < week_end_month + && is_target_month + { + new_dates.push(date.clone()); + continue; + } else if week_start_year < week_end_year && is_target_month { + new_dates.push(date.clone()); + continue; + } + } else if frequency == &RepeatPeriod::ANNUALLY { + let new_date = + match date.clone().replace_month(Month::from_number(target_month)) { + Ok(dt) => dt, + _ => continue, + }; + + let years_to_add = if date.year() == new_date.year() + && date.month().to_number() > target_month + { + 1 + } else { + 0 + }; + + new_dates.push( + match new_date.replace_year(new_date.year() + years_to_add) { + Ok(date) => date, + _ => continue, + }, + ); + + continue; + } + + if date.month().to_number() == target_month { + new_dates.push(date.clone()); + } + } + } + + new_dates + } + + fn apply_week_no_rules( + &self, + dates: Vec, + rules: &Vec<&'a ByRule>, + week_start: Weekday, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in &dates { + let parsed_week: i8 = match rule.interval.parse::() { + Ok(week) => week, + _ => continue, + }; + + let mut new_date = date.clone(); + let mut week_number: u8; + + if parsed_week < 0 { + week_number = weeks_in_year(date.year()) - parsed_week.unsigned_abs() + 1 + } else { + new_date = new_date.replace_date( + Date::from_iso_week_date( + new_date.year(), + parsed_week as u8, + new_date.weekday(), + ) + .unwrap(), + ); + week_number = parsed_week as u8 + } + + let year_offset = if new_date.assume_utc().unix_timestamp() + < date.assume_utc().unix_timestamp() + { + date.year() - new_date.year() + 1 + } else { + 0 + }; + let year = new_date.year() + year_offset; + new_date = new_date + .replace_date(Date::from_iso_week_date(year, week_number, week_start).unwrap()); + + for i in 0..7 { + let final_date = new_date.add(Duration::days(i)); + if final_date.year() > new_date.year() { + break; + } + + new_dates.push(final_date) + } + } + } + + new_dates + } + + fn apply_year_day_rules( + &self, + dates: Vec, + rules: &Vec<&ByRule>, + evaluate_same_week: bool, + evaluate_same_month: bool, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in &dates { + let parsed_day: i64 = match rule.interval.parse::() { + Ok(day) => day, + _ => continue, + }; + + let mut new_date: PrimitiveDateTime; + if parsed_day.is_negative() { + new_date = date + .replace_month(Month::December) + .unwrap() + .replace_day(31) + .unwrap() + .sub(Duration::days((parsed_day.unsigned_abs() - 1) as i64)); + } else { + new_date = date + .replace_month(Month::January) + .unwrap() + .replace_day(1) + .unwrap() + .add(Duration::days(parsed_day - 1)); + } + + let year_offset = if new_date.assume_utc().unix_timestamp() + < date.assume_utc().unix_timestamp() + { + 1 + } else { + 0 + }; + new_date = match new_date.replace_year(new_date.year() + year_offset) { + Ok(date) => date, + _ => continue, + }; + + if (evaluate_same_week && date.iso_week() != new_date.iso_week()) + || (evaluate_same_month && date.month() != new_date.month()) + { + continue; + } + + new_dates.push(new_date) + } + } + + new_dates + } + + fn apply_month_day_rules( + &self, + dates: Vec, + rules: &Vec<&ByRule>, + is_daily_event: bool, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in &dates { + let target_day: i8 = match rule.interval.parse::() { + Ok(day) => day, + _ => continue, + }; + let days_diff = + date.month().length(date.year()) as i8 - target_day.unsigned_abs() as i8 + 1; + + if is_daily_event { + if target_day.is_positive() && date.day() == target_day.unsigned_abs() { + new_dates.push(date.clone()); + } else if target_day.is_negative() && days_diff == date.day() as i8 { + new_dates.push(date.clone()); + } + + continue; + } + + if target_day >= 0 && target_day.unsigned_abs() <= date.month().length(date.year()) + { + let date = match date.replace_day(target_day.unsigned_abs()) { + Ok(date) => date, + _ => continue, + }; + + new_dates.push(date); + } else if days_diff > 0 + && target_day.unsigned_abs() <= date.month().length(date.year()) + { + let date = match date.replace_day(days_diff.unsigned_abs()) { + Ok(date) => date, + _ => continue, + }; + + new_dates.push(date); + } + } + } + + new_dates + } + + fn apply_day_rules( + &self, + dates: Vec, + rules: &Vec<&ByRule>, + frequency: &RepeatPeriod, + valid_months: Vec, + week_start: Weekday, + has_week_no: bool, + valid_month_days: Vec, + valid_year_days: Vec, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + let regex = Regex::new(r"^([-+]?\d{0,3})([a-zA-Z]{2})?$").unwrap(); + + for &rule in rules { + for date in &dates { + let Some(parsed_rule) = regex.captures(rule.interval.as_str()) else { + continue; + }; + let target_week_day = parsed_rule.get(2); + let leading_value = parsed_rule.get(1); + + if frequency == &RepeatPeriod::DAILY + && target_week_day.is_some() + && date.weekday() == Weekday::from_short(target_week_day.unwrap().as_str()) + { + new_dates.push(date.clone()) + } else if frequency == &RepeatPeriod::WEEKLY && target_week_day.is_some() { + let mut new_date = date.replace_date( + Date::from_iso_week_date( + date.year(), + date.iso_week(), + Weekday::from_short(target_week_day.unwrap().as_str()), + ) + .unwrap(), + ); + let interval_start = date.replace_date( + Date::from_iso_week_date(date.year(), date.iso_week(), week_start).unwrap(), + ); + + if new_date.assume_utc().unix_timestamp() + > interval_start + .add(Duration::weeks(1)) + .assume_utc() + .unix_timestamp() + { + continue; + } else if new_date.assume_utc().unix_timestamp() + < interval_start.assume_utc().unix_timestamp() + { + new_date = new_date.add(Duration::weeks(1)); + } + + if valid_months.len() == 0 + || valid_months.contains(&new_date.month().to_number()) + { + new_dates.push(new_date) + } + } else if frequency == &RepeatPeriod::MONTHLY && target_week_day.is_some() { + let mut allowed_days: Vec = Vec::new(); + + let week_change = + match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { + Ok(val) => val, + _ => 0, + }; + + let base_date = date.replace_day(1).unwrap(); + let stop_condition = + PrimitiveDateTime::new(base_date.date().add_month(), base_date.time()); + + for allowed_day in &valid_month_days { + if allowed_day.is_positive() { + allowed_days.push(allowed_day.unsigned_abs()); + continue; + } + + let day = + base_date.month().length(date.year()) - allowed_day.unsigned_abs() + 1; + allowed_days.push(day); + } + + let is_allowed_in_month_day = |day: u8| -> bool { + if allowed_days.len() == 0 { + return true; + } + + allowed_days.contains(&day) + }; + + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + + if week_change != 0 { + let mut new_date = base_date; + if week_change.is_negative() { + new_date = new_date + .replace_day(new_date.month().length(new_date.year())) + .unwrap(); + new_date = new_date.replace_date( + Date::from_iso_week_date( + new_date.year(), + new_date.iso_week(), + parsed_weekday, + ) + .unwrap(), + ); + + let new_week = new_date.iso_week() - week_change.unsigned_abs() + 1; + new_date = new_date.replace_date( + Date::from_iso_week_date( + new_date.year(), + new_week, + new_date.weekday(), + ) + .unwrap(), + ) + } else { + while new_date.weekday() != parsed_weekday { + new_date = new_date.add(Duration::days(1)); + } + + new_date = new_date.replace_date( + Date::from_iso_week_date( + new_date.year(), + new_date.iso_week() + week_change.unsigned_abs() - 1, + new_date.weekday(), + ) + .unwrap(), + ) + } + + if new_date.assume_utc().unix_timestamp() + >= base_date.assume_utc().unix_timestamp() + && new_date.assume_utc().unix_timestamp() + <= stop_condition.assume_utc().unix_timestamp() + && is_allowed_in_month_day(new_date.day()) + { + new_dates.push(new_date) + } + } else { + let mut current_date = base_date; + while current_date.assume_utc().unix_timestamp() + < stop_condition.assume_utc().unix_timestamp() + { + let new_date = current_date.replace_date( + Date::from_iso_week_date( + current_date.year(), + current_date.iso_week(), + parsed_weekday, + ) + .unwrap(), + ); + if new_date.assume_utc().unix_timestamp() + >= base_date.assume_utc().unix_timestamp() + && is_allowed_in_month_day(new_date.day()) + { + if valid_months.len() > 0 + && valid_months.contains(&new_date.month().to_number()) + { + new_dates.push(new_date) + } else if valid_months.len() == 0 { + new_dates.push(new_date) + } + } + + current_date = new_date.add(Duration::days(7)); + } + } + } else if frequency == &RepeatPeriod::ANNUALLY { + let week_change = + match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { + Ok(val) => val, + _ => 0, + }; + + if has_week_no && week_change != 0 { + println!( + "Invalid repeat rule, can't use BYWEEKNO with Week Offset on BYDAY" + ); + continue; + } + + if week_change != 0 && !has_week_no { + let mut new_date: PrimitiveDateTime; + + if !target_week_day.is_some() { + if week_change > 0 { + new_date = date + .replace_day(1) + .unwrap() + .replace_month(Month::January) + .unwrap() + .add(Duration::days(week_change - 1)) + } else { + new_date = date + .replace_month(Month::December) + .unwrap() + .replace_day(31) + .unwrap() + .sub(Duration::days(week_change.abs() - 1)) + } + } else { + let parsed_weekday = + Weekday::from_short(target_week_day.unwrap().as_str()); + + if week_change > 0 { + new_date = date + .replace_day(1) + .unwrap() + .replace_month(Month::January) + .unwrap() + .add(Duration::weeks(week_change - 1)); + + while new_date.weekday() != parsed_weekday { + new_date = new_date.add(Duration::days(1)); + } + } else { + new_date = date + .replace_month(Month::December) + .unwrap() + .replace_day(31) + .unwrap() + .sub(Duration::weeks(week_change.abs() - 1)); + while new_date.weekday() != parsed_weekday { + new_date = new_date.sub(Duration::days(1)); + } + } + } + + if new_date.assume_utc().unix_timestamp() + < date.assume_utc().unix_timestamp() + { + match new_date.replace_year(new_date.year() + 1) { + Ok(dt) => new_dates.push(dt), + _ => continue, + } + } else { + new_dates.push(new_date) + } + } else if has_week_no { + if !target_week_day.is_some() { + continue; + } + + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + let new_date = date.replace_date( + Date::from_iso_week_date(date.year(), date.iso_week(), parsed_weekday) + .unwrap(), + ); + + let interval_start = date.replace_date( + Date::from_iso_week_date(date.year(), date.iso_week(), week_start) + .unwrap(), + ); + let week_ahead = interval_start.add(Duration::days(7)); + + if new_date.assume_utc().unix_timestamp() + > week_ahead.assume_utc().unix_timestamp() + || new_date.assume_utc().unix_timestamp() + < date.assume_utc().unix_timestamp() + { + } else if new_date.assume_utc().unix_timestamp() + < interval_start.assume_utc().unix_timestamp() + { + new_dates.push(interval_start.add(Duration::days(7))); + } else { + new_dates.push(new_date); + } + } else { + if !target_week_day.is_some() { + continue; + } + + let day_one = date.replace_day(1).unwrap(); + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + + let stop_date = match Date::from_calendar_date( + date.year() + 1, + date.month(), + date.day(), + ) { + Ok(date) => date, + _ => continue, + }; + + let stop_condition = date.replace_date(stop_date); + let mut current_date = date.replace_date( + Date::from_iso_week_date( + date.year(), + day_one.iso_week(), + parsed_weekday, + ) + .unwrap(), + ); + + if current_date.assume_utc().unix_timestamp() + >= day_one.assume_utc().unix_timestamp() + { + new_dates.push(current_date); + } + + current_date = current_date.add(Duration::days(7)); + + while current_date.assume_utc().unix_timestamp() + < stop_condition.assume_utc().unix_timestamp() + { + new_dates.push(current_date); + current_date = current_date.add(Duration::days(7)); + } + } + } + } + } + + if frequency == &RepeatPeriod::ANNUALLY { + return new_dates + .iter() + .filter(|date| self.is_valid_day_in_year(**date, valid_year_days.clone())) + .map(|date| *date) + .collect(); + } + + new_dates + } + + fn get_valid_days_in_year(&self, year: i32, valid_year_days: &Vec) -> Vec { + let days_in_year = Date::from_calendar_date(year, Month::December, 31) + .unwrap() + .ordinal(); + let mut allowed_days: Vec = Vec::new(); + + for allowed_day in valid_year_days { + if allowed_day > &0 { + allowed_days.push(allowed_day.abs() as u16); + continue; + } + + let day = days_in_year - allowed_day.unsigned_abs() + 1; + allowed_days.push(day); + } + + allowed_days + } + + fn is_valid_day_in_year(&self, date: PrimitiveDateTime, valid_year_days: Vec) -> bool { + let valid_days = self.get_valid_days_in_year(date.year(), &valid_year_days); + + if valid_days.len() == 0 { + return true; + } + + let day_in_year = date.ordinal(); + + let is_valid = valid_days.contains(&day_in_year); + + return is_valid; + } + + fn finish_rules( + &self, + dates: Vec, + valid_months: Vec, + event_start_time: Option, + ) -> Vec { + let mut clean_dates; + + if valid_months.len() > 0 { + clean_dates = dates + .iter() + .filter(|date| valid_months.contains(&date.month().to_number())) + .map(|date| *date) + .collect(); + } else { + clean_dates = dates + }; + + if event_start_time.is_some() { + clean_dates = clean_dates + .iter() + .filter(|date| { + let date_unix_timestamp = date.assume_utc().unix_timestamp(); + date_unix_timestamp >= event_start_time.unwrap() + }) + .map(|date| *date) + .collect(); + } + + clean_dates.sort_by(|a, b| { + a.assume_utc() + .unix_timestamp() + .cmp(&b.assume_utc().unix_timestamp()) + }); + clean_dates.dedup(); + + clean_dates + } } #[cfg(test)] mod tests { - use time::{Date, Month, PrimitiveDateTime, Time}; - - use super::*; - - #[test] - fn test_parse_weekly_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::January, 23).unwrap(), time); - let invalid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::March, 11).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_month_rules(&vec![valid_date], &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], &RepeatPeriod::WEEKLY), vec![valid_date]); - - assert_eq!(event_recurrence.apply_month_rules(&vec![invalid_date], &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], &RepeatPeriod::WEEKLY), vec![]); - } - - #[test] - fn test_parse_monthly_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::January, 23).unwrap(), time); - let invalid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::March, 11).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_month_rules(&vec![valid_date], &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], &RepeatPeriod::MONTHLY), vec![valid_date]); - - assert_eq!(event_recurrence.apply_month_rules(&vec![invalid_date], &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], &RepeatPeriod::MONTHLY), vec![]); - } - - #[test] - fn test_parse_yearly_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::January, 23).unwrap(), time); - let to_next_year = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::March, 11).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_month_rules(&vec![valid_date], &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], &RepeatPeriod::ANNUALLY), vec![valid_date, valid_date.replace_month(Month::February).unwrap()]); - - // BYMONTH never limits on Yearly, just expands - assert_eq!( - event_recurrence.apply_month_rules( - &vec![to_next_year], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::ANNUALLY, - ), - vec![ - to_next_year.replace_year(2025).unwrap().replace_month(Month::January).unwrap(), - to_next_year.replace_year(2025).unwrap().replace_month(Month::February).unwrap(), - ] - ); - } - - #[test] - fn test_parse_daily_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::January, 23).unwrap(), time); - let second_valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::February, 12).unwrap(), time); - let invalid_date = PrimitiveDateTime::new(Date::from_calendar_date(2024, Month::March, 11).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_month_rules(&vec![valid_date], &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], &RepeatPeriod::DAILY), vec![valid_date]); - - assert_eq!(event_recurrence.apply_month_rules(&vec![invalid_date], &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], &RepeatPeriod::DAILY), vec![]); - } - - #[test] - fn test_parse_positive_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 31).unwrap(), time); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2025, Month::January, 27).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new(base_date.add(Duration::days(i)), time)); - } - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_week_no_rules(vec![valid_date], &vec![ - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "5".to_string(), - }, - ], Weekday::Monday), valid_dates); - } - - #[test] - fn test_parse_wkst_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 31).unwrap(), time); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2025, Month::January, 28).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new(base_date.add(Duration::days(i)), time)); - } - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_week_no_rules(vec![valid_date], &vec![ - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "5".to_string(), - }, - ], Weekday::Tuesday), valid_dates); - } - - #[test] - fn test_parse_negative_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::December, 4).unwrap(), time); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2025, Month::November, 24).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new(base_date.add(Duration::days(i)), time)); - } - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_week_no_rules(vec![valid_date], &vec![ - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "-5".to_string(), - }, - ], Weekday::Monday), valid_dates); - } - - #[test] - fn test_parse_edge_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new(Date::from_calendar_date(2026, Month::December, 29).unwrap(), time); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2026, Month::December, 28).unwrap(); - for i in 0..4 { - valid_dates.push(PrimitiveDateTime::new(base_date.add(Duration::days(i)), time)); - } - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_week_no_rules(vec![valid_date], &vec![ - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "-1".to_string(), - }, - ], Weekday::Monday), valid_dates); - } - - #[test] - fn test_parse_out_of_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 22).unwrap(), time); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2026, Month::January, 26).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new(base_date.add(Duration::days(i)), time)); - } - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_week_no_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "5".to_string(), - }, - ], Weekday::Monday), valid_dates); - } - - #[test] - fn test_parse_year_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 1).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_year_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "40".to_string(), - } - ], false, false), [ - date.replace_day(9).unwrap() - ]); - } - - #[test] - fn test_parse_year_day_keep_week() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 1).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_year_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "40".to_string(), - } - ], true, false), []); - } - - #[test] - fn test_parse_year_day_keep_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 22).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_year_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "40".to_string(), - } - ], true, true), []); - } - - #[test] - fn test_parse_out_of_year_year_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 22).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_year_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "40".to_string(), - } - ], false, false), [ - date.replace_year(2026).unwrap().replace_day(9).unwrap() - ]); - } - - #[test] - fn test_parse_negative_year_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 22).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_year_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "-1".to_string(), - } - ], false, false), [ - date.replace_month(Month::December).unwrap().replace_day(31).unwrap() - ]); - } - - #[test] - fn test_parse_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 22).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_month_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "10".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "20".to_string(), - }, - ], false), [ - date.replace_day(10).unwrap(), - date.replace_day(20).unwrap() - ]); - } - - #[test] - fn test_parse_invalid_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 22).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_month_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "30".to_string(), - }, - ], false), []); - } - - #[test] - fn test_parse_daily_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::February, 20).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_month_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "20".to_string(), - } - ], false), [ - date.replace_day(20).unwrap() - ]); - } - - #[test] - fn test_parse_negative_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_month_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "-1".to_string(), - }, - ], false), [ - date.replace_day(31).unwrap(), - ]); - } - - #[test] - fn test_parse_invalid_date_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_month_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "32".to_string(), - }, - ], false), []); - } - - #[test] - fn test_parse_by_day_daily() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - } - ], &RepeatPeriod::DAILY, vec![], Weekday::Monday, false, vec![], vec![]), [date]); - } - - #[test] - fn test_parse_by_day_daily_invalid() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 8).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - } - ], &RepeatPeriod::DAILY, vec![], Weekday::Monday, false, vec![], vec![]), []); - } - - #[test] - fn test_parse_by_day_weekly() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 9).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "SA".to_string(), - }, - ], &RepeatPeriod::WEEKLY, vec![], Weekday::Monday, false, vec![], vec![]), [ - date.replace_day(10).unwrap(), - date.replace_day(11).unwrap() - ]); - } - - #[test] - fn test_parse_by_day_monthly() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 6).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - // Can be WEEKDAY + WEEK - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "MO".to_string(), - }, - ], &RepeatPeriod::MONTHLY, vec![], Weekday::Monday, false, vec![], vec![]), [ - date, - date.replace_day(13).unwrap(), - date.replace_day(20).unwrap(), - date.replace_day(27).unwrap() - ]); - } - - #[test] - fn test_parse_by_day_monthly_with_monthday() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 6).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - let rules = vec![ - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "MO".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "7".to_string(), - }, - ]; - let by_day_rules: Vec<&ByRule> = rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYDAY }).collect(); - let by_month_day_rules: Vec<&ByRule> = rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYMONTHDAY }).collect(); - - let valid_month_days: Vec = by_month_day_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &by_day_rules, &RepeatPeriod::MONTHLY, vec![], Weekday::Monday, false, valid_month_days, vec![]), []); - } - - #[test] - fn test_parse_by_day_monthly_with_week() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - // Can be WEEKDAY + WEEK - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2MO".to_string(), - }, - ], &RepeatPeriod::MONTHLY, vec![], Weekday::Monday, false, vec![], vec![]), [ - date.replace_day(13).unwrap() - ]); - } - - #[test] - fn test_parse_by_day_monthly_with_monthday_and_week() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 6).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - let rules = vec![ - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2MO".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "7".to_string(), - }, - ]; - let by_day_rules: Vec<&ByRule> = rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYDAY }).collect(); - let by_month_day_rules: Vec<&ByRule> = rules.iter().filter(|&x| { x.by_rule == ByRuleType::BYMONTHDAY }).collect(); - - let valid_month_days: Vec = by_month_day_rules.iter().clone().map(|&x| { x.interval.parse::().unwrap() }).collect(); - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &by_day_rules, &RepeatPeriod::MONTHLY, vec![], Weekday::Monday, false, valid_month_days, vec![]), []); - } - - #[test] - fn test_parse_by_day_yearly() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 6).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - let end_date = date.replace_year(2026).unwrap(); - let mut current_date = date; - let mut expected_dates: Vec = Vec::new(); - - while current_date.assume_utc().unix_timestamp() < end_date.assume_utc().unix_timestamp() { - expected_dates.push(current_date); - current_date = current_date.add(Duration::days(7)) - } - - // Can be WEEKDAY + WEEK - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "MO".to_string(), - }, - ], &RepeatPeriod::ANNUALLY, vec![], Weekday::Monday, false, vec![], vec![]), expected_dates); - } - - #[test] - fn test_parse_by_day_yearly_with_week() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - // Can be WEEKDAY + WEEK - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2MO".to_string(), - }, - ], &RepeatPeriod::ANNUALLY, vec![], Weekday::Monday, false, vec![], vec![]), [ - date.replace_day(13).unwrap(), - ]); - } - - #[test] - fn test_parse_by_day_yearly_with_ordinal_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - // Can be WEEKDAY + WEEK - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "35".to_string(), - }, - ], &RepeatPeriod::ANNUALLY, vec![], Weekday::Monday, false, vec![], vec![]), [ - date.replace_month(Month::February).unwrap().replace_day(4).unwrap(), - ]); - } - - #[test] - fn test_parse_by_day_yearly_with_weekno() { - //FIXME - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 6).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "MO".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "6".to_string(), - }, - ], &RepeatPeriod::ANNUALLY, vec![], Weekday::Monday, true, vec![], vec![]), [date]); - } - - #[test] - fn test_parse_by_day_yearly_with_unmatch_weekno() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "35".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "7".to_string(), - }, - ], &RepeatPeriod::ANNUALLY, vec![], Weekday::Monday, true, vec![], vec![]), []); - } - - #[test] - fn test_parse_by_day_yearly_with_invalid_rule() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new(Date::from_calendar_date(2025, Month::January, 10).unwrap(), time); - - let event_recurrence = EventRecurrence {}; - // Can be WEEKDAY + WEEK - - assert_eq!(event_recurrence.apply_day_rules(vec![date], &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2MO".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "6".to_string(), - }, - ], &RepeatPeriod::ANNUALLY, vec![], Weekday::Monday, true, vec![], vec![]), []); - } -} \ No newline at end of file + use time::{Date, Month, PrimitiveDateTime, Time}; + + use super::*; + + #[test] + fn test_parse_weekly_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 23).unwrap(), + time, + ); + let invalid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::March, 11).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![valid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::WEEKLY + ), + vec![valid_date] + ); + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![invalid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::WEEKLY + ), + vec![] + ); + } + + #[test] + fn test_parse_monthly_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 23).unwrap(), + time, + ); + let invalid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::March, 11).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![valid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::MONTHLY + ), + vec![valid_date] + ); + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![invalid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::MONTHLY + ), + vec![] + ); + } + + #[test] + fn test_parse_yearly_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 23).unwrap(), + time, + ); + let to_next_year = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::March, 11).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![valid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY + ), + vec![ + valid_date, + valid_date.replace_month(Month::February).unwrap() + ] + ); + + // BYMONTH never limits on Yearly, just expands + assert_eq!( + event_recurrence.apply_month_rules( + &vec![to_next_year], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + ), + vec![ + to_next_year + .replace_year(2025) + .unwrap() + .replace_month(Month::January) + .unwrap(), + to_next_year + .replace_year(2025) + .unwrap() + .replace_month(Month::February) + .unwrap(), + ] + ); + } + + #[test] + fn test_parse_daily_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 23).unwrap(), + time, + ); + let invalid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::March, 11).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![valid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::DAILY + ), + vec![valid_date] + ); + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![invalid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::DAILY + ), + vec![] + ); + } + + #[test] + fn test_parse_positive_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 31).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::January, 27).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![valid_date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "5".to_string(), + },], + Weekday::Monday + ), + valid_dates + ); + } + + #[test] + fn test_parse_wkst_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 31).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::January, 28).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![valid_date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "5".to_string(), + },], + Weekday::Tuesday + ), + valid_dates + ); + } + + #[test] + fn test_parse_negative_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::December, 4).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::December, 1).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![valid_date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "-5".to_string(), + },], + Weekday::Monday + ), + valid_dates + ); + } + + #[test] + fn test_parse_edge_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2026, Month::December, 29).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2026, Month::December, 28).unwrap(); + for i in 0..4 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![valid_date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "-1".to_string(), + },], + Weekday::Monday + ), + valid_dates + ); + } + + #[test] + fn test_parse_out_of_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2026, Month::January, 26).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "5".to_string(), + },], + Weekday::Monday + ), + valid_dates + ); + } + + #[test] + fn test_parse_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 1).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + }], + false, + false + ), + [date.replace_day(9).unwrap()] + ); + } + + #[test] + fn test_parse_year_day_keep_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 1).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + }], + true, + false + ), + [] + ); + } + + #[test] + fn test_parse_year_day_keep_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 22).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + }], + true, + true + ), + [] + ); + } + + #[test] + fn test_parse_out_of_year_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + }], + false, + false + ), + [date.replace_year(2026).unwrap().replace_day(9).unwrap()] + ); + } + + #[test] + fn test_parse_negative_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "-1".to_string(), + }], + false, + false + ), + [date + .replace_month(Month::December) + .unwrap() + .replace_day(31) + .unwrap()] + ); + } + + #[test] + fn test_parse_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "10".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "20".to_string(), + }, + ], + false + ), + [date.replace_day(10).unwrap(), date.replace_day(20).unwrap()] + ); + } + + #[test] + fn test_parse_invalid_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "30".to_string(), + },], + false + ), + [] + ); + } + + #[test] + fn test_parse_daily_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 20).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "20".to_string(), + }], + false + ), + [date.replace_day(20).unwrap()] + ); + } + + #[test] + fn test_parse_negative_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "-1".to_string(), + },], + false + ), + [date.replace_day(31).unwrap(),] + ); + } + + #[test] + fn test_parse_invalid_date_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "32".to_string(), + },], + false + ), + [] + ); + } + + #[test] + fn test_parse_by_day_daily() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }], + &RepeatPeriod::DAILY, + vec![], + Weekday::Monday, + false, + vec![], + vec![] + ), + [date] + ); + } + + #[test] + fn test_parse_by_day_daily_invalid() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 8).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }], + &RepeatPeriod::DAILY, + vec![], + Weekday::Monday, + false, + vec![], + vec![] + ), + [] + ); + } + + #[test] + fn test_parse_by_day_weekly() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 9).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "SA".to_string(), + }, + ], + &RepeatPeriod::WEEKLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![] + ), + [date.replace_day(10).unwrap(), date.replace_day(11).unwrap()] + ); + } + + #[test] + fn test_parse_by_day_monthly() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + },], + &RepeatPeriod::MONTHLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![] + ), + [ + date, + date.replace_day(13).unwrap(), + date.replace_day(20).unwrap(), + date.replace_day(27).unwrap() + ] + ); + } + + #[test] + fn test_parse_by_day_monthly_with_monthday() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + let rules = vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "7".to_string(), + }, + ]; + let by_day_rules: Vec<&ByRule> = rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYDAY) + .collect(); + let by_month_day_rules: Vec<&ByRule> = rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) + .collect(); + + let valid_month_days: Vec = by_month_day_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &by_day_rules, + &RepeatPeriod::MONTHLY, + vec![], + Weekday::Monday, + false, + valid_month_days, + vec![] + ), + [] + ); + } + + #[test] + fn test_parse_by_day_monthly_with_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + },], + &RepeatPeriod::MONTHLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![] + ), + [date.replace_day(13).unwrap()] + ); + } + + #[test] + fn test_parse_by_day_monthly_with_monthday_and_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + let rules = vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "7".to_string(), + }, + ]; + let by_day_rules: Vec<&ByRule> = rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYDAY) + .collect(); + let by_month_day_rules: Vec<&ByRule> = rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) + .collect(); + + let valid_month_days: Vec = by_month_day_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &by_day_rules, + &RepeatPeriod::MONTHLY, + vec![], + Weekday::Monday, + false, + valid_month_days, + vec![] + ), + [] + ); + } + + #[test] + fn test_parse_by_day_yearly() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + let end_date = date.replace_year(2026).unwrap(); + let mut current_date = date; + let mut expected_dates: Vec = Vec::new(); + + while current_date.assume_utc().unix_timestamp() < end_date.assume_utc().unix_timestamp() { + expected_dates.push(current_date); + current_date = current_date.add(Duration::days(7)) + } + + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + },], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![] + ), + expected_dates + ); + } + + #[test] + fn test_parse_by_day_yearly_with_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + },], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![] + ), + [date.replace_day(13).unwrap(),] + ); + } + + #[test] + fn test_parse_by_day_yearly_with_ordinal_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "35".to_string(), + },], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![] + ), + [date + .replace_month(Month::February) + .unwrap() + .replace_day(4) + .unwrap(),] + ); + } + + #[test] + fn test_parse_by_day_yearly_with_weekno() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "6".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + true, + vec![], + vec![] + ), + [date] + ); + } + + #[test] + fn test_parse_by_day_yearly_with_unmatch_weekno() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "35".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "7".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + true, + vec![], + vec![] + ), + [] + ); + } + + #[test] + fn test_parse_by_day_yearly_with_invalid_rule() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventRecurrence {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "6".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + true, + vec![], + vec![] + ), + [] + ); + } + + #[test] + fn test_flow_with_by_month_daily() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::March, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::DAILY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "3".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "6".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::January).unwrap(), + repeat_rule.clone() + ), + [] + ); + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date] + ); + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::February).unwrap(), + repeat_rule.clone() + ), + [date.replace_month(Month::February).unwrap()] + ); + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::June).unwrap(), + repeat_rule.clone() + ), + [date.replace_month(Month::June).unwrap()] + ); + } + + #[test] + fn test_flow_daily_with_by_month_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::DAILY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [] + ); + } + + #[test] + fn test_flow_daily_with_by_month_and_by_day_and_by_monthday() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 14).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::DAILY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "14".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date.replace_day(14).unwrap()] + ); + assert_eq!( + event_recurrence + .generate_future_instances(date.replace_day(13).unwrap(), repeat_rule.clone()), + [] + ); + } + + #[test] + fn test_flow_weekly_with_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::WEEKLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date.replace_day(10).unwrap(),] + ); + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::January).unwrap(), + repeat_rule.clone() + ), + [] + ); + } + + #[test] + fn test_flow_weekly_with_by_month_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::WEEKLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date.replace_day(13).unwrap(), date.replace_day(14).unwrap()] + ); + } + + #[test] + fn test_flow_weekly_with_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::WEEKLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date.replace_day(13).unwrap(), date.replace_day(14).unwrap()] + ); + } + + #[test] + fn test_flow_weekly_with_by_day_and_wkst() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::WEEKLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::WKST, + interval: "FR".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date.replace_day(14).unwrap(), date.replace_day(20).unwrap()] + ); + } + + #[test] + fn test_flow_monthly_with_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [ + date.replace_day(14).unwrap(), + date.replace_day(21).unwrap(), + date.replace_day(28).unwrap() + ] + ); + } + + #[test] + fn test_flow_monthly_with_second_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2FR".to_string(), + }], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date.replace_day(14).unwrap(),] + ); + } + + #[test] + fn test_flow_monthly_with_two_last_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "-1FR".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "-2FR".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date.replace_day(21).unwrap(), date.replace_day(28).unwrap(),] + ); + } + + #[test] + fn test_flow_monthly_with_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + let date_not_in_range = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::March, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date.replace_day(10).unwrap(),] + ); + assert_eq!( + event_recurrence.generate_future_instances(date_not_in_range, repeat_rule.clone()), + [] + ); + } + + #[test] + fn test_flow_monthly_with_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "25".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "28".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date.replace_day(25).unwrap(), date.replace_day(28).unwrap(),] + ); + } + + #[test] + fn test_flow_monthly_with_by_month_day_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "25".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "28".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date.replace_day(28).unwrap(),] + ); + } + + #[test] + fn test_flow_monthly_with_by_month_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [ + date.replace_day(13).unwrap(), + date.replace_day(14).unwrap(), + date.replace_day(20).unwrap(), + date.replace_day(21).unwrap(), + date.replace_day(27).unwrap(), + date.replace_day(28).unwrap() + ] + ); + } + + #[test] + fn test_flow_yearly_with_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let stop_condition = PrimitiveDateTime::new( + Date::from_calendar_date(2026, Month::February, 10).unwrap(), + time, + ); + let mut expected_dates: Vec = Vec::new(); + let mut current_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 13).unwrap(), + time, + ); + + while current_date.assume_utc().unix_timestamp() + < stop_condition.assume_utc().unix_timestamp() + { + expected_dates.push(current_date); + expected_dates.push(current_date.add(Duration::days(1))); + + current_date = current_date.add(Duration::days(7)); + } + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + expected_dates + ); + } + + #[test] + fn test_flow_yearly_with_by_day_and_by_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "44".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date.replace_day(13).unwrap()] + ); + + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::March).unwrap(), + repeat_rule.clone() + ), + [] + ); + } + + #[test] + fn test_flow_yearly_with_by_week_no_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "8".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [date.replace_day(20).unwrap()] + ); + + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::March).unwrap(), + repeat_rule.clone() + ), + [date.replace_year(2026).unwrap().replace_day(19).unwrap()] + ); + } + + #[test] + fn test_flow_yearly_with_negative_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::December, 4).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::December, 1).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "-5".to_string(), + }], + }; + + let event_recurrence = EventRecurrence {}; + + assert_eq!( + event_recurrence.generate_future_instances(valid_date, repeat_rule), + [] + ); + } + + #[test] + fn test_flow_yearly_with_by_week_no_and_wkst() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "8".to_string(), + }, + ByRule { + by_rule: ByRuleType::WKST, + interval: "TU".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [ + date.replace_day(18).unwrap(), + date.replace_day(19).unwrap(), + date.replace_day(20).unwrap(), + date.replace_day(21).unwrap(), + date.replace_day(22).unwrap(), + date.replace_day(23).unwrap(), + date.replace_day(24).unwrap(), + ] + ); + } + + #[test] + fn test_flow_yearly_with_by_month_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = RepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventRecurrence {}; + assert_eq!( + event_recurrence.generate_future_instances(date, repeat_rule.clone()), + [ + date.replace_day(13).unwrap(), + date.replace_day(14).unwrap(), + date.replace_day(20).unwrap(), + date.replace_day(21).unwrap(), + date.replace_day(27).unwrap(), + date.replace_day(28).unwrap(), + date.replace_year(2026).unwrap().replace_day(5).unwrap(), + date.replace_year(2026).unwrap().replace_day(6).unwrap(), + ] + ); + } +} From 5c78df1e02f3030e59db8977e8b76297dc96fd98 Mon Sep 17 00:00:00 2001 From: mup Date: Wed, 8 Jan 2025 12:19:46 +0100 Subject: [PATCH 20/29] [SDK] Exposes EventFacade to uniffi Co-authored-by: paw-hub <104824185+paw-hub@users.noreply.github.com> --- tuta-sdk/rust/sdk/src/date.rs | 2 +- tuta-sdk/rust/sdk/src/date/event_facade.rs | 2763 +++++++++++++++++ .../rust/sdk/src/date/event_recurrence.rs | 2737 ---------------- 3 files changed, 2764 insertions(+), 2738 deletions(-) create mode 100644 tuta-sdk/rust/sdk/src/date/event_facade.rs delete mode 100644 tuta-sdk/rust/sdk/src/date/event_recurrence.rs diff --git a/tuta-sdk/rust/sdk/src/date.rs b/tuta-sdk/rust/sdk/src/date.rs index 16ec9c2b48c5..6512ecb4037b 100644 --- a/tuta-sdk/rust/sdk/src/date.rs +++ b/tuta-sdk/rust/sdk/src/date.rs @@ -4,7 +4,7 @@ pub(crate) mod date_provider; #[cfg(not(test))] pub(crate) mod date_provider; mod date_time; -mod event_recurrence; +mod event_facade; pub use date_provider::DateProvider; pub use date_time::DateTime; diff --git a/tuta-sdk/rust/sdk/src/date/event_facade.rs b/tuta-sdk/rust/sdk/src/date/event_facade.rs new file mode 100644 index 000000000000..a613a3bf226a --- /dev/null +++ b/tuta-sdk/rust/sdk/src/date/event_facade.rs @@ -0,0 +1,2763 @@ +use std::ops::{Add, Sub}; + +use regex::Regex; +use time::{Date, Duration, Month, OffsetDateTime, PrimitiveDateTime, Weekday}; +use time::util::weeks_in_year; + +use crate::date::DateTime; + +#[derive(uniffi::Enum, PartialEq, Copy, Clone)] +pub enum ByRuleType { + BYMINUTE, + BYHOUR, + BYDAY, + BYMONTHDAY, + BYYEARDAY, + BYWEEKNO, + BYMONTH, + BYSETPOS, + WKST, +} + +impl ByRuleType { + fn value(&self) -> &str { + match *self { + ByRuleType::BYMINUTE => "0", + ByRuleType::BYHOUR => "1", + ByRuleType::BYDAY => "2", + ByRuleType::BYMONTHDAY => "3", + ByRuleType::BYYEARDAY => "4", + ByRuleType::BYWEEKNO => "5", + ByRuleType::BYMONTH => "6", + ByRuleType::BYSETPOS => "7", + ByRuleType::WKST => "8", + } + } + + fn from_str(value: &str) -> ByRuleType { + match value { + "0" => ByRuleType::BYMINUTE, + "1" => ByRuleType::BYHOUR, + "2" => ByRuleType::BYDAY, + "3" => ByRuleType::BYMONTHDAY, + "4" => ByRuleType::BYYEARDAY, + "5" => ByRuleType::BYWEEKNO, + "6" => ByRuleType::BYMONTH, + "7" => ByRuleType::BYSETPOS, + "8" => ByRuleType::WKST, + _ => panic!("Invalid ByRule {value}"), + } + } +} + +#[derive(uniffi::Enum, PartialEq, Copy, Clone)] +pub enum RepeatPeriod { + DAILY, + WEEKLY, + MONTHLY, + ANNUALLY, +} + +impl RepeatPeriod { + fn value(&self) -> &str { + match *self { + RepeatPeriod::DAILY => "0", + RepeatPeriod::WEEKLY => "1", + RepeatPeriod::MONTHLY => "2", + RepeatPeriod::ANNUALLY => "3", + } + } + + fn from_str(value: &str) -> RepeatPeriod { + match value { + "0" => RepeatPeriod::DAILY, + "1" => RepeatPeriod::WEEKLY, + "2" => RepeatPeriod::MONTHLY, + "3" => RepeatPeriod::ANNUALLY, + _ => panic!("Invalid RepeatPeriod {value}"), + } + } +} + +#[derive(Clone)] +#[derive(uniffi::Record)] +pub struct ByRule { + pub by_rule: ByRuleType, + pub interval: String, +} + +#[derive(Clone)] +#[derive(uniffi::Record)] +pub struct EventRepeatRule { + pub frequency: RepeatPeriod, + pub by_rules: Vec, +} + +trait MonthNumber { + fn to_number(&self) -> u8; + fn from_number(number: u8) -> Month; +} + +trait WeekdayString { + fn from_short(short_weekday: &str) -> Weekday; +} + +impl MonthNumber for Month { + fn to_number(&self) -> u8 { + match *self { + Month::January => 1, + Month::February => 2, + Month::March => 3, + Month::April => 4, + Month::May => 5, + Month::June => 6, + Month::July => 7, + Month::August => 8, + Month::September => 9, + Month::October => 10, + Month::November => 11, + Month::December => 12, + } + } + + fn from_number(number: u8) -> Month { + match number { + 1 => Month::January, + 2 => Month::February, + 3 => Month::March, + 4 => Month::April, + 5 => Month::May, + 6 => Month::June, + 7 => Month::July, + 8 => Month::August, + 9 => Month::September, + 10 => Month::October, + 11 => Month::November, + 12 => Month::December, + _ => panic!("Invalid Month {number}"), + } + } +} + +impl WeekdayString for Weekday { + fn from_short(short_weekday: &str) -> Weekday { + match short_weekday { + "MO" => Weekday::Monday, + "TU" => Weekday::Tuesday, + "WE" => Weekday::Wednesday, + "TH" => Weekday::Thursday, + "FR" => Weekday::Friday, + "SA" => Weekday::Saturday, + "SU" => Weekday::Sunday, + _ => panic!("Invalid Weekday {short_weekday}"), + } + } +} + +trait DateExpansion { + fn add_month(&self) -> Date; +} + +impl DateExpansion for Date { + fn add_month(&self) -> Date { + self.add(Duration::days(i64::from(self.month().length(self.year())))) + } +} + +#[derive(uniffi::Object)] +pub struct EventFacade; + +#[uniffi::export] +impl EventFacade { + #[uniffi::constructor] + pub fn new() -> Self { + EventFacade {} + } + + pub fn generate_future_instances( + &self, + date: DateTime, + repeat_rule: EventRepeatRule, + ) -> Vec { + let Ok(parsed_date) = OffsetDateTime::from_unix_timestamp(date.as_millis() as i64) else { + // FIXME: fix paul's code :( + return Vec::new(); + }; + + let date = PrimitiveDateTime::new(parsed_date.date(), parsed_date.time()); + + let by_month_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYMONTH) + .collect(); + let by_day_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYDAY) + .collect(); + let by_month_day_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) + .collect(); + let by_year_day_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYYEARDAY) + .collect(); + let by_week_no_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYWEEKNO) + .collect(); + + let week_start: Weekday; + + if repeat_rule.frequency == RepeatPeriod::ANNUALLY + || repeat_rule.frequency == RepeatPeriod::WEEKLY + { + week_start = match repeat_rule + .by_rules + .iter() + .find(|&x| x.by_rule == ByRuleType::WKST) + { + Some(rule) => match rule.interval.as_str() { + "MO" => Weekday::Monday, + "TU" => Weekday::Tuesday, + "WE" => Weekday::Wednesday, + "TH" => Weekday::Thursday, + "FR" => Weekday::Friday, + "SA" => Weekday::Saturday, + "SU" => Weekday::Sunday, + _ => Weekday::Monday, + }, + None => Weekday::Monday, + }; + } else { + week_start = Weekday::Monday + } + + let valid_months: Vec = by_month_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + let valid_month_days: Vec = by_month_day_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + let valid_year_days: Vec = by_year_day_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + + let month_applied_events: Vec = + self.apply_month_rules(&vec![date], &by_month_rules, &repeat_rule.frequency); + + let week_no_applied_events: Vec = + if repeat_rule.frequency == RepeatPeriod::ANNUALLY { + self.apply_week_no_rules(month_applied_events, &by_week_no_rules, week_start) + } else { + month_applied_events + }; + + let year_day_applied_events: Vec = + if repeat_rule.frequency == RepeatPeriod::ANNUALLY { + self.apply_year_day_rules( + week_no_applied_events, + &by_year_day_rules, + by_week_no_rules.len() > 0, + by_month_rules.len() > 0, + ) + } else { + week_no_applied_events + }; + + let month_day_applied_events: Vec = self.apply_month_day_rules( + year_day_applied_events, + &by_month_day_rules, + &repeat_rule.frequency == &RepeatPeriod::DAILY, + ); + let day_applied_events: Vec = self.apply_day_rules( + month_day_applied_events, + &by_day_rules, + &repeat_rule.frequency, + valid_months.clone(), + week_start, + by_week_no_rules.len() > 0, + valid_month_days, + valid_year_days, + ); + + let date_timestamp = date.assume_utc().unix_timestamp(); + self.finish_rules( + day_applied_events, + valid_months.clone(), + Some(date_timestamp), + ).iter().map(|date| { DateTime::from_millis(date.assume_utc().unix_timestamp() as u64) }).collect() + } +} + +impl EventFacade { + fn apply_month_rules( + &self, + dates: &Vec, + rules: &Vec<&ByRule>, + frequency: &RepeatPeriod, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in dates { + let target_month: u8 = match rule.interval.parse::() { + Ok(month) => month, + _ => continue, + }; + + if frequency == &RepeatPeriod::WEEKLY { + let week_start = PrimitiveDateTime::new( + Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Monday) + .unwrap(), + date.time(), + ); + let week_end = PrimitiveDateTime::new( + Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Sunday) + .unwrap(), + date.time(), + ); + + let week_start_year = week_start.year(); + let week_end_year = week_end.year(); + + let week_start_month = week_start.month().to_number(); + let week_end_month = week_end.month().to_number(); + + let is_target_month = + week_end_month == target_month || week_start_month == target_month; + + if week_start_year == week_end_year + && week_start_month < week_end_month + && is_target_month + { + new_dates.push(date.clone()); + continue; + } else if week_start_year < week_end_year && is_target_month { + new_dates.push(date.clone()); + continue; + } + } else if frequency == &RepeatPeriod::ANNUALLY { + let new_date = + match date.clone().replace_month(Month::from_number(target_month)) { + Ok(dt) => dt, + _ => continue, + }; + + let years_to_add = if date.year() == new_date.year() + && date.month().to_number() > target_month + { + 1 + } else { + 0 + }; + + new_dates.push( + match new_date.replace_year(new_date.year() + years_to_add) { + Ok(date) => date, + _ => continue, + }, + ); + + continue; + } + + if date.month().to_number() == target_month { + new_dates.push(date.clone()); + } + } + } + + new_dates + } + + fn apply_week_no_rules( + &self, + dates: Vec, + rules: &Vec<&ByRule>, + week_start: Weekday, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in &dates { + let parsed_week: i8 = match rule.interval.parse::() { + Ok(week) => week, + _ => continue, + }; + + let mut new_date = date.clone(); + + let total_weeks = weeks_in_year(date.year()); + + let week_number = if parsed_week < 0 { + total_weeks - parsed_week.unsigned_abs() + 1 + } else { + new_date = new_date.replace_date( + Date::from_iso_week_date( + new_date.year(), + parsed_week as u8, + new_date.weekday(), + ) + .unwrap(), + ); + parsed_week as u8 + }; + + let year_offset = if new_date.assume_utc().unix_timestamp() + < date.assume_utc().unix_timestamp() + { + date.year() - new_date.year() + 1 + } else { + 0 + }; + let year = new_date.year() + year_offset; + new_date = new_date + .replace_date(Date::from_iso_week_date(year, week_number, week_start).unwrap()); + + for i in 0..7 { + let final_date = new_date.add(Duration::days(i)); + if final_date.year() > new_date.year() { + break; + } + + new_dates.push(final_date) + } + } + } + + new_dates + } + + fn apply_year_day_rules( + &self, + dates: Vec, + rules: &Vec<&ByRule>, + evaluate_same_week: bool, + evaluate_same_month: bool, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in &dates { + let parsed_day: i64 = match rule.interval.parse::() { + Ok(day) => day, + _ => continue, + }; + + let mut new_date: PrimitiveDateTime; + if parsed_day.is_negative() { + new_date = date + .replace_month(Month::December) + .unwrap() + .replace_day(31) + .unwrap() + .sub(Duration::days((parsed_day.unsigned_abs() - 1) as i64)); + } else { + new_date = date + .replace_month(Month::January) + .unwrap() + .replace_day(1) + .unwrap() + .add(Duration::days(parsed_day - 1)); + } + + let year_offset = if new_date.assume_utc().unix_timestamp() + < date.assume_utc().unix_timestamp() + { + 1 + } else { + 0 + }; + new_date = match new_date.replace_year(new_date.year() + year_offset) { + Ok(date) => date, + _ => continue, + }; + + if (evaluate_same_week && date.iso_week() != new_date.iso_week()) + || (evaluate_same_month && date.month() != new_date.month()) + { + continue; + } + + new_dates.push(new_date) + } + } + + new_dates + } + + fn apply_month_day_rules( + &self, + dates: Vec, + rules: &Vec<&ByRule>, + is_daily_event: bool, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in &dates { + let target_day: i8 = match rule.interval.parse::() { + Ok(day) => day, + _ => continue, + }; + let days_diff = + date.month().length(date.year()) as i8 - target_day.unsigned_abs() as i8 + 1; + + if is_daily_event { + if target_day.is_positive() && date.day() == target_day.unsigned_abs() { + new_dates.push(date.clone()); + } else if target_day.is_negative() && days_diff == date.day() as i8 { + new_dates.push(date.clone()); + } + + continue; + } + + if target_day >= 0 && target_day.unsigned_abs() <= date.month().length(date.year()) + { + let date = match date.replace_day(target_day.unsigned_abs()) { + Ok(date) => date, + _ => continue, + }; + + new_dates.push(date); + } else if days_diff > 0 + && target_day.unsigned_abs() <= date.month().length(date.year()) + { + let date = match date.replace_day(days_diff.unsigned_abs()) { + Ok(date) => date, + _ => continue, + }; + + new_dates.push(date); + } + } + } + + new_dates + } + + fn apply_day_rules( + &self, + dates: Vec, + rules: &Vec<&ByRule>, + frequency: &RepeatPeriod, + valid_months: Vec, + week_start: Weekday, + has_week_no: bool, + valid_month_days: Vec, + valid_year_days: Vec, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + let regex = Regex::new(r"^([-+]?\d{0,3})([a-zA-Z]{2})?$").unwrap(); + + for &rule in rules { + for date in &dates { + let Some(parsed_rule) = regex.captures(rule.interval.as_str()) else { + continue; + }; + let target_week_day = parsed_rule.get(2); + let leading_value = parsed_rule.get(1); + + if frequency == &RepeatPeriod::DAILY + && target_week_day.is_some() + && date.weekday() == Weekday::from_short(target_week_day.unwrap().as_str()) + { + new_dates.push(date.clone()) + } else if frequency == &RepeatPeriod::WEEKLY && target_week_day.is_some() { + let mut new_date = date.replace_date( + Date::from_iso_week_date( + date.year(), + date.iso_week(), + Weekday::from_short(target_week_day.unwrap().as_str()), + ) + .unwrap(), + ); + let interval_start = date.replace_date( + Date::from_iso_week_date(date.year(), date.iso_week(), week_start).unwrap(), + ); + + if new_date.assume_utc().unix_timestamp() + > interval_start + .add(Duration::weeks(1)) + .assume_utc() + .unix_timestamp() + { + continue; + } else if new_date.assume_utc().unix_timestamp() + < interval_start.assume_utc().unix_timestamp() + { + new_date = new_date.add(Duration::weeks(1)); + } + + if valid_months.len() == 0 + || valid_months.contains(&new_date.month().to_number()) + { + new_dates.push(new_date) + } + } else if frequency == &RepeatPeriod::MONTHLY && target_week_day.is_some() { + let mut allowed_days: Vec = Vec::new(); + + let week_change = + match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { + Ok(val) => val, + _ => 0, + }; + + let base_date = date.replace_day(1).unwrap(); + let stop_condition = + PrimitiveDateTime::new(base_date.date().add_month(), base_date.time()); + + for allowed_day in &valid_month_days { + if allowed_day.is_positive() { + allowed_days.push(allowed_day.unsigned_abs()); + continue; + } + + let day = + base_date.month().length(date.year()) - allowed_day.unsigned_abs() + 1; + allowed_days.push(day); + } + + let is_allowed_in_month_day = |day: u8| -> bool { + if allowed_days.len() == 0 { + return true; + } + + allowed_days.contains(&day) + }; + + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + + if week_change != 0 { + let mut new_date = base_date; + if week_change.is_negative() { + new_date = new_date + .replace_day(new_date.month().length(new_date.year())) + .unwrap(); + new_date = new_date.replace_date( + Date::from_iso_week_date( + new_date.year(), + new_date.iso_week(), + parsed_weekday, + ) + .unwrap(), + ); + + let new_week = new_date.iso_week() - week_change.unsigned_abs() + 1; + new_date = new_date.replace_date( + Date::from_iso_week_date( + new_date.year(), + new_week, + new_date.weekday(), + ) + .unwrap(), + ) + } else { + while new_date.weekday() != parsed_weekday { + new_date = new_date.add(Duration::days(1)); + } + + new_date = new_date.replace_date( + Date::from_iso_week_date( + new_date.year(), + new_date.iso_week() + week_change.unsigned_abs() - 1, + new_date.weekday(), + ) + .unwrap(), + ) + } + + if new_date.assume_utc().unix_timestamp() + >= base_date.assume_utc().unix_timestamp() + && new_date.assume_utc().unix_timestamp() + <= stop_condition.assume_utc().unix_timestamp() + && is_allowed_in_month_day(new_date.day()) + { + new_dates.push(new_date) + } + } else { + let mut current_date = base_date; + while current_date.assume_utc().unix_timestamp() + < stop_condition.assume_utc().unix_timestamp() + { + let new_date = current_date.replace_date( + Date::from_iso_week_date( + current_date.year(), + current_date.iso_week(), + parsed_weekday, + ) + .unwrap(), + ); + if new_date.assume_utc().unix_timestamp() + >= base_date.assume_utc().unix_timestamp() + && is_allowed_in_month_day(new_date.day()) + { + if valid_months.len() > 0 + && valid_months.contains(&new_date.month().to_number()) + { + new_dates.push(new_date) + } else if valid_months.len() == 0 { + new_dates.push(new_date) + } + } + + current_date = new_date.add(Duration::days(7)); + } + } + } else if frequency == &RepeatPeriod::ANNUALLY { + let week_change = + match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { + Ok(val) => val, + _ => 0, + }; + + if has_week_no && week_change != 0 { + println!( + "Invalid repeat rule, can't use BYWEEKNO with Week Offset on BYDAY" + ); + continue; + } + + if week_change != 0 && !has_week_no { + let mut new_date: PrimitiveDateTime; + + if !target_week_day.is_some() { + if week_change > 0 { + new_date = date + .replace_day(1) + .unwrap() + .replace_month(Month::January) + .unwrap() + .add(Duration::days(week_change - 1)) + } else { + new_date = date + .replace_month(Month::December) + .unwrap() + .replace_day(31) + .unwrap() + .sub(Duration::days(week_change.abs() - 1)) + } + } else { + let parsed_weekday = + Weekday::from_short(target_week_day.unwrap().as_str()); + + if week_change > 0 { + new_date = date + .replace_day(1) + .unwrap() + .replace_month(Month::January) + .unwrap() + .add(Duration::weeks(week_change - 1)); + + while new_date.weekday() != parsed_weekday { + new_date = new_date.add(Duration::days(1)); + } + } else { + new_date = date + .replace_month(Month::December) + .unwrap() + .replace_day(31) + .unwrap() + .sub(Duration::weeks(week_change.abs() - 1)); + while new_date.weekday() != parsed_weekday { + new_date = new_date.sub(Duration::days(1)); + } + } + } + + if new_date.assume_utc().unix_timestamp() + < date.assume_utc().unix_timestamp() + { + match new_date.replace_year(new_date.year() + 1) { + Ok(dt) => new_dates.push(dt), + _ => continue, + } + } else { + new_dates.push(new_date) + } + } else if has_week_no { + if !target_week_day.is_some() { + continue; + } + + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + let new_date = date.replace_date( + Date::from_iso_week_date(date.year(), date.iso_week(), parsed_weekday) + .unwrap(), + ); + + let interval_start = date.replace_date( + Date::from_iso_week_date(date.year(), date.iso_week(), week_start) + .unwrap(), + ); + let week_ahead = interval_start.add(Duration::days(7)); + + if new_date.assume_utc().unix_timestamp() + > week_ahead.assume_utc().unix_timestamp() + || new_date.assume_utc().unix_timestamp() + < date.assume_utc().unix_timestamp() + {} else if new_date.assume_utc().unix_timestamp() + < interval_start.assume_utc().unix_timestamp() + { + new_dates.push(interval_start.add(Duration::days(7))); + } else { + new_dates.push(new_date); + } + } else { + if !target_week_day.is_some() { + continue; + } + + let day_one = date.replace_day(1).unwrap(); + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + + let stop_date = match Date::from_calendar_date( + date.year() + 1, + date.month(), + date.day(), + ) { + Ok(date) => date, + _ => continue, + }; + + let stop_condition = date.replace_date(stop_date); + let mut current_date = date.replace_date( + Date::from_iso_week_date( + date.year(), + day_one.iso_week(), + parsed_weekday, + ) + .unwrap(), + ); + + if current_date.assume_utc().unix_timestamp() + >= day_one.assume_utc().unix_timestamp() + { + new_dates.push(current_date); + } + + current_date = current_date.add(Duration::days(7)); + + while current_date.assume_utc().unix_timestamp() + < stop_condition.assume_utc().unix_timestamp() + { + new_dates.push(current_date); + current_date = current_date.add(Duration::days(7)); + } + } + } + } + } + + if frequency == &RepeatPeriod::ANNUALLY { + return new_dates + .iter() + .filter(|date| self.is_valid_day_in_year(**date, valid_year_days.clone())) + .map(|date| *date) + .collect(); + } + + new_dates + } + + fn get_valid_days_in_year(&self, year: i32, valid_year_days: &Vec) -> Vec { + let days_in_year = Date::from_calendar_date(year, Month::December, 31) + .unwrap() + .ordinal(); + let mut allowed_days: Vec = Vec::new(); + + for allowed_day in valid_year_days { + if allowed_day > &0 { + allowed_days.push(allowed_day.abs() as u16); + continue; + } + + let day = days_in_year - allowed_day.unsigned_abs() + 1; + allowed_days.push(day); + } + + allowed_days + } + + fn is_valid_day_in_year(&self, date: PrimitiveDateTime, valid_year_days: Vec) -> bool { + let valid_days = self.get_valid_days_in_year(date.year(), &valid_year_days); + + if valid_days.len() == 0 { + return true; + } + + let day_in_year = date.ordinal(); + + let is_valid = valid_days.contains(&day_in_year); + + return is_valid; + } + + fn finish_rules( + &self, + dates: Vec, + valid_months: Vec, + event_start_time: Option, + ) -> Vec { + let mut clean_dates; + + if valid_months.len() > 0 { + clean_dates = dates + .iter() + .filter(|date| valid_months.contains(&date.month().to_number())) + .map(|date| *date) + .collect(); + } else { + clean_dates = dates + }; + + if event_start_time.is_some() { + clean_dates = clean_dates + .iter() + .filter(|date| { + let date_unix_timestamp = date.assume_utc().unix_timestamp(); + date_unix_timestamp >= event_start_time.unwrap() + }) + .map(|date| *date) + .collect(); + } + + clean_dates.sort_by(|a, b| { + a.assume_utc() + .unix_timestamp() + .cmp(&b.assume_utc().unix_timestamp()) + }); + clean_dates.dedup(); + + clean_dates + } +} + +#[cfg(test)] +mod tests { + use time::{Date, Month, PrimitiveDateTime, Time}; + + use super::*; + + trait PrimitiveToDateTime { + fn to_date_time(&self) -> DateTime; + } + impl PrimitiveToDateTime for PrimitiveDateTime { + fn to_date_time(&self) -> DateTime { + DateTime::from_millis(self.assume_utc().unix_timestamp() as u64) + } + } + + #[test] + fn test_parse_weekly_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 23).unwrap(), + time, + ); + let invalid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::March, 11).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![valid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::WEEKLY, + ), + vec![valid_date] + ); + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![invalid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::WEEKLY, + ), + vec![] + ); + } + + #[test] + fn test_parse_monthly_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 23).unwrap(), + time, + ); + let invalid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::March, 11).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![valid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::MONTHLY, + ), + vec![valid_date] + ); + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![invalid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::MONTHLY, + ), + vec![] + ); + } + + #[test] + fn test_parse_yearly_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 23).unwrap(), + time, + ); + let to_next_year = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::March, 11).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![valid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + ), + vec![ + valid_date, + valid_date.replace_month(Month::February).unwrap(), + ] + ); + + // BYMONTH never limits on Yearly, just expands + assert_eq!( + event_recurrence.apply_month_rules( + &vec![to_next_year], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + ), + vec![ + to_next_year + .replace_year(2025) + .unwrap() + .replace_month(Month::January) + .unwrap(), + to_next_year + .replace_year(2025) + .unwrap() + .replace_month(Month::February) + .unwrap(), + ] + ); + } + + #[test] + fn test_parse_daily_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 23).unwrap(), + time, + ); + let invalid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::March, 11).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![valid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::DAILY, + ), + vec![valid_date] + ); + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![invalid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::DAILY, + ), + vec![] + ); + } + + #[test] + fn test_parse_positive_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 31).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::January, 27).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![valid_date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "5".to_string(), + }, ], + Weekday::Monday, + ), + valid_dates + ); + } + + #[test] + fn test_parse_wkst_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 31).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::January, 28).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![valid_date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "5".to_string(), + }, ], + Weekday::Tuesday, + ), + valid_dates + ); + } + + #[test] + fn test_parse_negative_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::December, 4).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::November, 24).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![valid_date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "-5".to_string(), + }, ], + Weekday::Monday, + ), + valid_dates + ); + } + + #[test] + fn test_parse_edge_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2026, Month::December, 29).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2026, Month::December, 28).unwrap(); + for i in 0..4 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![valid_date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "-1".to_string(), + }, ], + Weekday::Monday, + ), + valid_dates + ); + } + + #[test] + fn test_parse_out_of_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2026, Month::January, 26).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "5".to_string(), + }, ], + Weekday::Monday, + ), + valid_dates + ); + } + + #[test] + fn test_parse_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 1).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + }], + false, + false, + ), + [date.replace_day(9).unwrap()] + ); + } + + #[test] + fn test_parse_year_day_keep_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 1).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + }], + true, + false, + ), + [] + ); + } + + #[test] + fn test_parse_year_day_keep_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 22).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + }], + true, + true, + ), + [] + ); + } + + #[test] + fn test_parse_out_of_year_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + }], + false, + false, + ), + [date.replace_year(2026).unwrap().replace_day(9).unwrap()] + ); + } + + #[test] + fn test_parse_negative_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "-1".to_string(), + }], + false, + false, + ), + [date + .replace_month(Month::December) + .unwrap() + .replace_day(31) + .unwrap()] + ); + } + + #[test] + fn test_parse_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "10".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "20".to_string(), + }, + ], + false, + ), + [date.replace_day(10).unwrap(), date.replace_day(20).unwrap()] + ); + } + + #[test] + fn test_parse_invalid_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "30".to_string(), + }, ], + false, + ), + [] + ); + } + + #[test] + fn test_parse_daily_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 20).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "20".to_string(), + }], + false, + ), + [date.replace_day(20).unwrap()] + ); + } + + #[test] + fn test_parse_negative_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "-1".to_string(), + }, ], + false, + ), + [date.replace_day(31).unwrap(), ] + ); + } + + #[test] + fn test_parse_invalid_date_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "32".to_string(), + }, ], + false, + ), + [] + ); + } + + #[test] + fn test_parse_by_day_daily() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }], + &RepeatPeriod::DAILY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [date] + ); + } + + #[test] + fn test_parse_by_day_daily_invalid() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 8).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }], + &RepeatPeriod::DAILY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [] + ); + } + + #[test] + fn test_parse_by_day_weekly() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 9).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "SA".to_string(), + }, + ], + &RepeatPeriod::WEEKLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [date.replace_day(10).unwrap(), date.replace_day(11).unwrap()] + ); + } + + #[test] + fn test_parse_by_day_monthly() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + }, ], + &RepeatPeriod::MONTHLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [ + date, + date.replace_day(13).unwrap(), + date.replace_day(20).unwrap(), + date.replace_day(27).unwrap() + ] + ); + } + + #[test] + fn test_parse_by_day_monthly_with_monthday() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + let rules = vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "7".to_string(), + }, + ]; + let by_day_rules: Vec<&ByRule> = rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYDAY) + .collect(); + let by_month_day_rules: Vec<&ByRule> = rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) + .collect(); + + let valid_month_days: Vec = by_month_day_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &by_day_rules, + &RepeatPeriod::MONTHLY, + vec![], + Weekday::Monday, + false, + valid_month_days, + vec![], + ), + [] + ); + } + + #[test] + fn test_parse_by_day_monthly_with_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + }, ], + &RepeatPeriod::MONTHLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [date.replace_day(13).unwrap()] + ); + } + + #[test] + fn test_parse_by_day_monthly_with_monthday_and_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + let rules = vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "7".to_string(), + }, + ]; + let by_day_rules: Vec<&ByRule> = rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYDAY) + .collect(); + let by_month_day_rules: Vec<&ByRule> = rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) + .collect(); + + let valid_month_days: Vec = by_month_day_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &by_day_rules, + &RepeatPeriod::MONTHLY, + vec![], + Weekday::Monday, + false, + valid_month_days, + vec![], + ), + [] + ); + } + + #[test] + fn test_parse_by_day_yearly() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + let end_date = date.replace_year(2026).unwrap(); + let mut current_date = date; + let mut expected_dates: Vec = Vec::new(); + + while current_date.assume_utc().unix_timestamp() < end_date.assume_utc().unix_timestamp() { + expected_dates.push(current_date); + current_date = current_date.add(Duration::days(7)) + } + + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + }, ], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + expected_dates + ); + } + + #[test] + fn test_parse_by_day_yearly_with_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + }, ], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [date.replace_day(13).unwrap(), ] + ); + } + + #[test] + fn test_parse_by_day_yearly_with_ordinal_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "35".to_string(), + }, ], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [date + .replace_month(Month::February) + .unwrap() + .replace_day(4) + .unwrap(), ] + ); + } + + #[test] + fn test_parse_by_day_yearly_with_weekno() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "6".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + true, + vec![], + vec![], + ), + [date] + ); + } + + #[test] + fn test_parse_by_day_yearly_with_unmatch_weekno() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "35".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "7".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + true, + vec![], + vec![], + ), + [] + ); + } + + #[test] + fn test_parse_by_day_yearly_with_invalid_rule() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "6".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + true, + vec![], + vec![], + ), + [] + ); + } + + #[test] + fn test_flow_with_by_month_daily() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::March, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::DAILY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "3".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "6".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::January).unwrap().to_date_time(), + repeat_rule.clone(), + ), + [] + ); + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.to_date_time()] + ); + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::February).unwrap().to_date_time(), + repeat_rule.clone(), + ), + [date.replace_month(Month::February).unwrap().to_date_time()] + ); + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::June).unwrap().to_date_time(), + repeat_rule.clone(), + ), + [date.replace_month(Month::June).unwrap().to_date_time()] + ); + } + + #[test] + fn test_flow_daily_with_by_month_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::DAILY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [] + ); + } + + #[test] + fn test_flow_daily_with_by_month_and_by_day_and_by_monthday() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 14).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::DAILY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "14".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(14).unwrap().to_date_time()] + ); + assert_eq!( + event_recurrence + .generate_future_instances(date.replace_day(13).unwrap().to_date_time(), repeat_rule.clone()), + [] + ); + } + + #[test] + fn test_flow_weekly_with_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::WEEKLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(10).unwrap().to_date_time(), ] + ); + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::January).unwrap().to_date_time(), + repeat_rule.clone(), + ), + [] + ); + } + + #[test] + fn test_flow_weekly_with_by_month_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::WEEKLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(13).unwrap().to_date_time(), date.replace_day(14).unwrap().to_date_time()] + ); + } + + #[test] + fn test_flow_weekly_with_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::WEEKLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(13).unwrap().to_date_time(), date.replace_day(14).unwrap().to_date_time()] + ); + } + + #[test] + fn test_flow_weekly_with_by_day_and_wkst() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::WEEKLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::WKST, + interval: "FR".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(14).unwrap().to_date_time(), date.replace_day(20).unwrap().to_date_time()] + ); + } + + #[test] + fn test_flow_monthly_with_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(14).unwrap().to_date_time(), + date.replace_day(21).unwrap().to_date_time(), + date.replace_day(28).unwrap().to_date_time() + ] + ); + } + + #[test] + fn test_flow_monthly_with_second_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2FR".to_string(), + }], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(14).unwrap().to_date_time(), ] + ); + } + + #[test] + fn test_flow_monthly_with_two_last_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "-1FR".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "-2FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(21).unwrap().to_date_time(), date.replace_day(28).unwrap().to_date_time(), ] + ); + } + + #[test] + fn test_flow_monthly_with_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + let date_not_in_range = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::March, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(10).unwrap().to_date_time(), ] + ); + assert_eq!( + event_recurrence.generate_future_instances(date_not_in_range.to_date_time(), repeat_rule.clone()), + [] + ); + } + + #[test] + fn test_flow_monthly_with_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "25".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "28".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(25).unwrap().to_date_time(), date.replace_day(28).unwrap().to_date_time(), ] + ); + } + + #[test] + fn test_flow_monthly_with_by_month_day_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "25".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "28".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(28).unwrap().to_date_time()] + ); + } + + #[test] + fn test_flow_monthly_with_by_month_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(13).unwrap().to_date_time(), + date.replace_day(14).unwrap().to_date_time(), + date.replace_day(20).unwrap().to_date_time(), + date.replace_day(21).unwrap().to_date_time(), + date.replace_day(27).unwrap().to_date_time(), + date.replace_day(28).unwrap().to_date_time() + ] + ); + } + + #[test] + fn test_flow_yearly_with_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let stop_condition = PrimitiveDateTime::new( + Date::from_calendar_date(2026, Month::February, 10).unwrap(), + time, + ); + let mut expected_dates: Vec = Vec::new(); + let mut current_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 13).unwrap(), + time, + ); + + while current_date.assume_utc().unix_timestamp() + < stop_condition.assume_utc().unix_timestamp() + { + expected_dates.push(current_date.to_date_time()); + expected_dates.push(current_date.add(Duration::days(1)).to_date_time()); + + current_date = current_date.add(Duration::days(7)); + } + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + expected_dates + ); + } + + #[test] + fn test_flow_yearly_with_by_day_and_by_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "44".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(13).unwrap().to_date_time()] + ); + + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::March).unwrap().to_date_time(), + repeat_rule.clone(), + ), + [] + ); + } + + #[test] + fn test_flow_yearly_with_by_week_no_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "8".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(20).unwrap().to_date_time()] + ); + + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::March).unwrap().to_date_time(), + repeat_rule.clone(), + ), + [date.replace_year(2026).unwrap().replace_day(19).unwrap().to_date_time()] + ); + } + + #[test] + fn test_flow_yearly_with_negative_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::December, 4).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::December, 1).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "-5".to_string(), + }], + }; + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.generate_future_instances(valid_date.to_date_time(), repeat_rule), + [] + ); + } + + #[test] + fn test_flow_yearly_with_by_week_no_and_wkst() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "8".to_string(), + }, + ByRule { + by_rule: ByRuleType::WKST, + interval: "TU".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(18).unwrap().to_date_time(), + date.replace_day(19).unwrap().to_date_time(), + date.replace_day(20).unwrap().to_date_time(), + date.replace_day(21).unwrap().to_date_time(), + date.replace_day(22).unwrap().to_date_time(), + date.replace_day(23).unwrap().to_date_time(), + date.replace_day(24).unwrap().to_date_time(), + ] + ); + } + + #[test] + fn test_flow_yearly_with_by_month_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(13).unwrap().to_date_time(), + date.replace_day(14).unwrap().to_date_time(), + date.replace_day(20).unwrap().to_date_time(), + date.replace_day(21).unwrap().to_date_time(), + date.replace_day(27).unwrap().to_date_time(), + date.replace_day(28).unwrap().to_date_time(), + date.replace_year(2026).unwrap().replace_day(5).unwrap().to_date_time(), + date.replace_year(2026).unwrap().replace_day(6).unwrap().to_date_time(), + ] + ); + } +} diff --git a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs b/tuta-sdk/rust/sdk/src/date/event_recurrence.rs deleted file mode 100644 index d37a0ad5c62b..000000000000 --- a/tuta-sdk/rust/sdk/src/date/event_recurrence.rs +++ /dev/null @@ -1,2737 +0,0 @@ -use std::ops::{Add, Sub}; - -use regex::Regex; -use time::util::weeks_in_year; -use time::{Date, Duration, Month, PrimitiveDateTime, Weekday}; - -#[derive(PartialEq, Clone)] -enum ByRuleType { - BYMINUTE, - BYHOUR, - BYDAY, - BYMONTHDAY, - BYYEARDAY, - BYWEEKNO, - BYMONTH, - BYSETPOS, - WKST, -} - -impl ByRuleType { - fn value(&self) -> &str { - match *self { - ByRuleType::BYMINUTE => "0", - ByRuleType::BYHOUR => "1", - ByRuleType::BYDAY => "2", - ByRuleType::BYMONTHDAY => "3", - ByRuleType::BYYEARDAY => "4", - ByRuleType::BYWEEKNO => "5", - ByRuleType::BYMONTH => "6", - ByRuleType::BYSETPOS => "7", - ByRuleType::WKST => "8", - } - } - - fn from_str(value: &str) -> ByRuleType { - match value { - "0" => ByRuleType::BYMINUTE, - "1" => ByRuleType::BYHOUR, - "2" => ByRuleType::BYDAY, - "3" => ByRuleType::BYMONTHDAY, - "4" => ByRuleType::BYYEARDAY, - "5" => ByRuleType::BYWEEKNO, - "6" => ByRuleType::BYMONTH, - "7" => ByRuleType::BYSETPOS, - "8" => ByRuleType::WKST, - _ => panic!("Invalid ByRule {value}"), - } - } -} - -#[derive(PartialEq, Clone)] -enum RepeatPeriod { - DAILY, - WEEKLY, - MONTHLY, - ANNUALLY, -} - -impl RepeatPeriod { - fn value(&self) -> &str { - match *self { - RepeatPeriod::DAILY => "0", - RepeatPeriod::WEEKLY => "1", - RepeatPeriod::MONTHLY => "2", - RepeatPeriod::ANNUALLY => "3", - } - } - - fn from_str(value: &str) -> RepeatPeriod { - match value { - "0" => RepeatPeriod::DAILY, - "1" => RepeatPeriod::WEEKLY, - "2" => RepeatPeriod::MONTHLY, - "3" => RepeatPeriod::ANNUALLY, - _ => panic!("Invalid RepeatPeriod {value}"), - } - } -} - -#[derive(Clone)] -pub struct ByRule { - by_rule: ByRuleType, - interval: String, -} - -#[derive(Clone)] -pub struct RepeatRule { - frequency: RepeatPeriod, - by_rules: Vec, -} - -trait MonthNumber { - fn to_number(&self) -> u8; - fn from_number(number: u8) -> Month; -} - -trait WeekdayString { - fn from_short(short_weekday: &str) -> Weekday; -} - -impl MonthNumber for Month { - fn to_number(&self) -> u8 { - match *self { - Month::January => 1, - Month::February => 2, - Month::March => 3, - Month::April => 4, - Month::May => 5, - Month::June => 6, - Month::July => 7, - Month::August => 8, - Month::September => 9, - Month::October => 10, - Month::November => 11, - Month::December => 12, - } - } - - fn from_number(number: u8) -> Month { - match number { - 1 => Month::January, - 2 => Month::February, - 3 => Month::March, - 4 => Month::April, - 5 => Month::May, - 6 => Month::June, - 7 => Month::July, - 8 => Month::August, - 9 => Month::September, - 10 => Month::October, - 11 => Month::November, - 12 => Month::December, - _ => panic!("Invalid Month {number}"), - } - } -} - -impl WeekdayString for Weekday { - fn from_short(short_weekday: &str) -> Weekday { - match short_weekday { - "MO" => Weekday::Monday, - "TU" => Weekday::Tuesday, - "WE" => Weekday::Wednesday, - "TH" => Weekday::Thursday, - "FR" => Weekday::Friday, - "SA" => Weekday::Saturday, - "SU" => Weekday::Sunday, - _ => panic!("Invalid Weekday {short_weekday}"), - } - } -} - -trait DateExpansion { - fn add_month(&self) -> Date; -} - -impl DateExpansion for Date { - fn add_month(&self) -> Date { - self.add(Duration::days(i64::from(self.month().length(self.year())))) - } -} - -pub struct EventRecurrence; - -impl<'a> EventRecurrence { - pub fn new() -> Self { - EventRecurrence {} - } - pub fn generate_future_instances( - &self, - date: PrimitiveDateTime, - repeat_rule: RepeatRule, - ) -> Vec { - let by_month_rules: Vec<&ByRule> = repeat_rule - .by_rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYMONTH) - .collect(); - let by_day_rules: Vec<&ByRule> = repeat_rule - .by_rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYDAY) - .collect(); - let by_month_day_rules: Vec<&ByRule> = repeat_rule - .by_rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) - .collect(); - let by_year_day_rules: Vec<&ByRule> = repeat_rule - .by_rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYYEARDAY) - .collect(); - let by_week_no_rules: Vec<&ByRule> = repeat_rule - .by_rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYWEEKNO) - .collect(); - - let week_start: Weekday; - - if repeat_rule.frequency == RepeatPeriod::ANNUALLY - || repeat_rule.frequency == RepeatPeriod::WEEKLY - { - week_start = match repeat_rule - .by_rules - .iter() - .find(|&x| x.by_rule == ByRuleType::WKST) - { - Some(rule) => match rule.interval.as_str() { - "MO" => Weekday::Monday, - "TU" => Weekday::Tuesday, - "WE" => Weekday::Wednesday, - "TH" => Weekday::Thursday, - "FR" => Weekday::Friday, - "SA" => Weekday::Saturday, - "SU" => Weekday::Sunday, - _ => Weekday::Monday, - }, - None => Weekday::Monday, - }; - } else { - week_start = Weekday::Monday - } - - let valid_months: Vec = by_month_rules - .iter() - .clone() - .map(|&x| x.interval.parse::().unwrap()) - .collect(); - let valid_month_days: Vec = by_month_day_rules - .iter() - .clone() - .map(|&x| x.interval.parse::().unwrap()) - .collect(); - let valid_year_days: Vec = by_year_day_rules - .iter() - .clone() - .map(|&x| x.interval.parse::().unwrap()) - .collect(); - - let month_applied_events: Vec = - self.apply_month_rules(&vec![date], &by_month_rules, &repeat_rule.frequency); - - let week_no_applied_events: Vec = - if repeat_rule.frequency == RepeatPeriod::ANNUALLY { - self.apply_week_no_rules(month_applied_events, &by_week_no_rules, week_start) - } else { - month_applied_events - }; - - let year_day_applied_events: Vec = - if repeat_rule.frequency == RepeatPeriod::ANNUALLY { - self.apply_year_day_rules( - week_no_applied_events, - &by_year_day_rules, - by_week_no_rules.len() > 0, - by_month_rules.len() > 0, - ) - } else { - week_no_applied_events - }; - - let month_day_applied_events: Vec = self.apply_month_day_rules( - year_day_applied_events, - &by_month_day_rules, - &repeat_rule.frequency == &RepeatPeriod::DAILY, - ); - let day_applied_events: Vec = self.apply_day_rules( - month_day_applied_events, - &by_day_rules, - &repeat_rule.frequency, - valid_months.clone(), - week_start, - by_week_no_rules.len() > 0, - valid_month_days, - valid_year_days, - ); - - let date_timestamp = date.assume_utc().unix_timestamp(); - self.finish_rules( - day_applied_events, - valid_months.clone(), - Some(date_timestamp), - ) - } - - fn apply_month_rules( - &self, - dates: &Vec, - rules: &Vec<&'a ByRule>, - frequency: &RepeatPeriod, - ) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - - for &rule in rules { - for date in dates { - let target_month: u8 = match rule.interval.parse::() { - Ok(month) => month, - _ => continue, - }; - - if frequency == &RepeatPeriod::WEEKLY { - let week_start = PrimitiveDateTime::new( - Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Monday) - .unwrap(), - date.time(), - ); - let week_end = PrimitiveDateTime::new( - Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Sunday) - .unwrap(), - date.time(), - ); - - let week_start_year = week_start.year(); - let week_end_year = week_end.year(); - - let week_start_month = week_start.month().to_number(); - let week_end_month = week_end.month().to_number(); - - let is_target_month = - week_end_month == target_month || week_start_month == target_month; - - if week_start_year == week_end_year - && week_start_month < week_end_month - && is_target_month - { - new_dates.push(date.clone()); - continue; - } else if week_start_year < week_end_year && is_target_month { - new_dates.push(date.clone()); - continue; - } - } else if frequency == &RepeatPeriod::ANNUALLY { - let new_date = - match date.clone().replace_month(Month::from_number(target_month)) { - Ok(dt) => dt, - _ => continue, - }; - - let years_to_add = if date.year() == new_date.year() - && date.month().to_number() > target_month - { - 1 - } else { - 0 - }; - - new_dates.push( - match new_date.replace_year(new_date.year() + years_to_add) { - Ok(date) => date, - _ => continue, - }, - ); - - continue; - } - - if date.month().to_number() == target_month { - new_dates.push(date.clone()); - } - } - } - - new_dates - } - - fn apply_week_no_rules( - &self, - dates: Vec, - rules: &Vec<&'a ByRule>, - week_start: Weekday, - ) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - - for &rule in rules { - for date in &dates { - let parsed_week: i8 = match rule.interval.parse::() { - Ok(week) => week, - _ => continue, - }; - - let mut new_date = date.clone(); - let mut week_number: u8; - - if parsed_week < 0 { - week_number = weeks_in_year(date.year()) - parsed_week.unsigned_abs() + 1 - } else { - new_date = new_date.replace_date( - Date::from_iso_week_date( - new_date.year(), - parsed_week as u8, - new_date.weekday(), - ) - .unwrap(), - ); - week_number = parsed_week as u8 - } - - let year_offset = if new_date.assume_utc().unix_timestamp() - < date.assume_utc().unix_timestamp() - { - date.year() - new_date.year() + 1 - } else { - 0 - }; - let year = new_date.year() + year_offset; - new_date = new_date - .replace_date(Date::from_iso_week_date(year, week_number, week_start).unwrap()); - - for i in 0..7 { - let final_date = new_date.add(Duration::days(i)); - if final_date.year() > new_date.year() { - break; - } - - new_dates.push(final_date) - } - } - } - - new_dates - } - - fn apply_year_day_rules( - &self, - dates: Vec, - rules: &Vec<&ByRule>, - evaluate_same_week: bool, - evaluate_same_month: bool, - ) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - - for &rule in rules { - for date in &dates { - let parsed_day: i64 = match rule.interval.parse::() { - Ok(day) => day, - _ => continue, - }; - - let mut new_date: PrimitiveDateTime; - if parsed_day.is_negative() { - new_date = date - .replace_month(Month::December) - .unwrap() - .replace_day(31) - .unwrap() - .sub(Duration::days((parsed_day.unsigned_abs() - 1) as i64)); - } else { - new_date = date - .replace_month(Month::January) - .unwrap() - .replace_day(1) - .unwrap() - .add(Duration::days(parsed_day - 1)); - } - - let year_offset = if new_date.assume_utc().unix_timestamp() - < date.assume_utc().unix_timestamp() - { - 1 - } else { - 0 - }; - new_date = match new_date.replace_year(new_date.year() + year_offset) { - Ok(date) => date, - _ => continue, - }; - - if (evaluate_same_week && date.iso_week() != new_date.iso_week()) - || (evaluate_same_month && date.month() != new_date.month()) - { - continue; - } - - new_dates.push(new_date) - } - } - - new_dates - } - - fn apply_month_day_rules( - &self, - dates: Vec, - rules: &Vec<&ByRule>, - is_daily_event: bool, - ) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - - for &rule in rules { - for date in &dates { - let target_day: i8 = match rule.interval.parse::() { - Ok(day) => day, - _ => continue, - }; - let days_diff = - date.month().length(date.year()) as i8 - target_day.unsigned_abs() as i8 + 1; - - if is_daily_event { - if target_day.is_positive() && date.day() == target_day.unsigned_abs() { - new_dates.push(date.clone()); - } else if target_day.is_negative() && days_diff == date.day() as i8 { - new_dates.push(date.clone()); - } - - continue; - } - - if target_day >= 0 && target_day.unsigned_abs() <= date.month().length(date.year()) - { - let date = match date.replace_day(target_day.unsigned_abs()) { - Ok(date) => date, - _ => continue, - }; - - new_dates.push(date); - } else if days_diff > 0 - && target_day.unsigned_abs() <= date.month().length(date.year()) - { - let date = match date.replace_day(days_diff.unsigned_abs()) { - Ok(date) => date, - _ => continue, - }; - - new_dates.push(date); - } - } - } - - new_dates - } - - fn apply_day_rules( - &self, - dates: Vec, - rules: &Vec<&ByRule>, - frequency: &RepeatPeriod, - valid_months: Vec, - week_start: Weekday, - has_week_no: bool, - valid_month_days: Vec, - valid_year_days: Vec, - ) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - let regex = Regex::new(r"^([-+]?\d{0,3})([a-zA-Z]{2})?$").unwrap(); - - for &rule in rules { - for date in &dates { - let Some(parsed_rule) = regex.captures(rule.interval.as_str()) else { - continue; - }; - let target_week_day = parsed_rule.get(2); - let leading_value = parsed_rule.get(1); - - if frequency == &RepeatPeriod::DAILY - && target_week_day.is_some() - && date.weekday() == Weekday::from_short(target_week_day.unwrap().as_str()) - { - new_dates.push(date.clone()) - } else if frequency == &RepeatPeriod::WEEKLY && target_week_day.is_some() { - let mut new_date = date.replace_date( - Date::from_iso_week_date( - date.year(), - date.iso_week(), - Weekday::from_short(target_week_day.unwrap().as_str()), - ) - .unwrap(), - ); - let interval_start = date.replace_date( - Date::from_iso_week_date(date.year(), date.iso_week(), week_start).unwrap(), - ); - - if new_date.assume_utc().unix_timestamp() - > interval_start - .add(Duration::weeks(1)) - .assume_utc() - .unix_timestamp() - { - continue; - } else if new_date.assume_utc().unix_timestamp() - < interval_start.assume_utc().unix_timestamp() - { - new_date = new_date.add(Duration::weeks(1)); - } - - if valid_months.len() == 0 - || valid_months.contains(&new_date.month().to_number()) - { - new_dates.push(new_date) - } - } else if frequency == &RepeatPeriod::MONTHLY && target_week_day.is_some() { - let mut allowed_days: Vec = Vec::new(); - - let week_change = - match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { - Ok(val) => val, - _ => 0, - }; - - let base_date = date.replace_day(1).unwrap(); - let stop_condition = - PrimitiveDateTime::new(base_date.date().add_month(), base_date.time()); - - for allowed_day in &valid_month_days { - if allowed_day.is_positive() { - allowed_days.push(allowed_day.unsigned_abs()); - continue; - } - - let day = - base_date.month().length(date.year()) - allowed_day.unsigned_abs() + 1; - allowed_days.push(day); - } - - let is_allowed_in_month_day = |day: u8| -> bool { - if allowed_days.len() == 0 { - return true; - } - - allowed_days.contains(&day) - }; - - let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); - - if week_change != 0 { - let mut new_date = base_date; - if week_change.is_negative() { - new_date = new_date - .replace_day(new_date.month().length(new_date.year())) - .unwrap(); - new_date = new_date.replace_date( - Date::from_iso_week_date( - new_date.year(), - new_date.iso_week(), - parsed_weekday, - ) - .unwrap(), - ); - - let new_week = new_date.iso_week() - week_change.unsigned_abs() + 1; - new_date = new_date.replace_date( - Date::from_iso_week_date( - new_date.year(), - new_week, - new_date.weekday(), - ) - .unwrap(), - ) - } else { - while new_date.weekday() != parsed_weekday { - new_date = new_date.add(Duration::days(1)); - } - - new_date = new_date.replace_date( - Date::from_iso_week_date( - new_date.year(), - new_date.iso_week() + week_change.unsigned_abs() - 1, - new_date.weekday(), - ) - .unwrap(), - ) - } - - if new_date.assume_utc().unix_timestamp() - >= base_date.assume_utc().unix_timestamp() - && new_date.assume_utc().unix_timestamp() - <= stop_condition.assume_utc().unix_timestamp() - && is_allowed_in_month_day(new_date.day()) - { - new_dates.push(new_date) - } - } else { - let mut current_date = base_date; - while current_date.assume_utc().unix_timestamp() - < stop_condition.assume_utc().unix_timestamp() - { - let new_date = current_date.replace_date( - Date::from_iso_week_date( - current_date.year(), - current_date.iso_week(), - parsed_weekday, - ) - .unwrap(), - ); - if new_date.assume_utc().unix_timestamp() - >= base_date.assume_utc().unix_timestamp() - && is_allowed_in_month_day(new_date.day()) - { - if valid_months.len() > 0 - && valid_months.contains(&new_date.month().to_number()) - { - new_dates.push(new_date) - } else if valid_months.len() == 0 { - new_dates.push(new_date) - } - } - - current_date = new_date.add(Duration::days(7)); - } - } - } else if frequency == &RepeatPeriod::ANNUALLY { - let week_change = - match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { - Ok(val) => val, - _ => 0, - }; - - if has_week_no && week_change != 0 { - println!( - "Invalid repeat rule, can't use BYWEEKNO with Week Offset on BYDAY" - ); - continue; - } - - if week_change != 0 && !has_week_no { - let mut new_date: PrimitiveDateTime; - - if !target_week_day.is_some() { - if week_change > 0 { - new_date = date - .replace_day(1) - .unwrap() - .replace_month(Month::January) - .unwrap() - .add(Duration::days(week_change - 1)) - } else { - new_date = date - .replace_month(Month::December) - .unwrap() - .replace_day(31) - .unwrap() - .sub(Duration::days(week_change.abs() - 1)) - } - } else { - let parsed_weekday = - Weekday::from_short(target_week_day.unwrap().as_str()); - - if week_change > 0 { - new_date = date - .replace_day(1) - .unwrap() - .replace_month(Month::January) - .unwrap() - .add(Duration::weeks(week_change - 1)); - - while new_date.weekday() != parsed_weekday { - new_date = new_date.add(Duration::days(1)); - } - } else { - new_date = date - .replace_month(Month::December) - .unwrap() - .replace_day(31) - .unwrap() - .sub(Duration::weeks(week_change.abs() - 1)); - while new_date.weekday() != parsed_weekday { - new_date = new_date.sub(Duration::days(1)); - } - } - } - - if new_date.assume_utc().unix_timestamp() - < date.assume_utc().unix_timestamp() - { - match new_date.replace_year(new_date.year() + 1) { - Ok(dt) => new_dates.push(dt), - _ => continue, - } - } else { - new_dates.push(new_date) - } - } else if has_week_no { - if !target_week_day.is_some() { - continue; - } - - let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); - let new_date = date.replace_date( - Date::from_iso_week_date(date.year(), date.iso_week(), parsed_weekday) - .unwrap(), - ); - - let interval_start = date.replace_date( - Date::from_iso_week_date(date.year(), date.iso_week(), week_start) - .unwrap(), - ); - let week_ahead = interval_start.add(Duration::days(7)); - - if new_date.assume_utc().unix_timestamp() - > week_ahead.assume_utc().unix_timestamp() - || new_date.assume_utc().unix_timestamp() - < date.assume_utc().unix_timestamp() - { - } else if new_date.assume_utc().unix_timestamp() - < interval_start.assume_utc().unix_timestamp() - { - new_dates.push(interval_start.add(Duration::days(7))); - } else { - new_dates.push(new_date); - } - } else { - if !target_week_day.is_some() { - continue; - } - - let day_one = date.replace_day(1).unwrap(); - let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); - - let stop_date = match Date::from_calendar_date( - date.year() + 1, - date.month(), - date.day(), - ) { - Ok(date) => date, - _ => continue, - }; - - let stop_condition = date.replace_date(stop_date); - let mut current_date = date.replace_date( - Date::from_iso_week_date( - date.year(), - day_one.iso_week(), - parsed_weekday, - ) - .unwrap(), - ); - - if current_date.assume_utc().unix_timestamp() - >= day_one.assume_utc().unix_timestamp() - { - new_dates.push(current_date); - } - - current_date = current_date.add(Duration::days(7)); - - while current_date.assume_utc().unix_timestamp() - < stop_condition.assume_utc().unix_timestamp() - { - new_dates.push(current_date); - current_date = current_date.add(Duration::days(7)); - } - } - } - } - } - - if frequency == &RepeatPeriod::ANNUALLY { - return new_dates - .iter() - .filter(|date| self.is_valid_day_in_year(**date, valid_year_days.clone())) - .map(|date| *date) - .collect(); - } - - new_dates - } - - fn get_valid_days_in_year(&self, year: i32, valid_year_days: &Vec) -> Vec { - let days_in_year = Date::from_calendar_date(year, Month::December, 31) - .unwrap() - .ordinal(); - let mut allowed_days: Vec = Vec::new(); - - for allowed_day in valid_year_days { - if allowed_day > &0 { - allowed_days.push(allowed_day.abs() as u16); - continue; - } - - let day = days_in_year - allowed_day.unsigned_abs() + 1; - allowed_days.push(day); - } - - allowed_days - } - - fn is_valid_day_in_year(&self, date: PrimitiveDateTime, valid_year_days: Vec) -> bool { - let valid_days = self.get_valid_days_in_year(date.year(), &valid_year_days); - - if valid_days.len() == 0 { - return true; - } - - let day_in_year = date.ordinal(); - - let is_valid = valid_days.contains(&day_in_year); - - return is_valid; - } - - fn finish_rules( - &self, - dates: Vec, - valid_months: Vec, - event_start_time: Option, - ) -> Vec { - let mut clean_dates; - - if valid_months.len() > 0 { - clean_dates = dates - .iter() - .filter(|date| valid_months.contains(&date.month().to_number())) - .map(|date| *date) - .collect(); - } else { - clean_dates = dates - }; - - if event_start_time.is_some() { - clean_dates = clean_dates - .iter() - .filter(|date| { - let date_unix_timestamp = date.assume_utc().unix_timestamp(); - date_unix_timestamp >= event_start_time.unwrap() - }) - .map(|date| *date) - .collect(); - } - - clean_dates.sort_by(|a, b| { - a.assume_utc() - .unix_timestamp() - .cmp(&b.assume_utc().unix_timestamp()) - }); - clean_dates.dedup(); - - clean_dates - } -} - -#[cfg(test)] -mod tests { - use time::{Date, Month, PrimitiveDateTime, Time}; - - use super::*; - - #[test] - fn test_parse_weekly_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::January, 23).unwrap(), - time, - ); - let invalid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::March, 11).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![valid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::WEEKLY - ), - vec![valid_date] - ); - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![invalid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::WEEKLY - ), - vec![] - ); - } - - #[test] - fn test_parse_monthly_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::January, 23).unwrap(), - time, - ); - let invalid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::March, 11).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![valid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::MONTHLY - ), - vec![valid_date] - ); - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![invalid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::MONTHLY - ), - vec![] - ); - } - - #[test] - fn test_parse_yearly_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::January, 23).unwrap(), - time, - ); - let to_next_year = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::March, 11).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![valid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::ANNUALLY - ), - vec![ - valid_date, - valid_date.replace_month(Month::February).unwrap() - ] - ); - - // BYMONTH never limits on Yearly, just expands - assert_eq!( - event_recurrence.apply_month_rules( - &vec![to_next_year], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::ANNUALLY, - ), - vec![ - to_next_year - .replace_year(2025) - .unwrap() - .replace_month(Month::January) - .unwrap(), - to_next_year - .replace_year(2025) - .unwrap() - .replace_month(Month::February) - .unwrap(), - ] - ); - } - - #[test] - fn test_parse_daily_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::January, 23).unwrap(), - time, - ); - let invalid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::March, 11).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![valid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::DAILY - ), - vec![valid_date] - ); - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![invalid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::DAILY - ), - vec![] - ); - } - - #[test] - fn test_parse_positive_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 31).unwrap(), - time, - ); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2025, Month::January, 27).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new( - base_date.add(Duration::days(i)), - time, - )); - } - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_week_no_rules( - vec![valid_date], - &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "5".to_string(), - },], - Weekday::Monday - ), - valid_dates - ); - } - - #[test] - fn test_parse_wkst_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 31).unwrap(), - time, - ); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2025, Month::January, 28).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new( - base_date.add(Duration::days(i)), - time, - )); - } - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_week_no_rules( - vec![valid_date], - &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "5".to_string(), - },], - Weekday::Tuesday - ), - valid_dates - ); - } - - #[test] - fn test_parse_negative_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::December, 4).unwrap(), - time, - ); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2025, Month::December, 1).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new( - base_date.add(Duration::days(i)), - time, - )); - } - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_week_no_rules( - vec![valid_date], - &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "-5".to_string(), - },], - Weekday::Monday - ), - valid_dates - ); - } - - #[test] - fn test_parse_edge_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2026, Month::December, 29).unwrap(), - time, - ); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2026, Month::December, 28).unwrap(); - for i in 0..4 { - valid_dates.push(PrimitiveDateTime::new( - base_date.add(Duration::days(i)), - time, - )); - } - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_week_no_rules( - vec![valid_date], - &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "-1".to_string(), - },], - Weekday::Monday - ), - valid_dates - ); - } - - #[test] - fn test_parse_out_of_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 22).unwrap(), - time, - ); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2026, Month::January, 26).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new( - base_date.add(Duration::days(i)), - time, - )); - } - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_week_no_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "5".to_string(), - },], - Weekday::Monday - ), - valid_dates - ); - } - - #[test] - fn test_parse_year_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 1).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_year_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "40".to_string(), - }], - false, - false - ), - [date.replace_day(9).unwrap()] - ); - } - - #[test] - fn test_parse_year_day_keep_week() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 1).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_year_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "40".to_string(), - }], - true, - false - ), - [] - ); - } - - #[test] - fn test_parse_year_day_keep_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 22).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_year_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "40".to_string(), - }], - true, - true - ), - [] - ); - } - - #[test] - fn test_parse_out_of_year_year_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 22).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_year_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "40".to_string(), - }], - false, - false - ), - [date.replace_year(2026).unwrap().replace_day(9).unwrap()] - ); - } - - #[test] - fn test_parse_negative_year_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 22).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_year_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "-1".to_string(), - }], - false, - false - ), - [date - .replace_month(Month::December) - .unwrap() - .replace_day(31) - .unwrap()] - ); - } - - #[test] - fn test_parse_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 22).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_month_day_rules( - vec![date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "10".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "20".to_string(), - }, - ], - false - ), - [date.replace_day(10).unwrap(), date.replace_day(20).unwrap()] - ); - } - - #[test] - fn test_parse_invalid_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 22).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_month_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "30".to_string(), - },], - false - ), - [] - ); - } - - #[test] - fn test_parse_daily_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 20).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_month_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "20".to_string(), - }], - false - ), - [date.replace_day(20).unwrap()] - ); - } - - #[test] - fn test_parse_negative_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_month_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "-1".to_string(), - },], - false - ), - [date.replace_day(31).unwrap(),] - ); - } - - #[test] - fn test_parse_invalid_date_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_month_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "32".to_string(), - },], - false - ), - [] - ); - } - - #[test] - fn test_parse_by_day_daily() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }], - &RepeatPeriod::DAILY, - vec![], - Weekday::Monday, - false, - vec![], - vec![] - ), - [date] - ); - } - - #[test] - fn test_parse_by_day_daily_invalid() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 8).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }], - &RepeatPeriod::DAILY, - vec![], - Weekday::Monday, - false, - vec![], - vec![] - ), - [] - ); - } - - #[test] - fn test_parse_by_day_weekly() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 9).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "SA".to_string(), - }, - ], - &RepeatPeriod::WEEKLY, - vec![], - Weekday::Monday, - false, - vec![], - vec![] - ), - [date.replace_day(10).unwrap(), date.replace_day(11).unwrap()] - ); - } - - #[test] - fn test_parse_by_day_monthly() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 6).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - // Can be WEEKDAY + WEEK - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "MO".to_string(), - },], - &RepeatPeriod::MONTHLY, - vec![], - Weekday::Monday, - false, - vec![], - vec![] - ), - [ - date, - date.replace_day(13).unwrap(), - date.replace_day(20).unwrap(), - date.replace_day(27).unwrap() - ] - ); - } - - #[test] - fn test_parse_by_day_monthly_with_monthday() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 6).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - let rules = vec![ - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "MO".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "7".to_string(), - }, - ]; - let by_day_rules: Vec<&ByRule> = rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYDAY) - .collect(); - let by_month_day_rules: Vec<&ByRule> = rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) - .collect(); - - let valid_month_days: Vec = by_month_day_rules - .iter() - .clone() - .map(|&x| x.interval.parse::().unwrap()) - .collect(); - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &by_day_rules, - &RepeatPeriod::MONTHLY, - vec![], - Weekday::Monday, - false, - valid_month_days, - vec![] - ), - [] - ); - } - - #[test] - fn test_parse_by_day_monthly_with_week() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - // Can be WEEKDAY + WEEK - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2MO".to_string(), - },], - &RepeatPeriod::MONTHLY, - vec![], - Weekday::Monday, - false, - vec![], - vec![] - ), - [date.replace_day(13).unwrap()] - ); - } - - #[test] - fn test_parse_by_day_monthly_with_monthday_and_week() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 6).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - let rules = vec![ - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2MO".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "7".to_string(), - }, - ]; - let by_day_rules: Vec<&ByRule> = rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYDAY) - .collect(); - let by_month_day_rules: Vec<&ByRule> = rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) - .collect(); - - let valid_month_days: Vec = by_month_day_rules - .iter() - .clone() - .map(|&x| x.interval.parse::().unwrap()) - .collect(); - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &by_day_rules, - &RepeatPeriod::MONTHLY, - vec![], - Weekday::Monday, - false, - valid_month_days, - vec![] - ), - [] - ); - } - - #[test] - fn test_parse_by_day_yearly() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 6).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - let end_date = date.replace_year(2026).unwrap(); - let mut current_date = date; - let mut expected_dates: Vec = Vec::new(); - - while current_date.assume_utc().unix_timestamp() < end_date.assume_utc().unix_timestamp() { - expected_dates.push(current_date); - current_date = current_date.add(Duration::days(7)) - } - - // Can be WEEKDAY + WEEK - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "MO".to_string(), - },], - &RepeatPeriod::ANNUALLY, - vec![], - Weekday::Monday, - false, - vec![], - vec![] - ), - expected_dates - ); - } - - #[test] - fn test_parse_by_day_yearly_with_week() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - // Can be WEEKDAY + WEEK - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2MO".to_string(), - },], - &RepeatPeriod::ANNUALLY, - vec![], - Weekday::Monday, - false, - vec![], - vec![] - ), - [date.replace_day(13).unwrap(),] - ); - } - - #[test] - fn test_parse_by_day_yearly_with_ordinal_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - // Can be WEEKDAY + WEEK - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "35".to_string(), - },], - &RepeatPeriod::ANNUALLY, - vec![], - Weekday::Monday, - false, - vec![], - vec![] - ), - [date - .replace_month(Month::February) - .unwrap() - .replace_day(4) - .unwrap(),] - ); - } - - #[test] - fn test_parse_by_day_yearly_with_weekno() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 6).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "MO".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "6".to_string(), - }, - ], - &RepeatPeriod::ANNUALLY, - vec![], - Weekday::Monday, - true, - vec![], - vec![] - ), - [date] - ); - } - - #[test] - fn test_parse_by_day_yearly_with_unmatch_weekno() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "35".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "7".to_string(), - }, - ], - &RepeatPeriod::ANNUALLY, - vec![], - Weekday::Monday, - true, - vec![], - vec![] - ), - [] - ); - } - - #[test] - fn test_parse_by_day_yearly_with_invalid_rule() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventRecurrence {}; - // Can be WEEKDAY + WEEK - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2MO".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "6".to_string(), - }, - ], - &RepeatPeriod::ANNUALLY, - vec![], - Weekday::Monday, - true, - vec![], - vec![] - ), - [] - ); - } - - #[test] - fn test_flow_with_by_month_daily() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::March, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::DAILY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "3".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "6".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances( - date.replace_month(Month::January).unwrap(), - repeat_rule.clone() - ), - [] - ); - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date] - ); - assert_eq!( - event_recurrence.generate_future_instances( - date.replace_month(Month::February).unwrap(), - repeat_rule.clone() - ), - [date.replace_month(Month::February).unwrap()] - ); - assert_eq!( - event_recurrence.generate_future_instances( - date.replace_month(Month::June).unwrap(), - repeat_rule.clone() - ), - [date.replace_month(Month::June).unwrap()] - ); - } - - #[test] - fn test_flow_daily_with_by_month_and_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::DAILY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [] - ); - } - - #[test] - fn test_flow_daily_with_by_month_and_by_day_and_by_monthday() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 14).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::DAILY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "14".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date.replace_day(14).unwrap()] - ); - assert_eq!( - event_recurrence - .generate_future_instances(date.replace_day(13).unwrap(), repeat_rule.clone()), - [] - ); - } - - #[test] - fn test_flow_weekly_with_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::WEEKLY, - by_rules: vec![ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date.replace_day(10).unwrap(),] - ); - assert_eq!( - event_recurrence.generate_future_instances( - date.replace_month(Month::January).unwrap(), - repeat_rule.clone() - ), - [] - ); - } - - #[test] - fn test_flow_weekly_with_by_month_and_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::WEEKLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date.replace_day(13).unwrap(), date.replace_day(14).unwrap()] - ); - } - - #[test] - fn test_flow_weekly_with_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::WEEKLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date.replace_day(13).unwrap(), date.replace_day(14).unwrap()] - ); - } - - #[test] - fn test_flow_weekly_with_by_day_and_wkst() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::WEEKLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::WKST, - interval: "FR".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date.replace_day(14).unwrap(), date.replace_day(20).unwrap()] - ); - } - - #[test] - fn test_flow_monthly_with_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [ - date.replace_day(14).unwrap(), - date.replace_day(21).unwrap(), - date.replace_day(28).unwrap() - ] - ); - } - - #[test] - fn test_flow_monthly_with_second_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2FR".to_string(), - }], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date.replace_day(14).unwrap(),] - ); - } - - #[test] - fn test_flow_monthly_with_two_last_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "-1FR".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "-2FR".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date.replace_day(21).unwrap(), date.replace_day(28).unwrap(),] - ); - } - - #[test] - fn test_flow_monthly_with_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - let date_not_in_range = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::March, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date.replace_day(10).unwrap(),] - ); - assert_eq!( - event_recurrence.generate_future_instances(date_not_in_range, repeat_rule.clone()), - [] - ); - } - - #[test] - fn test_flow_monthly_with_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "25".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "28".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date.replace_day(25).unwrap(), date.replace_day(28).unwrap(),] - ); - } - - #[test] - fn test_flow_monthly_with_by_month_day_and_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "25".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "28".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date.replace_day(28).unwrap(),] - ); - } - - #[test] - fn test_flow_monthly_with_by_month_and_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [ - date.replace_day(13).unwrap(), - date.replace_day(14).unwrap(), - date.replace_day(20).unwrap(), - date.replace_day(21).unwrap(), - date.replace_day(27).unwrap(), - date.replace_day(28).unwrap() - ] - ); - } - - #[test] - fn test_flow_yearly_with_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let stop_condition = PrimitiveDateTime::new( - Date::from_calendar_date(2026, Month::February, 10).unwrap(), - time, - ); - let mut expected_dates: Vec = Vec::new(); - let mut current_date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 13).unwrap(), - time, - ); - - while current_date.assume_utc().unix_timestamp() - < stop_condition.assume_utc().unix_timestamp() - { - expected_dates.push(current_date); - expected_dates.push(current_date.add(Duration::days(1))); - - current_date = current_date.add(Duration::days(7)); - } - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::ANNUALLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - expected_dates - ); - } - - #[test] - fn test_flow_yearly_with_by_day_and_by_year_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::ANNUALLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "44".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date.replace_day(13).unwrap()] - ); - - assert_eq!( - event_recurrence.generate_future_instances( - date.replace_month(Month::March).unwrap(), - repeat_rule.clone() - ), - [] - ); - } - - #[test] - fn test_flow_yearly_with_by_week_no_and_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::ANNUALLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "8".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [date.replace_day(20).unwrap()] - ); - - assert_eq!( - event_recurrence.generate_future_instances( - date.replace_month(Month::March).unwrap(), - repeat_rule.clone() - ), - [date.replace_year(2026).unwrap().replace_day(19).unwrap()] - ); - } - - #[test] - fn test_flow_yearly_with_negative_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::December, 4).unwrap(), - time, - ); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2025, Month::December, 1).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new( - base_date.add(Duration::days(i)), - time, - )); - } - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::ANNUALLY, - by_rules: vec![ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "-5".to_string(), - }], - }; - - let event_recurrence = EventRecurrence {}; - - assert_eq!( - event_recurrence.generate_future_instances(valid_date, repeat_rule), - [] - ); - } - - #[test] - fn test_flow_yearly_with_by_week_no_and_wkst() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::ANNUALLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "8".to_string(), - }, - ByRule { - by_rule: ByRuleType::WKST, - interval: "TU".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [ - date.replace_day(18).unwrap(), - date.replace_day(19).unwrap(), - date.replace_day(20).unwrap(), - date.replace_day(21).unwrap(), - date.replace_day(22).unwrap(), - date.replace_day(23).unwrap(), - date.replace_day(24).unwrap(), - ] - ); - } - - #[test] - fn test_flow_yearly_with_by_month_and_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = RepeatRule { - frequency: RepeatPeriod::ANNUALLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventRecurrence {}; - assert_eq!( - event_recurrence.generate_future_instances(date, repeat_rule.clone()), - [ - date.replace_day(13).unwrap(), - date.replace_day(14).unwrap(), - date.replace_day(20).unwrap(), - date.replace_day(21).unwrap(), - date.replace_day(27).unwrap(), - date.replace_day(28).unwrap(), - date.replace_year(2026).unwrap().replace_day(5).unwrap(), - date.replace_year(2026).unwrap().replace_day(6).unwrap(), - ] - ); - } -} From c8623fe0812813b709227f50dda8acf3e79fb6d7 Mon Sep 17 00:00:00 2001 From: mup Date: Wed, 8 Jan 2025 13:09:46 +0100 Subject: [PATCH 21/29] [Android] Integrates SDK event expansion during alarm scheduling --- app-android/app/build.gradle | 4 +- .../alarms/AlarmNotificationsManager.kt | 6 +- .../java/de/tutao/tutanota/AlarmModelTest.kt | 16 +- .../tutanota/AlarmNotificationsManagerTest.kt | 20 +- app-android/calendar/build.gradle.kts | 4 +- .../alarms/AlarmNotificationsManager.kt | 5 +- .../calendar/alarms/SystemAlarmFacade.kt | 8 +- .../calendar/AlarmNotificationsManagerTest.kt | 20 +- .../5.json | 284 + .../de/tutao/tutashared/alarms/AlarmModel.kt | 82 +- .../alarms/AlarmNotificationEntity.kt | 2 + .../tutashared/alarms/EncryptedRepeatRule.kt | 71 +- .../de/tutao/tutashared/data/AppDatabase.kt | 5 +- packages/node-mimimi/Cargo.lock | 1 + .../native/main/NativePushServiceApp.ts | 2 +- tuta-sdk/rust/sdk/src/date/event_facade.rs | 5439 +++++++++-------- 16 files changed, 3227 insertions(+), 2742 deletions(-) create mode 100644 app-android/tutashared/schemas/de.tutao.tutashared.data.AppDatabase/5.json diff --git a/app-android/app/build.gradle b/app-android/app/build.gradle index c196d2c0856f..07b6ededdb08 100644 --- a/app-android/app/build.gradle +++ b/app-android/app/build.gradle @@ -83,8 +83,8 @@ android { buildTypes.each { it.buildConfigField 'String', 'FILE_PROVIDER_AUTHORITY', '"' + it.manifestPlaceholders['contentProviderAuthority'] + '"' // keep in sync with src/native/main/NativePushServiceApp.ts - it.buildConfigField 'String', "SYS_MODEL_VERSION", '"99"' - it.buildConfigField 'String', "TUTANOTA_MODEL_VERSION", '"73"' + it.buildConfigField 'String', "SYS_MODEL_VERSION", '"118"' + it.buildConfigField 'String', "TUTANOTA_MODEL_VERSION", '"80"' it.buildConfigField 'String', 'RES_ADDRESS', '"tutanota"' } diff --git a/app-android/app/src/main/java/de/tutao/tutanota/alarms/AlarmNotificationsManager.kt b/app-android/app/src/main/java/de/tutao/tutanota/alarms/AlarmNotificationsManager.kt index 7f61f96077a9..8e094b4163cc 100644 --- a/app-android/app/src/main/java/de/tutao/tutanota/alarms/AlarmNotificationsManager.kt +++ b/app-android/app/src/main/java/de/tutao/tutanota/alarms/AlarmNotificationsManager.kt @@ -3,6 +3,7 @@ package de.tutao.tutanota.alarms import android.util.Log import de.tutao.tutanota.* import de.tutao.tutanota.push.LocalNotificationsFacade +import de.tutao.tutasdk.ByRule import de.tutao.tutashared.AndroidNativeCryptoFacade import de.tutao.tutashared.CryptoError import de.tutao.tutashared.OperationType @@ -185,7 +186,6 @@ class AlarmNotificationsManager( alarmNotification: EncryptedAlarmNotification, pushKeyResolver: PushKeyResolver, ) { - // The DELETE notification we receive from the server has only placeholder fields and no keys. We must use our saved alarm to cancel notifications. val savedAlarmNotification = sseStorage.readAlarmNotifications().find { it.alarmInfo.identifier == alarmNotification.alarmInfo.identifier @@ -243,10 +243,12 @@ class AlarmNotificationsManager( val endValue = repeatRule.endValue val excludedDates = repeatRule.excludedDates val alarmTrigger: AlarmInterval = alarmNotification.alarmInfo.trigger + val byRules: List = alarmNotification.repeatRule?.advancedRules ?: listOf() + AlarmModel.iterateAlarmOccurrences( Date(), timeZone, eventStart, eventEnd, frequency, interval, endType, - endValue, alarmTrigger, TimeZone.getDefault(), excludedDates, callback + endValue, alarmTrigger, TimeZone.getDefault(), excludedDates, byRules, callback ) } diff --git a/app-android/app/src/test/java/de/tutao/tutanota/AlarmModelTest.kt b/app-android/app/src/test/java/de/tutao/tutanota/AlarmModelTest.kt index 360cbfcc8a9b..7984fdfde021 100644 --- a/app-android/app/src/test/java/de/tutao/tutanota/AlarmModelTest.kt +++ b/app-android/app/src/test/java/de/tutao/tutanota/AlarmModelTest.kt @@ -23,7 +23,7 @@ class AlarmModelTest { val eventStart = getDate(timeZone, 2019, 4, 2, 12, 0) iterateAlarmOccurrences( now, timeZone, eventStart, eventStart, RepeatPeriod.WEEKLY, - 1, EndType.NEVER, 0, AlarmInterval(AlarmIntervalUnit.HOUR, 1), timeZone, emptyList() + 1, EndType.NEVER, 0, AlarmInterval(AlarmIntervalUnit.HOUR, 1), timeZone, emptyList(), emptyList() ) { time: Date, _: Int, _: Date? -> occurrences.add(time) } Assert.assertArrayEquals( listOf( @@ -45,8 +45,18 @@ class AlarmModelTest { val eventEnd = getAllDayDateUTC(getDate(timeZone, 2019, 4, 3, 0, 0), timeZone) val repeatEnd = getAllDayDateUTC(getDate(timeZone, 2019, 4, 4, 0, 0), timeZone) iterateAlarmOccurrences( - now, repeatTimeZone, eventStart, eventEnd, RepeatPeriod.DAILY, - 1, EndType.UNTIL, repeatEnd.time, AlarmInterval(AlarmIntervalUnit.DAY, 1), timeZone, emptyList() + now, + repeatTimeZone, + eventStart, + eventEnd, + RepeatPeriod.DAILY, + 1, + EndType.UNTIL, + repeatEnd.time, + AlarmInterval(AlarmIntervalUnit.DAY, 1), + timeZone, + emptyList(), + emptyList() ) { time: Date, _: Int, _: Date? -> occurrences.add(time) } val expected = listOf( // Event on 2nd, alarm on 1st getDate(timeZone, 2019, 4, 1, 0, 0), // Event on 3rd, alarm on 2d diff --git a/app-android/app/src/test/java/de/tutao/tutanota/AlarmNotificationsManagerTest.kt b/app-android/app/src/test/java/de/tutao/tutanota/AlarmNotificationsManagerTest.kt index 019fdc996d8c..bd9b905139f1 100644 --- a/app-android/app/src/test/java/de/tutao/tutanota/AlarmNotificationsManagerTest.kt +++ b/app-android/app/src/test/java/de/tutao/tutanota/AlarmNotificationsManagerTest.kt @@ -77,7 +77,15 @@ class AlarmNotificationsManagerTest { val repeatingAlarmIdentifier = "repeatingAlarmIdentifier" val alarmNotification = createEncryptedAlarmNotification(userId, singleAlarmIdentifier, null, null) val repeatRule = - EncryptedRepeatRule("1", "1", "Europe/Berlin", EndType.COUNT.ordinal.toString(), "2", emptyList()) + EncryptedRepeatRule( + "1", + "1", + "Europe/Berlin", + EndType.COUNT.ordinal.toString(), + "2", + emptyList(), + emptyList() + ) val repeatingAlarmNotification = createEncryptedAlarmNotification( userId, repeatingAlarmIdentifier, null, repeatRule ) @@ -126,7 +134,15 @@ class AlarmNotificationsManagerTest { val identifier = "notTooFarR" val startDate = Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) val repeatRule = - EncryptedRepeatRule(RepeatPeriod.WEEKLY.value().toString(), "1", "Europe/Berlin", "0", "0", emptyList()) + EncryptedRepeatRule( + RepeatPeriod.WEEKLY.value().toString(), + "1", + "Europe/Berlin", + "0", + "0", + emptyList(), + emptyList() + ) val alarmNotification = createEncryptedAlarmNotification(userId, identifier, startDate, repeatRule) manager.scheduleNewAlarms(listOf(alarmNotification)) diff --git a/app-android/calendar/build.gradle.kts b/app-android/calendar/build.gradle.kts index 6007f4355c33..fd25022080e3 100644 --- a/app-android/calendar/build.gradle.kts +++ b/app-android/calendar/build.gradle.kts @@ -123,8 +123,8 @@ android { "\"" + it.manifestPlaceholders["contentProviderAuthority"] + "\"" ) // keep in sync with src/native/main/NativePushServiceApp.ts - it.buildConfigField("String", "SYS_MODEL_VERSION", "\"99\"") - it.buildConfigField("String", "TUTANOTA_MODEL_VERSION", "\"73\"") + it.buildConfigField("String", "SYS_MODEL_VERSION", "\"118\"") + it.buildConfigField("String", "TUTANOTA_MODEL_VERSION", "\"80\"") it.buildConfigField("String", "RES_ADDRESS", "\"tutanota\"") } diff --git a/app-android/calendar/src/main/java/de/tutao/calendar/alarms/AlarmNotificationsManager.kt b/app-android/calendar/src/main/java/de/tutao/calendar/alarms/AlarmNotificationsManager.kt index bc7716c88e46..243361e31879 100644 --- a/app-android/calendar/src/main/java/de/tutao/calendar/alarms/AlarmNotificationsManager.kt +++ b/app-android/calendar/src/main/java/de/tutao/calendar/alarms/AlarmNotificationsManager.kt @@ -3,6 +3,7 @@ package de.tutao.calendar.alarms import android.util.Log import de.tutao.calendar.* import de.tutao.calendar.push.LocalNotificationsFacade +import de.tutao.tutasdk.ByRule import de.tutao.tutashared.AndroidNativeCryptoFacade import de.tutao.tutashared.CryptoError import de.tutao.tutashared.OperationType @@ -229,10 +230,12 @@ class AlarmNotificationsManager( val endValue = repeatRule.endValue val excludedDates = repeatRule.excludedDates val alarmTrigger: AlarmInterval = alarmNotification.alarmInfo.trigger + val byRules: List = alarmNotification.repeatRule?.advancedRules ?: listOf() + AlarmModel.iterateAlarmOccurrences( Date(), timeZone, eventStart, eventEnd, frequency, interval, endType, - endValue, alarmTrigger, TimeZone.getDefault(), excludedDates, callback + endValue, alarmTrigger, TimeZone.getDefault(), excludedDates, byRules, callback ) } diff --git a/app-android/calendar/src/main/java/de/tutao/calendar/alarms/SystemAlarmFacade.kt b/app-android/calendar/src/main/java/de/tutao/calendar/alarms/SystemAlarmFacade.kt index af9fcff03b70..8d290bd56bc0 100644 --- a/app-android/calendar/src/main/java/de/tutao/calendar/alarms/SystemAlarmFacade.kt +++ b/app-android/calendar/src/main/java/de/tutao/calendar/alarms/SystemAlarmFacade.kt @@ -5,6 +5,7 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.util.Log +import de.tutao.calendar.BuildConfig import java.util.Date @@ -17,7 +18,12 @@ class SystemAlarmFacade(private val context: Context) { eventDate: Date, user: String ) { - Log.d(TAG, "Scheduled notification $identifier") + if (BuildConfig.DEBUG) { + Log.d(TAG, "Scheduled notification $identifier at $alarmTime") + } else { + Log.d(TAG, "Scheduled notification $identifier") + } + val alarmManager = alarmManager val pendingIntent = makeAlarmPendingIntent(occurrence, identifier, summary, eventDate, user) alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, alarmTime.time, pendingIntent) diff --git a/app-android/calendar/src/test/java/de/tutao/calendar/AlarmNotificationsManagerTest.kt b/app-android/calendar/src/test/java/de/tutao/calendar/AlarmNotificationsManagerTest.kt index 0744ba9352d3..e9a558990862 100644 --- a/app-android/calendar/src/test/java/de/tutao/calendar/AlarmNotificationsManagerTest.kt +++ b/app-android/calendar/src/test/java/de/tutao/calendar/AlarmNotificationsManagerTest.kt @@ -76,7 +76,15 @@ class AlarmNotificationsManagerTest { val repeatingAlarmIdentifier = "repeatingAlarmIdentifier" val alarmNotification = createEncryptedAlarmNotification(userId, singleAlarmIdentifier, null, null) val repeatRule = - EncryptedRepeatRule("1", "1", "Europe/Berlin", EndType.COUNT.ordinal.toString(), "2", emptyList()) + EncryptedRepeatRule( + "1", + "1", + "Europe/Berlin", + EndType.COUNT.ordinal.toString(), + "2", + emptyList(), + emptyList() + ) val repeatingAlarmNotification = createEncryptedAlarmNotification( userId, repeatingAlarmIdentifier, null, repeatRule ) @@ -125,7 +133,15 @@ class AlarmNotificationsManagerTest { val identifier = "notTooFarR" val startDate = Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) val repeatRule = - EncryptedRepeatRule(RepeatPeriod.WEEKLY.value().toString(), "1", "Europe/Berlin", "0", "0", emptyList()) + EncryptedRepeatRule( + RepeatPeriod.WEEKLY.value().toString(), + "1", + "Europe/Berlin", + "0", + "0", + emptyList(), + emptyList() + ) val alarmNotification = createEncryptedAlarmNotification(userId, identifier, startDate, repeatRule) manager.scheduleNewAlarms(listOf(alarmNotification)) diff --git a/app-android/tutashared/schemas/de.tutao.tutashared.data.AppDatabase/5.json b/app-android/tutashared/schemas/de.tutao.tutashared.data.AppDatabase/5.json new file mode 100644 index 000000000000..1be3da5a5a2f --- /dev/null +++ b/app-android/tutashared/schemas/de.tutao.tutashared.data.AppDatabase/5.json @@ -0,0 +1,284 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "82bad00d3c210935974bca9b30a584d1", + "entities": [ + { + "tableName": "KeyValue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "KeyBinary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` BLOB, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "PushIdentifierKey", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pushIdentifierId` TEXT NOT NULL, `deviceEncPushIdentifierKey` BLOB, PRIMARY KEY(`pushIdentifierId`))", + "fields": [ + { + "fieldPath": "pushIdentifierId", + "columnName": "pushIdentifierId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceEncPushIdentifierKey", + "columnName": "deviceEncPushIdentifierKey", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "pushIdentifierId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AlarmNotification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`operation` INTEGER, `summary` TEXT, `eventStart` TEXT, `eventEnd` TEXT, `user` TEXT, `trigger` TEXT NOT NULL, `identifier` TEXT NOT NULL, `frequency` TEXT, `interval` TEXT, `timeZone` TEXT, `endType` TEXT, `endValue` TEXT, `excludedDates` TEXT, `advancedRules` TEXT, `keypushIdentifierSessionEncSessionKey` TEXT, `keylistId` TEXT, `keyelementId` TEXT, PRIMARY KEY(`identifier`))", + "fields": [ + { + "fieldPath": "operation", + "columnName": "operation", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "summary", + "columnName": "summary", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eventStart", + "columnName": "eventStart", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eventEnd", + "columnName": "eventEnd", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "alarmInfo.trigger", + "columnName": "trigger", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alarmInfo.identifier", + "columnName": "identifier", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repeatRule.frequency", + "columnName": "frequency", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "repeatRule.interval", + "columnName": "interval", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "repeatRule.timeZone", + "columnName": "timeZone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "repeatRule.endType", + "columnName": "endType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "repeatRule.endValue", + "columnName": "endValue", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "repeatRule.excludedDates", + "columnName": "excludedDates", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "repeatRule.advancedRules", + "columnName": "advancedRules", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notificationSessionKey.pushIdentifierSessionEncSessionKey", + "columnName": "keypushIdentifierSessionEncSessionKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notificationSessionKey.pushIdentifier.listId", + "columnName": "keylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notificationSessionKey.pushIdentifier.elementId", + "columnName": "keyelementId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "identifier" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "PersistedCredentials", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`login` TEXT NOT NULL, `type` TEXT NOT NULL, `userId` TEXT NOT NULL, `encryptedPassword` TEXT NOT NULL, `databaseKey` BLOB, `accessToken` BLOB NOT NULL, `encryptedPassphraseKey` BLOB, PRIMARY KEY(`userId`))", + "fields": [ + { + "fieldPath": "login", + "columnName": "login", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedPassword", + "columnName": "encryptedPassword", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "databaseKey", + "columnName": "databaseKey", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "encryptedPassphraseKey", + "columnName": "encryptedPassphraseKey", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, PRIMARY KEY(`userId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '82bad00d3c210935974bca9b30a584d1')" + ] + } +} \ No newline at end of file diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmModel.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmModel.kt index a66682f318f0..2a0ec3e18a9d 100644 --- a/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmModel.kt +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmModel.kt @@ -1,6 +1,14 @@ package de.tutao.tutashared.alarms -import java.util.* +import de.tutao.tutasdk.ByRule +import de.tutao.tutasdk.ByRuleType +import de.tutao.tutasdk.DateTime +import de.tutao.tutasdk.EventFacade +import de.tutao.tutasdk.EventRepeatRule +import java.time.Instant +import java.util.Calendar +import java.util.Date +import java.util.TimeZone object AlarmModel { private const val OCCURRENCES_SCHEDULED_AHEAD = 10 @@ -18,7 +26,8 @@ object AlarmModel { alarmTrigger: AlarmInterval, localTimeZone: TimeZone, excludedDates: List, - callback: AlarmIterationCallback, + byRules: List, + callback: AlarmIterationCallback ) { val isAllDayEvent = isAllDayEventByTimes(eventStart, eventEnd) val calcEventStart = if (isAllDayEvent) { @@ -43,24 +52,70 @@ object AlarmModel { null } val calendar = Calendar.getInstance(if (isAllDayEvent) localTimeZone else timeZone) + val setPosRules = byRules.filter { rule -> rule.byRule == ByRuleType.BYSETPOS } + val eventFacade = EventFacade() + var occurrences = 0 var futureOccurrences = 0 + var intervalOccurrences = 0 + while ( futureOccurrences < OCCURRENCES_SCHEDULED_AHEAD && (endType != EndType.COUNT || occurrences < endValue!!) ) { calendar.time = calcEventStart - incrementByRepeatPeriod(calendar, frequency, interval * occurrences) + + incrementByRepeatPeriod(calendar, frequency, interval * intervalOccurrences) + + var expandedEvents: List = eventFacade.generateFutureInstances( + calendar.timeInMillis.toULong(), + EventRepeatRule(frequency.toSdkPeriod(), byRules) + ) + + // Add the progenitor if it isn't included in the expansion + if (intervalOccurrences == 0 && !expandedEvents.contains(calcEventStart.time.toULong())) { + expandedEvents = expandedEvents.plus(calcEventStart.time.toULong()) + } + + // This map + filter prevent an infinity loop trap by removing invalid rules like POS 320 for freq. weekly + // and ensures that 0 <= abs(SETPOS) < eventCount + val parsedSetPos = setPosRules.map { + if (it.interval.toInt() < 0) { + expandedEvents.count() - it.interval.toInt() + } else { + it.interval.toInt() - 1 + } + }.filter { it < expandedEvents.count() && it >= 0 } + if (endType == EndType.UNTIL && calendar.timeInMillis >= endDate!!.time) { break } - val alarmTime = calculateAlarmTime(calendar.time, localTimeZone, alarmTrigger) - val startTimeCalendar = calculateLocalStartTime(calendar.time, localTimeZone) - if (alarmTime.after(now) && calcExcludedDates.none { it.time == startTimeCalendar.time.time }) { - callback.call(alarmTime, occurrences, calendar.time) - futureOccurrences++ + + for (index in expandedEvents.indices) { + if (endValue != null && occurrences >= endValue) { + break + } + + if (parsedSetPos.isNotEmpty() && !parsedSetPos.contains(index)) { + continue + } + + val event = expandedEvents[index] + + val eventDate = Date.from(Instant.ofEpochSecond(event.toLong())) + + val alarmTime = calculateAlarmTime(eventDate, localTimeZone, alarmTrigger) + val startTimeCalendar = calculateLocalStartTime(eventDate, localTimeZone) + + if (alarmTime.after(now) && calcExcludedDates.none { it.time == startTimeCalendar.time.time }) { + callback.call(alarmTime, occurrences, eventDate) + futureOccurrences++ + } + + occurrences++ } - occurrences++ + + intervalOccurrences++ } } @@ -147,4 +202,13 @@ object AlarmModel { fun interface AlarmIterationCallback { fun call(alarmTime: Date, occurrence: Int, eventStartTime: Date) } + + private fun RepeatPeriod.toSdkPeriod() = run { + when (this) { + RepeatPeriod.DAILY -> de.tutao.tutasdk.RepeatPeriod.DAILY + RepeatPeriod.WEEKLY -> de.tutao.tutasdk.RepeatPeriod.WEEKLY + RepeatPeriod.MONTHLY -> de.tutao.tutasdk.RepeatPeriod.MONTHLY + RepeatPeriod.ANNUALLY -> de.tutao.tutasdk.RepeatPeriod.ANNUALLY + } + } } \ No newline at end of file diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmNotificationEntity.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmNotificationEntity.kt index 195cd82394ff..fa7a58d55320 100644 --- a/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmNotificationEntity.kt +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmNotificationEntity.kt @@ -4,6 +4,7 @@ import androidx.room.Embedded import androidx.room.Entity import androidx.room.TypeConverter import androidx.room.TypeConverters +import de.tutao.tutasdk.ByRule import de.tutao.tutashared.AndroidNativeCryptoFacade import de.tutao.tutashared.IdTuple import de.tutao.tutashared.OperationType @@ -21,6 +22,7 @@ class RepeatRule( val endValue: Long?, val endType: EndType, val excludedDates: List, + val advancedRules: List ) class AlarmInfo( diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/EncryptedRepeatRule.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/EncryptedRepeatRule.kt index 2d0e3cb99114..6f9ee31256c8 100644 --- a/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/EncryptedRepeatRule.kt +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/EncryptedRepeatRule.kt @@ -1,21 +1,33 @@ package de.tutao.tutashared.alarms +import android.util.Log import androidx.room.TypeConverter import androidx.room.TypeConverters +import de.tutao.tutasdk.ByRule +import de.tutao.tutasdk.ByRuleType import de.tutao.tutashared.AndroidNativeCryptoFacade import de.tutao.tutashared.decryptDate import de.tutao.tutashared.decryptNumber import de.tutao.tutashared.decryptString import kotlinx.serialization.Serializable -import java.util.* +import java.util.TimeZone @Serializable class EncryptedDateWrapper( - val date: String + val date: String ) @Serializable -@TypeConverters(EncryptedRepeatRule.ExcludedDateWrapperConverter::class) +class EncryptedByRuleWrapper( + val interval: String, + val ruleType: String +) + +@Serializable +@TypeConverters( + EncryptedRepeatRule.ExcludedDateWrapperConverter::class, + EncryptedRepeatRule.ByRuleWrapperConverter::class +) class EncryptedRepeatRule( val frequency: String, val interval: String, @@ -23,6 +35,7 @@ class EncryptedRepeatRule( val endType: String, val endValue: String?, val excludedDates: List, + val advancedRules: List ) { internal class ExcludedDateWrapperConverter { @TypeConverter @@ -30,7 +43,22 @@ class EncryptedRepeatRule( excludedDatesList.joinToString(",") { it.date } @TypeConverter - fun stringToList(string: String?) = if (string != null && string.isNotEmpty()) string.split(",").map { EncryptedDateWrapper(it) } else emptyList() + fun stringToList(string: String?) = if (string != null && string.isNotEmpty()) string.split(",") + .map { EncryptedDateWrapper(it) } else emptyList() + } + + internal class ByRuleWrapperConverter { + @TypeConverter + fun listToString(rules: List) = + rules.joinToString(";") { "${it.interval},${it.ruleType}" } + + @TypeConverter + fun stringToList(string: String?) = if (!string.isNullOrEmpty()) { + string.split(";").map { + val (interval, ruleType) = it.split(",") + EncryptedByRuleWrapper(interval, ruleType) + } + } else emptyList() } } @@ -41,11 +69,34 @@ fun EncryptedRepeatRule.decrypt(crypto: AndroidNativeCryptoFacade, sessionKey: B val endTypeNumber = crypto.decryptNumber(endType, sessionKey) val endType = EndType[endTypeNumber] return RepeatRule( - frequency = repeatPeriod, - interval = crypto.decryptNumber(interval, sessionKey).toInt(), - timeZone = TimeZone.getTimeZone(crypto.decryptString(timeZone, sessionKey)), - endValue = if (endValue != null) crypto.decryptNumber(endValue, sessionKey) else null, - endType = endType, - excludedDates = excludedDates.map { crypto.decryptDate(it.date, sessionKey) }, + frequency = repeatPeriod, + interval = crypto.decryptNumber(interval, sessionKey).toInt(), + timeZone = TimeZone.getTimeZone(crypto.decryptString(timeZone, sessionKey)), + endValue = if (endValue != null) crypto.decryptNumber(endValue, sessionKey) else null, + endType = endType, + excludedDates = excludedDates.map { crypto.decryptDate(it.date, sessionKey) }, + advancedRules = advancedRules.map { + val interval = crypto.decryptString(it.interval, sessionKey) + val rule = ByRuleType.fromValue(crypto.decryptNumber(it.ruleType, sessionKey).toInt()) + ByRule(rule, interval) + } ) +} + +fun ByRuleType.Companion.fromValue(value: Int) = run { + when (value) { + 0 -> ByRuleType.BYMINUTE + 1 -> ByRuleType.BYHOUR + 2 -> ByRuleType.BYDAY + 3 -> ByRuleType.BYMONTHDAY + 4 -> ByRuleType.BYYEARDAY + 5 -> ByRuleType.BYWEEKNO + 6 -> ByRuleType.BYMONTH + 7 -> ByRuleType.BYSETPOS + 8 -> ByRuleType.WKST + + else -> { + throw Exception("Invalid rule type") + } + } } \ No newline at end of file diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/data/AppDatabase.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/data/AppDatabase.kt index 4b2dd1fe2a2d..5bef62a4e686 100644 --- a/app-android/tutashared/src/main/java/de/tutao/tutashared/data/AppDatabase.kt +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/data/AppDatabase.kt @@ -10,7 +10,7 @@ import de.tutao.tutashared.credentials.CredentialsDao import de.tutao.tutashared.credentials.PersistedCredentialsEntity @Database( - version = 4, entities = [ + version = 5, entities = [ KeyValue::class, KeyBinary::class, PushIdentifierKey::class, @@ -20,7 +20,8 @@ import de.tutao.tutashared.credentials.PersistedCredentialsEntity ], autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), - AutoMigration(from = 3, to = 4) + AutoMigration(from = 3, to = 4), + AutoMigration(from = 4, to = 5) ] ) abstract class AppDatabase : RoomDatabase() { diff --git a/packages/node-mimimi/Cargo.lock b/packages/node-mimimi/Cargo.lock index 62f35127025b..1a06c1a43ec9 100644 --- a/packages/node-mimimi/Cargo.lock +++ b/packages/node-mimimi/Cargo.lock @@ -2173,6 +2173,7 @@ dependencies = [ "pqcrypto-mlkem", "pqcrypto-traits", "rand_core", + "regex", "rsa", "rustls", "serde", diff --git a/src/common/native/main/NativePushServiceApp.ts b/src/common/native/main/NativePushServiceApp.ts index 300ca2c4308b..1fe4113aa4a1 100644 --- a/src/common/native/main/NativePushServiceApp.ts +++ b/src/common/native/main/NativePushServiceApp.ts @@ -21,7 +21,7 @@ import { AppType } from "../../misc/ClientConstants.js" // keep in sync with SYS_MODEL_VERSION in app-android/app/build.gradle // keep in sync with SYS_MODEL_VERSION in app-android/calendar/build.gradle.kts // keep in sync with app-ios/TutanotaSharedFramework/Utils/Utils.swift -const MOBILE_SYS_MODEL_VERSION = 99 +const MOBILE_SYS_MODEL_VERSION = 118 function effectiveModelVersion(): number { // on desktop we use generated classes diff --git a/tuta-sdk/rust/sdk/src/date/event_facade.rs b/tuta-sdk/rust/sdk/src/date/event_facade.rs index a613a3bf226a..d902608dc03f 100644 --- a/tuta-sdk/rust/sdk/src/date/event_facade.rs +++ b/tuta-sdk/rust/sdk/src/date/event_facade.rs @@ -1,167 +1,165 @@ use std::ops::{Add, Sub}; use regex::Regex; -use time::{Date, Duration, Month, OffsetDateTime, PrimitiveDateTime, Weekday}; use time::util::weeks_in_year; +use time::{Date, Duration, Month, OffsetDateTime, PrimitiveDateTime, Weekday}; use crate::date::DateTime; #[derive(uniffi::Enum, PartialEq, Copy, Clone)] pub enum ByRuleType { - BYMINUTE, - BYHOUR, - BYDAY, - BYMONTHDAY, - BYYEARDAY, - BYWEEKNO, - BYMONTH, - BYSETPOS, - WKST, + BYMINUTE, + BYHOUR, + BYDAY, + BYMONTHDAY, + BYYEARDAY, + BYWEEKNO, + BYMONTH, + BYSETPOS, + WKST, } impl ByRuleType { - fn value(&self) -> &str { - match *self { - ByRuleType::BYMINUTE => "0", - ByRuleType::BYHOUR => "1", - ByRuleType::BYDAY => "2", - ByRuleType::BYMONTHDAY => "3", - ByRuleType::BYYEARDAY => "4", - ByRuleType::BYWEEKNO => "5", - ByRuleType::BYMONTH => "6", - ByRuleType::BYSETPOS => "7", - ByRuleType::WKST => "8", - } - } - - fn from_str(value: &str) -> ByRuleType { - match value { - "0" => ByRuleType::BYMINUTE, - "1" => ByRuleType::BYHOUR, - "2" => ByRuleType::BYDAY, - "3" => ByRuleType::BYMONTHDAY, - "4" => ByRuleType::BYYEARDAY, - "5" => ByRuleType::BYWEEKNO, - "6" => ByRuleType::BYMONTH, - "7" => ByRuleType::BYSETPOS, - "8" => ByRuleType::WKST, - _ => panic!("Invalid ByRule {value}"), - } - } + fn value(&self) -> &str { + match *self { + ByRuleType::BYMINUTE => "0", + ByRuleType::BYHOUR => "1", + ByRuleType::BYDAY => "2", + ByRuleType::BYMONTHDAY => "3", + ByRuleType::BYYEARDAY => "4", + ByRuleType::BYWEEKNO => "5", + ByRuleType::BYMONTH => "6", + ByRuleType::BYSETPOS => "7", + ByRuleType::WKST => "8", + } + } + + fn from_str(value: &str) -> ByRuleType { + match value { + "0" => ByRuleType::BYMINUTE, + "1" => ByRuleType::BYHOUR, + "2" => ByRuleType::BYDAY, + "3" => ByRuleType::BYMONTHDAY, + "4" => ByRuleType::BYYEARDAY, + "5" => ByRuleType::BYWEEKNO, + "6" => ByRuleType::BYMONTH, + "7" => ByRuleType::BYSETPOS, + "8" => ByRuleType::WKST, + _ => panic!("Invalid ByRule {value}"), + } + } } #[derive(uniffi::Enum, PartialEq, Copy, Clone)] pub enum RepeatPeriod { - DAILY, - WEEKLY, - MONTHLY, - ANNUALLY, + DAILY, + WEEKLY, + MONTHLY, + ANNUALLY, } impl RepeatPeriod { - fn value(&self) -> &str { - match *self { - RepeatPeriod::DAILY => "0", - RepeatPeriod::WEEKLY => "1", - RepeatPeriod::MONTHLY => "2", - RepeatPeriod::ANNUALLY => "3", - } - } - - fn from_str(value: &str) -> RepeatPeriod { - match value { - "0" => RepeatPeriod::DAILY, - "1" => RepeatPeriod::WEEKLY, - "2" => RepeatPeriod::MONTHLY, - "3" => RepeatPeriod::ANNUALLY, - _ => panic!("Invalid RepeatPeriod {value}"), - } - } + fn value(&self) -> &str { + match *self { + RepeatPeriod::DAILY => "0", + RepeatPeriod::WEEKLY => "1", + RepeatPeriod::MONTHLY => "2", + RepeatPeriod::ANNUALLY => "3", + } + } + + fn from_str(value: &str) -> RepeatPeriod { + match value { + "0" => RepeatPeriod::DAILY, + "1" => RepeatPeriod::WEEKLY, + "2" => RepeatPeriod::MONTHLY, + "3" => RepeatPeriod::ANNUALLY, + _ => panic!("Invalid RepeatPeriod {value}"), + } + } } -#[derive(Clone)] -#[derive(uniffi::Record)] +#[derive(Clone, uniffi::Record)] pub struct ByRule { - pub by_rule: ByRuleType, - pub interval: String, + pub by_rule: ByRuleType, + pub interval: String, } -#[derive(Clone)] -#[derive(uniffi::Record)] +#[derive(Clone, uniffi::Record)] pub struct EventRepeatRule { - pub frequency: RepeatPeriod, - pub by_rules: Vec, + pub frequency: RepeatPeriod, + pub by_rules: Vec, } trait MonthNumber { - fn to_number(&self) -> u8; - fn from_number(number: u8) -> Month; + fn to_number(&self) -> u8; + fn from_number(number: u8) -> Month; } trait WeekdayString { - fn from_short(short_weekday: &str) -> Weekday; + fn from_short(short_weekday: &str) -> Weekday; } impl MonthNumber for Month { - fn to_number(&self) -> u8 { - match *self { - Month::January => 1, - Month::February => 2, - Month::March => 3, - Month::April => 4, - Month::May => 5, - Month::June => 6, - Month::July => 7, - Month::August => 8, - Month::September => 9, - Month::October => 10, - Month::November => 11, - Month::December => 12, - } - } - - fn from_number(number: u8) -> Month { - match number { - 1 => Month::January, - 2 => Month::February, - 3 => Month::March, - 4 => Month::April, - 5 => Month::May, - 6 => Month::June, - 7 => Month::July, - 8 => Month::August, - 9 => Month::September, - 10 => Month::October, - 11 => Month::November, - 12 => Month::December, - _ => panic!("Invalid Month {number}"), - } - } + fn to_number(&self) -> u8 { + match *self { + Month::January => 1, + Month::February => 2, + Month::March => 3, + Month::April => 4, + Month::May => 5, + Month::June => 6, + Month::July => 7, + Month::August => 8, + Month::September => 9, + Month::October => 10, + Month::November => 11, + Month::December => 12, + } + } + + fn from_number(number: u8) -> Month { + match number { + 1 => Month::January, + 2 => Month::February, + 3 => Month::March, + 4 => Month::April, + 5 => Month::May, + 6 => Month::June, + 7 => Month::July, + 8 => Month::August, + 9 => Month::September, + 10 => Month::October, + 11 => Month::November, + 12 => Month::December, + _ => panic!("Invalid Month {number}"), + } + } } impl WeekdayString for Weekday { - fn from_short(short_weekday: &str) -> Weekday { - match short_weekday { - "MO" => Weekday::Monday, - "TU" => Weekday::Tuesday, - "WE" => Weekday::Wednesday, - "TH" => Weekday::Thursday, - "FR" => Weekday::Friday, - "SA" => Weekday::Saturday, - "SU" => Weekday::Sunday, - _ => panic!("Invalid Weekday {short_weekday}"), - } - } + fn from_short(short_weekday: &str) -> Weekday { + match short_weekday { + "MO" => Weekday::Monday, + "TU" => Weekday::Tuesday, + "WE" => Weekday::Wednesday, + "TH" => Weekday::Thursday, + "FR" => Weekday::Friday, + "SA" => Weekday::Saturday, + "SU" => Weekday::Sunday, + _ => panic!("Invalid Weekday {short_weekday}"), + } + } } trait DateExpansion { - fn add_month(&self) -> Date; + fn add_month(&self) -> Date; } impl DateExpansion for Date { - fn add_month(&self) -> Date { - self.add(Duration::days(i64::from(self.month().length(self.year())))) - } + fn add_month(&self) -> Date { + self.add(Duration::days(i64::from(self.month().length(self.year())))) + } } #[derive(uniffi::Object)] @@ -169,2595 +167,2626 @@ pub struct EventFacade; #[uniffi::export] impl EventFacade { - #[uniffi::constructor] - pub fn new() -> Self { - EventFacade {} - } - - pub fn generate_future_instances( - &self, - date: DateTime, - repeat_rule: EventRepeatRule, - ) -> Vec { - let Ok(parsed_date) = OffsetDateTime::from_unix_timestamp(date.as_millis() as i64) else { - // FIXME: fix paul's code :( - return Vec::new(); - }; - - let date = PrimitiveDateTime::new(parsed_date.date(), parsed_date.time()); - - let by_month_rules: Vec<&ByRule> = repeat_rule - .by_rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYMONTH) - .collect(); - let by_day_rules: Vec<&ByRule> = repeat_rule - .by_rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYDAY) - .collect(); - let by_month_day_rules: Vec<&ByRule> = repeat_rule - .by_rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) - .collect(); - let by_year_day_rules: Vec<&ByRule> = repeat_rule - .by_rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYYEARDAY) - .collect(); - let by_week_no_rules: Vec<&ByRule> = repeat_rule - .by_rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYWEEKNO) - .collect(); - - let week_start: Weekday; - - if repeat_rule.frequency == RepeatPeriod::ANNUALLY - || repeat_rule.frequency == RepeatPeriod::WEEKLY - { - week_start = match repeat_rule - .by_rules - .iter() - .find(|&x| x.by_rule == ByRuleType::WKST) - { - Some(rule) => match rule.interval.as_str() { - "MO" => Weekday::Monday, - "TU" => Weekday::Tuesday, - "WE" => Weekday::Wednesday, - "TH" => Weekday::Thursday, - "FR" => Weekday::Friday, - "SA" => Weekday::Saturday, - "SU" => Weekday::Sunday, - _ => Weekday::Monday, - }, - None => Weekday::Monday, - }; - } else { - week_start = Weekday::Monday - } - - let valid_months: Vec = by_month_rules - .iter() - .clone() - .map(|&x| x.interval.parse::().unwrap()) - .collect(); - let valid_month_days: Vec = by_month_day_rules - .iter() - .clone() - .map(|&x| x.interval.parse::().unwrap()) - .collect(); - let valid_year_days: Vec = by_year_day_rules - .iter() - .clone() - .map(|&x| x.interval.parse::().unwrap()) - .collect(); - - let month_applied_events: Vec = - self.apply_month_rules(&vec![date], &by_month_rules, &repeat_rule.frequency); - - let week_no_applied_events: Vec = - if repeat_rule.frequency == RepeatPeriod::ANNUALLY { - self.apply_week_no_rules(month_applied_events, &by_week_no_rules, week_start) - } else { - month_applied_events - }; - - let year_day_applied_events: Vec = - if repeat_rule.frequency == RepeatPeriod::ANNUALLY { - self.apply_year_day_rules( - week_no_applied_events, - &by_year_day_rules, - by_week_no_rules.len() > 0, - by_month_rules.len() > 0, - ) - } else { - week_no_applied_events - }; - - let month_day_applied_events: Vec = self.apply_month_day_rules( - year_day_applied_events, - &by_month_day_rules, - &repeat_rule.frequency == &RepeatPeriod::DAILY, - ); - let day_applied_events: Vec = self.apply_day_rules( - month_day_applied_events, - &by_day_rules, - &repeat_rule.frequency, - valid_months.clone(), - week_start, - by_week_no_rules.len() > 0, - valid_month_days, - valid_year_days, - ); - - let date_timestamp = date.assume_utc().unix_timestamp(); - self.finish_rules( - day_applied_events, - valid_months.clone(), - Some(date_timestamp), - ).iter().map(|date| { DateTime::from_millis(date.assume_utc().unix_timestamp() as u64) }).collect() - } + #[uniffi::constructor] + pub fn new() -> Self { + EventFacade {} + } + + pub fn generate_future_instances( + &self, + date: DateTime, + repeat_rule: EventRepeatRule, + ) -> Vec { + let parsed_date: OffsetDateTime = date.as_system_time().into(); + let date = PrimitiveDateTime::new(parsed_date.date(), parsed_date.time()); + + let by_month_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYMONTH) + .collect(); + let by_day_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYDAY) + .collect(); + let by_month_day_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) + .collect(); + let by_year_day_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYYEARDAY) + .collect(); + let by_week_no_rules: Vec<&ByRule> = repeat_rule + .by_rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYWEEKNO) + .collect(); + + let week_start: Weekday; + + if repeat_rule.frequency == RepeatPeriod::ANNUALLY + || repeat_rule.frequency == RepeatPeriod::WEEKLY + { + week_start = match repeat_rule + .by_rules + .iter() + .find(|&x| x.by_rule == ByRuleType::WKST) + { + Some(rule) => match rule.interval.as_str() { + "MO" => Weekday::Monday, + "TU" => Weekday::Tuesday, + "WE" => Weekday::Wednesday, + "TH" => Weekday::Thursday, + "FR" => Weekday::Friday, + "SA" => Weekday::Saturday, + "SU" => Weekday::Sunday, + _ => Weekday::Monday, + }, + None => Weekday::Monday, + }; + } else { + week_start = Weekday::Monday + } + + let valid_months: Vec = by_month_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + let valid_month_days: Vec = by_month_day_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + let valid_year_days: Vec = by_year_day_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + + let month_applied_events: Vec = + self.apply_month_rules(&vec![date], &by_month_rules, &repeat_rule.frequency); + + let week_no_applied_events: Vec = + if repeat_rule.frequency == RepeatPeriod::ANNUALLY { + self.apply_week_no_rules(month_applied_events, &by_week_no_rules, week_start) + } else { + month_applied_events + }; + + let year_day_applied_events: Vec = + if repeat_rule.frequency == RepeatPeriod::ANNUALLY { + self.apply_year_day_rules( + week_no_applied_events, + &by_year_day_rules, + by_week_no_rules.len() > 0, + by_month_rules.len() > 0, + ) + } else { + week_no_applied_events + }; + + let month_day_applied_events: Vec = self.apply_month_day_rules( + year_day_applied_events, + &by_month_day_rules, + &repeat_rule.frequency == &RepeatPeriod::DAILY, + ); + let day_applied_events: Vec = self.apply_day_rules( + month_day_applied_events, + &by_day_rules, + &repeat_rule.frequency, + valid_months.clone(), + week_start, + by_week_no_rules.len() > 0, + valid_month_days, + valid_year_days, + ); + + let date_timestamp = date.assume_utc().unix_timestamp(); + self.finish_rules( + day_applied_events, + valid_months.clone(), + Some(date_timestamp), + ) + .iter() + .map(|date| DateTime::from_millis(date.assume_utc().unix_timestamp() as u64)) + .collect() + } } impl EventFacade { - fn apply_month_rules( - &self, - dates: &Vec, - rules: &Vec<&ByRule>, - frequency: &RepeatPeriod, - ) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - - for &rule in rules { - for date in dates { - let target_month: u8 = match rule.interval.parse::() { - Ok(month) => month, - _ => continue, - }; - - if frequency == &RepeatPeriod::WEEKLY { - let week_start = PrimitiveDateTime::new( - Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Monday) - .unwrap(), - date.time(), - ); - let week_end = PrimitiveDateTime::new( - Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Sunday) - .unwrap(), - date.time(), - ); - - let week_start_year = week_start.year(); - let week_end_year = week_end.year(); - - let week_start_month = week_start.month().to_number(); - let week_end_month = week_end.month().to_number(); - - let is_target_month = - week_end_month == target_month || week_start_month == target_month; - - if week_start_year == week_end_year - && week_start_month < week_end_month - && is_target_month - { - new_dates.push(date.clone()); - continue; - } else if week_start_year < week_end_year && is_target_month { - new_dates.push(date.clone()); - continue; - } - } else if frequency == &RepeatPeriod::ANNUALLY { - let new_date = - match date.clone().replace_month(Month::from_number(target_month)) { - Ok(dt) => dt, - _ => continue, - }; - - let years_to_add = if date.year() == new_date.year() - && date.month().to_number() > target_month - { - 1 - } else { - 0 - }; - - new_dates.push( - match new_date.replace_year(new_date.year() + years_to_add) { - Ok(date) => date, - _ => continue, - }, - ); - - continue; - } - - if date.month().to_number() == target_month { - new_dates.push(date.clone()); - } - } - } - - new_dates - } - - fn apply_week_no_rules( - &self, - dates: Vec, - rules: &Vec<&ByRule>, - week_start: Weekday, - ) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - - for &rule in rules { - for date in &dates { - let parsed_week: i8 = match rule.interval.parse::() { - Ok(week) => week, - _ => continue, - }; - - let mut new_date = date.clone(); - - let total_weeks = weeks_in_year(date.year()); - - let week_number = if parsed_week < 0 { - total_weeks - parsed_week.unsigned_abs() + 1 - } else { - new_date = new_date.replace_date( - Date::from_iso_week_date( - new_date.year(), - parsed_week as u8, - new_date.weekday(), - ) - .unwrap(), - ); - parsed_week as u8 - }; - - let year_offset = if new_date.assume_utc().unix_timestamp() - < date.assume_utc().unix_timestamp() - { - date.year() - new_date.year() + 1 - } else { - 0 - }; - let year = new_date.year() + year_offset; - new_date = new_date - .replace_date(Date::from_iso_week_date(year, week_number, week_start).unwrap()); - - for i in 0..7 { - let final_date = new_date.add(Duration::days(i)); - if final_date.year() > new_date.year() { - break; - } - - new_dates.push(final_date) - } - } - } - - new_dates - } - - fn apply_year_day_rules( - &self, - dates: Vec, - rules: &Vec<&ByRule>, - evaluate_same_week: bool, - evaluate_same_month: bool, - ) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - - for &rule in rules { - for date in &dates { - let parsed_day: i64 = match rule.interval.parse::() { - Ok(day) => day, - _ => continue, - }; - - let mut new_date: PrimitiveDateTime; - if parsed_day.is_negative() { - new_date = date - .replace_month(Month::December) - .unwrap() - .replace_day(31) - .unwrap() - .sub(Duration::days((parsed_day.unsigned_abs() - 1) as i64)); - } else { - new_date = date - .replace_month(Month::January) - .unwrap() - .replace_day(1) - .unwrap() - .add(Duration::days(parsed_day - 1)); - } - - let year_offset = if new_date.assume_utc().unix_timestamp() - < date.assume_utc().unix_timestamp() - { - 1 - } else { - 0 - }; - new_date = match new_date.replace_year(new_date.year() + year_offset) { - Ok(date) => date, - _ => continue, - }; - - if (evaluate_same_week && date.iso_week() != new_date.iso_week()) - || (evaluate_same_month && date.month() != new_date.month()) - { - continue; - } - - new_dates.push(new_date) - } - } - - new_dates - } - - fn apply_month_day_rules( - &self, - dates: Vec, - rules: &Vec<&ByRule>, - is_daily_event: bool, - ) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - - for &rule in rules { - for date in &dates { - let target_day: i8 = match rule.interval.parse::() { - Ok(day) => day, - _ => continue, - }; - let days_diff = - date.month().length(date.year()) as i8 - target_day.unsigned_abs() as i8 + 1; - - if is_daily_event { - if target_day.is_positive() && date.day() == target_day.unsigned_abs() { - new_dates.push(date.clone()); - } else if target_day.is_negative() && days_diff == date.day() as i8 { - new_dates.push(date.clone()); - } - - continue; - } - - if target_day >= 0 && target_day.unsigned_abs() <= date.month().length(date.year()) - { - let date = match date.replace_day(target_day.unsigned_abs()) { - Ok(date) => date, - _ => continue, - }; - - new_dates.push(date); - } else if days_diff > 0 - && target_day.unsigned_abs() <= date.month().length(date.year()) - { - let date = match date.replace_day(days_diff.unsigned_abs()) { - Ok(date) => date, - _ => continue, - }; - - new_dates.push(date); - } - } - } - - new_dates - } - - fn apply_day_rules( - &self, - dates: Vec, - rules: &Vec<&ByRule>, - frequency: &RepeatPeriod, - valid_months: Vec, - week_start: Weekday, - has_week_no: bool, - valid_month_days: Vec, - valid_year_days: Vec, - ) -> Vec { - if rules.len() == 0 { - return dates.clone(); - } - - let mut new_dates: Vec = Vec::new(); - let regex = Regex::new(r"^([-+]?\d{0,3})([a-zA-Z]{2})?$").unwrap(); - - for &rule in rules { - for date in &dates { - let Some(parsed_rule) = regex.captures(rule.interval.as_str()) else { - continue; - }; - let target_week_day = parsed_rule.get(2); - let leading_value = parsed_rule.get(1); - - if frequency == &RepeatPeriod::DAILY - && target_week_day.is_some() - && date.weekday() == Weekday::from_short(target_week_day.unwrap().as_str()) - { - new_dates.push(date.clone()) - } else if frequency == &RepeatPeriod::WEEKLY && target_week_day.is_some() { - let mut new_date = date.replace_date( - Date::from_iso_week_date( - date.year(), - date.iso_week(), - Weekday::from_short(target_week_day.unwrap().as_str()), - ) - .unwrap(), - ); - let interval_start = date.replace_date( - Date::from_iso_week_date(date.year(), date.iso_week(), week_start).unwrap(), - ); - - if new_date.assume_utc().unix_timestamp() - > interval_start - .add(Duration::weeks(1)) - .assume_utc() - .unix_timestamp() - { - continue; - } else if new_date.assume_utc().unix_timestamp() - < interval_start.assume_utc().unix_timestamp() - { - new_date = new_date.add(Duration::weeks(1)); - } - - if valid_months.len() == 0 - || valid_months.contains(&new_date.month().to_number()) - { - new_dates.push(new_date) - } - } else if frequency == &RepeatPeriod::MONTHLY && target_week_day.is_some() { - let mut allowed_days: Vec = Vec::new(); - - let week_change = - match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { - Ok(val) => val, - _ => 0, - }; - - let base_date = date.replace_day(1).unwrap(); - let stop_condition = - PrimitiveDateTime::new(base_date.date().add_month(), base_date.time()); - - for allowed_day in &valid_month_days { - if allowed_day.is_positive() { - allowed_days.push(allowed_day.unsigned_abs()); - continue; - } - - let day = - base_date.month().length(date.year()) - allowed_day.unsigned_abs() + 1; - allowed_days.push(day); - } - - let is_allowed_in_month_day = |day: u8| -> bool { - if allowed_days.len() == 0 { - return true; - } - - allowed_days.contains(&day) - }; - - let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); - - if week_change != 0 { - let mut new_date = base_date; - if week_change.is_negative() { - new_date = new_date - .replace_day(new_date.month().length(new_date.year())) - .unwrap(); - new_date = new_date.replace_date( - Date::from_iso_week_date( - new_date.year(), - new_date.iso_week(), - parsed_weekday, - ) - .unwrap(), - ); - - let new_week = new_date.iso_week() - week_change.unsigned_abs() + 1; - new_date = new_date.replace_date( - Date::from_iso_week_date( - new_date.year(), - new_week, - new_date.weekday(), - ) - .unwrap(), - ) - } else { - while new_date.weekday() != parsed_weekday { - new_date = new_date.add(Duration::days(1)); - } - - new_date = new_date.replace_date( - Date::from_iso_week_date( - new_date.year(), - new_date.iso_week() + week_change.unsigned_abs() - 1, - new_date.weekday(), - ) - .unwrap(), - ) - } - - if new_date.assume_utc().unix_timestamp() - >= base_date.assume_utc().unix_timestamp() - && new_date.assume_utc().unix_timestamp() - <= stop_condition.assume_utc().unix_timestamp() - && is_allowed_in_month_day(new_date.day()) - { - new_dates.push(new_date) - } - } else { - let mut current_date = base_date; - while current_date.assume_utc().unix_timestamp() - < stop_condition.assume_utc().unix_timestamp() - { - let new_date = current_date.replace_date( - Date::from_iso_week_date( - current_date.year(), - current_date.iso_week(), - parsed_weekday, - ) - .unwrap(), - ); - if new_date.assume_utc().unix_timestamp() - >= base_date.assume_utc().unix_timestamp() - && is_allowed_in_month_day(new_date.day()) - { - if valid_months.len() > 0 - && valid_months.contains(&new_date.month().to_number()) - { - new_dates.push(new_date) - } else if valid_months.len() == 0 { - new_dates.push(new_date) - } - } - - current_date = new_date.add(Duration::days(7)); - } - } - } else if frequency == &RepeatPeriod::ANNUALLY { - let week_change = - match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { - Ok(val) => val, - _ => 0, - }; - - if has_week_no && week_change != 0 { - println!( - "Invalid repeat rule, can't use BYWEEKNO with Week Offset on BYDAY" - ); - continue; - } - - if week_change != 0 && !has_week_no { - let mut new_date: PrimitiveDateTime; - - if !target_week_day.is_some() { - if week_change > 0 { - new_date = date - .replace_day(1) - .unwrap() - .replace_month(Month::January) - .unwrap() - .add(Duration::days(week_change - 1)) - } else { - new_date = date - .replace_month(Month::December) - .unwrap() - .replace_day(31) - .unwrap() - .sub(Duration::days(week_change.abs() - 1)) - } - } else { - let parsed_weekday = - Weekday::from_short(target_week_day.unwrap().as_str()); - - if week_change > 0 { - new_date = date - .replace_day(1) - .unwrap() - .replace_month(Month::January) - .unwrap() - .add(Duration::weeks(week_change - 1)); - - while new_date.weekday() != parsed_weekday { - new_date = new_date.add(Duration::days(1)); - } - } else { - new_date = date - .replace_month(Month::December) - .unwrap() - .replace_day(31) - .unwrap() - .sub(Duration::weeks(week_change.abs() - 1)); - while new_date.weekday() != parsed_weekday { - new_date = new_date.sub(Duration::days(1)); - } - } - } - - if new_date.assume_utc().unix_timestamp() - < date.assume_utc().unix_timestamp() - { - match new_date.replace_year(new_date.year() + 1) { - Ok(dt) => new_dates.push(dt), - _ => continue, - } - } else { - new_dates.push(new_date) - } - } else if has_week_no { - if !target_week_day.is_some() { - continue; - } - - let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); - let new_date = date.replace_date( - Date::from_iso_week_date(date.year(), date.iso_week(), parsed_weekday) - .unwrap(), - ); - - let interval_start = date.replace_date( - Date::from_iso_week_date(date.year(), date.iso_week(), week_start) - .unwrap(), - ); - let week_ahead = interval_start.add(Duration::days(7)); - - if new_date.assume_utc().unix_timestamp() - > week_ahead.assume_utc().unix_timestamp() - || new_date.assume_utc().unix_timestamp() - < date.assume_utc().unix_timestamp() - {} else if new_date.assume_utc().unix_timestamp() - < interval_start.assume_utc().unix_timestamp() - { - new_dates.push(interval_start.add(Duration::days(7))); - } else { - new_dates.push(new_date); - } - } else { - if !target_week_day.is_some() { - continue; - } - - let day_one = date.replace_day(1).unwrap(); - let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); - - let stop_date = match Date::from_calendar_date( - date.year() + 1, - date.month(), - date.day(), - ) { - Ok(date) => date, - _ => continue, - }; - - let stop_condition = date.replace_date(stop_date); - let mut current_date = date.replace_date( - Date::from_iso_week_date( - date.year(), - day_one.iso_week(), - parsed_weekday, - ) - .unwrap(), - ); - - if current_date.assume_utc().unix_timestamp() - >= day_one.assume_utc().unix_timestamp() - { - new_dates.push(current_date); - } - - current_date = current_date.add(Duration::days(7)); - - while current_date.assume_utc().unix_timestamp() - < stop_condition.assume_utc().unix_timestamp() - { - new_dates.push(current_date); - current_date = current_date.add(Duration::days(7)); - } - } - } - } - } - - if frequency == &RepeatPeriod::ANNUALLY { - return new_dates - .iter() - .filter(|date| self.is_valid_day_in_year(**date, valid_year_days.clone())) - .map(|date| *date) - .collect(); - } - - new_dates - } - - fn get_valid_days_in_year(&self, year: i32, valid_year_days: &Vec) -> Vec { - let days_in_year = Date::from_calendar_date(year, Month::December, 31) - .unwrap() - .ordinal(); - let mut allowed_days: Vec = Vec::new(); - - for allowed_day in valid_year_days { - if allowed_day > &0 { - allowed_days.push(allowed_day.abs() as u16); - continue; - } - - let day = days_in_year - allowed_day.unsigned_abs() + 1; - allowed_days.push(day); - } - - allowed_days - } - - fn is_valid_day_in_year(&self, date: PrimitiveDateTime, valid_year_days: Vec) -> bool { - let valid_days = self.get_valid_days_in_year(date.year(), &valid_year_days); - - if valid_days.len() == 0 { - return true; - } - - let day_in_year = date.ordinal(); - - let is_valid = valid_days.contains(&day_in_year); - - return is_valid; - } - - fn finish_rules( - &self, - dates: Vec, - valid_months: Vec, - event_start_time: Option, - ) -> Vec { - let mut clean_dates; - - if valid_months.len() > 0 { - clean_dates = dates - .iter() - .filter(|date| valid_months.contains(&date.month().to_number())) - .map(|date| *date) - .collect(); - } else { - clean_dates = dates - }; - - if event_start_time.is_some() { - clean_dates = clean_dates - .iter() - .filter(|date| { - let date_unix_timestamp = date.assume_utc().unix_timestamp(); - date_unix_timestamp >= event_start_time.unwrap() - }) - .map(|date| *date) - .collect(); - } - - clean_dates.sort_by(|a, b| { - a.assume_utc() - .unix_timestamp() - .cmp(&b.assume_utc().unix_timestamp()) - }); - clean_dates.dedup(); - - clean_dates - } + fn apply_month_rules( + &self, + dates: &Vec, + rules: &Vec<&ByRule>, + frequency: &RepeatPeriod, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in dates { + let target_month: u8 = match rule.interval.parse::() { + Ok(month) => month, + _ => continue, + }; + + if frequency == &RepeatPeriod::WEEKLY { + let week_start = PrimitiveDateTime::new( + Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Monday) + .unwrap(), + date.time(), + ); + let week_end = PrimitiveDateTime::new( + Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Sunday) + .unwrap(), + date.time(), + ); + + let week_start_year = week_start.year(); + let week_end_year = week_end.year(); + + let week_start_month = week_start.month().to_number(); + let week_end_month = week_end.month().to_number(); + + let is_target_month = + week_end_month == target_month || week_start_month == target_month; + + if week_start_year == week_end_year + && week_start_month < week_end_month + && is_target_month + { + new_dates.push(date.clone()); + continue; + } else if week_start_year < week_end_year && is_target_month { + new_dates.push(date.clone()); + continue; + } + } else if frequency == &RepeatPeriod::ANNUALLY { + let new_date = + match date.clone().replace_month(Month::from_number(target_month)) { + Ok(dt) => dt, + _ => continue, + }; + + let years_to_add = if date.year() == new_date.year() + && date.month().to_number() > target_month + { + 1 + } else { + 0 + }; + + new_dates.push( + match new_date.replace_year(new_date.year() + years_to_add) { + Ok(date) => date, + _ => continue, + }, + ); + + continue; + } + + if date.month().to_number() == target_month { + new_dates.push(date.clone()); + } + } + } + + new_dates + } + + fn apply_week_no_rules( + &self, + dates: Vec, + rules: &Vec<&ByRule>, + week_start: Weekday, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in &dates { + let parsed_week: i8 = match rule.interval.parse::() { + Ok(week) => week, + _ => continue, + }; + + let mut new_date = date.clone(); + + let total_weeks = weeks_in_year(date.year()); + + let week_number = if parsed_week < 0 { + total_weeks - parsed_week.unsigned_abs() + 1 + } else { + new_date = new_date.replace_date( + Date::from_iso_week_date( + new_date.year(), + parsed_week as u8, + new_date.weekday(), + ) + .unwrap(), + ); + parsed_week as u8 + }; + + let year_offset = if new_date.assume_utc().unix_timestamp() + < date.assume_utc().unix_timestamp() + { + date.year() - new_date.year() + 1 + } else { + 0 + }; + let year = new_date.year() + year_offset; + new_date = new_date + .replace_date(Date::from_iso_week_date(year, week_number, week_start).unwrap()); + + for i in 0..7 { + let final_date = new_date.add(Duration::days(i)); + if final_date.year() > new_date.year() { + break; + } + + new_dates.push(final_date) + } + } + } + + new_dates + } + + fn apply_year_day_rules( + &self, + dates: Vec, + rules: &Vec<&ByRule>, + evaluate_same_week: bool, + evaluate_same_month: bool, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in &dates { + let parsed_day: i64 = match rule.interval.parse::() { + Ok(day) => day, + _ => continue, + }; + + let mut new_date: PrimitiveDateTime; + if parsed_day.is_negative() { + new_date = date + .replace_month(Month::December) + .unwrap() + .replace_day(31) + .unwrap() + .sub(Duration::days((parsed_day.unsigned_abs() - 1) as i64)); + } else { + new_date = date + .replace_month(Month::January) + .unwrap() + .replace_day(1) + .unwrap() + .add(Duration::days(parsed_day - 1)); + } + + let year_offset = if new_date.assume_utc().unix_timestamp() + < date.assume_utc().unix_timestamp() + { + 1 + } else { + 0 + }; + new_date = match new_date.replace_year(new_date.year() + year_offset) { + Ok(date) => date, + _ => continue, + }; + + if (evaluate_same_week && date.iso_week() != new_date.iso_week()) + || (evaluate_same_month && date.month() != new_date.month()) + { + continue; + } + + new_dates.push(new_date) + } + } + + new_dates + } + + fn apply_month_day_rules( + &self, + dates: Vec, + rules: &Vec<&ByRule>, + is_daily_event: bool, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + + for &rule in rules { + for date in &dates { + let target_day: i8 = match rule.interval.parse::() { + Ok(day) => day, + _ => continue, + }; + let days_diff = + date.month().length(date.year()) as i8 - target_day.unsigned_abs() as i8 + 1; + + if is_daily_event { + if target_day.is_positive() && date.day() == target_day.unsigned_abs() { + new_dates.push(date.clone()); + } else if target_day.is_negative() && days_diff == date.day() as i8 { + new_dates.push(date.clone()); + } + + continue; + } + + if target_day >= 0 && target_day.unsigned_abs() <= date.month().length(date.year()) + { + let date = match date.replace_day(target_day.unsigned_abs()) { + Ok(date) => date, + _ => continue, + }; + + new_dates.push(date); + } else if days_diff > 0 + && target_day.unsigned_abs() <= date.month().length(date.year()) + { + let date = match date.replace_day(days_diff.unsigned_abs()) { + Ok(date) => date, + _ => continue, + }; + + new_dates.push(date); + } + } + } + + new_dates + } + + fn apply_day_rules( + &self, + dates: Vec, + rules: &Vec<&ByRule>, + frequency: &RepeatPeriod, + valid_months: Vec, + week_start: Weekday, + has_week_no: bool, + valid_month_days: Vec, + valid_year_days: Vec, + ) -> Vec { + if rules.len() == 0 { + return dates.clone(); + } + + let mut new_dates: Vec = Vec::new(); + let regex = Regex::new(r"^([-+]?\d{0,3})([a-zA-Z]{2})?$").unwrap(); + + for &rule in rules { + for date in &dates { + let Some(parsed_rule) = regex.captures(rule.interval.as_str()) else { + continue; + }; + let target_week_day = parsed_rule.get(2); + let leading_value = parsed_rule.get(1); + + if frequency == &RepeatPeriod::DAILY + && target_week_day.is_some() + && date.weekday() == Weekday::from_short(target_week_day.unwrap().as_str()) + { + new_dates.push(date.clone()) + } else if frequency == &RepeatPeriod::WEEKLY && target_week_day.is_some() { + let mut new_date = date.replace_date( + Date::from_iso_week_date( + date.year(), + date.iso_week(), + Weekday::from_short(target_week_day.unwrap().as_str()), + ) + .unwrap(), + ); + let interval_start = date.replace_date( + Date::from_iso_week_date(date.year(), date.iso_week(), week_start).unwrap(), + ); + + if new_date.assume_utc().unix_timestamp() + > interval_start + .add(Duration::weeks(1)) + .assume_utc() + .unix_timestamp() + { + continue; + } else if new_date.assume_utc().unix_timestamp() + < interval_start.assume_utc().unix_timestamp() + { + new_date = new_date.add(Duration::weeks(1)); + } + + if valid_months.len() == 0 + || valid_months.contains(&new_date.month().to_number()) + { + new_dates.push(new_date) + } + } else if frequency == &RepeatPeriod::MONTHLY && target_week_day.is_some() { + let mut allowed_days: Vec = Vec::new(); + + let week_change = + match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { + Ok(val) => val, + _ => 0, + }; + + let base_date = date.replace_day(1).unwrap(); + let stop_condition = + PrimitiveDateTime::new(base_date.date().add_month(), base_date.time()); + + for allowed_day in &valid_month_days { + if allowed_day.is_positive() { + allowed_days.push(allowed_day.unsigned_abs()); + continue; + } + + let day = + base_date.month().length(date.year()) - allowed_day.unsigned_abs() + 1; + allowed_days.push(day); + } + + let is_allowed_in_month_day = |day: u8| -> bool { + if allowed_days.len() == 0 { + return true; + } + + allowed_days.contains(&day) + }; + + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + + if week_change != 0 { + let mut new_date = base_date; + if week_change.is_negative() { + new_date = new_date + .replace_day(new_date.month().length(new_date.year())) + .unwrap(); + new_date = new_date.replace_date( + Date::from_iso_week_date( + new_date.year(), + new_date.iso_week(), + parsed_weekday, + ) + .unwrap(), + ); + + let new_week = new_date.iso_week() - week_change.unsigned_abs() + 1; + new_date = new_date.replace_date( + Date::from_iso_week_date( + new_date.year(), + new_week, + new_date.weekday(), + ) + .unwrap(), + ) + } else { + while new_date.weekday() != parsed_weekday { + new_date = new_date.add(Duration::days(1)); + } + + new_date = new_date.replace_date( + Date::from_iso_week_date( + new_date.year(), + new_date.iso_week() + week_change.unsigned_abs() - 1, + new_date.weekday(), + ) + .unwrap(), + ) + } + + if new_date.assume_utc().unix_timestamp() + >= base_date.assume_utc().unix_timestamp() + && new_date.assume_utc().unix_timestamp() + <= stop_condition.assume_utc().unix_timestamp() + && is_allowed_in_month_day(new_date.day()) + { + new_dates.push(new_date) + } + } else { + let mut current_date = base_date; + while current_date.assume_utc().unix_timestamp() + < stop_condition.assume_utc().unix_timestamp() + { + let new_date = current_date.replace_date( + Date::from_iso_week_date( + current_date.year(), + current_date.iso_week(), + parsed_weekday, + ) + .unwrap(), + ); + if new_date.assume_utc().unix_timestamp() + >= base_date.assume_utc().unix_timestamp() + && is_allowed_in_month_day(new_date.day()) + { + if valid_months.len() > 0 + && valid_months.contains(&new_date.month().to_number()) + { + new_dates.push(new_date) + } else if valid_months.len() == 0 { + new_dates.push(new_date) + } + } + + current_date = new_date.add(Duration::days(7)); + } + } + } else if frequency == &RepeatPeriod::ANNUALLY { + let week_change = + match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { + Ok(val) => val, + _ => 0, + }; + + if has_week_no && week_change != 0 { + println!( + "Invalid repeat rule, can't use BYWEEKNO with Week Offset on BYDAY" + ); + continue; + } + + if week_change != 0 && !has_week_no { + let mut new_date: PrimitiveDateTime; + + if !target_week_day.is_some() { + if week_change > 0 { + new_date = date + .replace_day(1) + .unwrap() + .replace_month(Month::January) + .unwrap() + .add(Duration::days(week_change - 1)) + } else { + new_date = date + .replace_month(Month::December) + .unwrap() + .replace_day(31) + .unwrap() + .sub(Duration::days(week_change.abs() - 1)) + } + } else { + let parsed_weekday = + Weekday::from_short(target_week_day.unwrap().as_str()); + + if week_change > 0 { + new_date = date + .replace_day(1) + .unwrap() + .replace_month(Month::January) + .unwrap() + .add(Duration::weeks(week_change - 1)); + + while new_date.weekday() != parsed_weekday { + new_date = new_date.add(Duration::days(1)); + } + } else { + new_date = date + .replace_month(Month::December) + .unwrap() + .replace_day(31) + .unwrap() + .sub(Duration::weeks(week_change.abs() - 1)); + while new_date.weekday() != parsed_weekday { + new_date = new_date.sub(Duration::days(1)); + } + } + } + + if new_date.assume_utc().unix_timestamp() + < date.assume_utc().unix_timestamp() + { + match new_date.replace_year(new_date.year() + 1) { + Ok(dt) => new_dates.push(dt), + _ => continue, + } + } else { + new_dates.push(new_date) + } + } else if has_week_no { + if !target_week_day.is_some() { + continue; + } + + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + let new_date = date.replace_date( + Date::from_iso_week_date(date.year(), date.iso_week(), parsed_weekday) + .unwrap(), + ); + + let interval_start = date.replace_date( + Date::from_iso_week_date(date.year(), date.iso_week(), week_start) + .unwrap(), + ); + let week_ahead = interval_start.add(Duration::days(7)); + + if new_date.assume_utc().unix_timestamp() + > week_ahead.assume_utc().unix_timestamp() + || new_date.assume_utc().unix_timestamp() + < date.assume_utc().unix_timestamp() + { + } else if new_date.assume_utc().unix_timestamp() + < interval_start.assume_utc().unix_timestamp() + { + new_dates.push(interval_start.add(Duration::days(7))); + } else { + new_dates.push(new_date); + } + } else { + if !target_week_day.is_some() { + continue; + } + + let day_one = date.replace_day(1).unwrap(); + let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); + + let stop_date = match Date::from_calendar_date( + date.year() + 1, + date.month(), + date.day(), + ) { + Ok(date) => date, + _ => continue, + }; + + let stop_condition = date.replace_date(stop_date); + let mut current_date = date.replace_date( + Date::from_iso_week_date( + date.year(), + day_one.iso_week(), + parsed_weekday, + ) + .unwrap(), + ); + + if current_date.assume_utc().unix_timestamp() + >= day_one.assume_utc().unix_timestamp() + { + new_dates.push(current_date); + } + + current_date = current_date.add(Duration::days(7)); + + while current_date.assume_utc().unix_timestamp() + < stop_condition.assume_utc().unix_timestamp() + { + new_dates.push(current_date); + current_date = current_date.add(Duration::days(7)); + } + } + } + } + } + + if frequency == &RepeatPeriod::ANNUALLY { + return new_dates + .iter() + .filter(|date| self.is_valid_day_in_year(**date, valid_year_days.clone())) + .map(|date| *date) + .collect(); + } + + new_dates + } + + fn get_valid_days_in_year(&self, year: i32, valid_year_days: &Vec) -> Vec { + let days_in_year = Date::from_calendar_date(year, Month::December, 31) + .unwrap() + .ordinal(); + let mut allowed_days: Vec = Vec::new(); + + for allowed_day in valid_year_days { + if allowed_day > &0 { + allowed_days.push(allowed_day.abs() as u16); + continue; + } + + let day = days_in_year - allowed_day.unsigned_abs() + 1; + allowed_days.push(day); + } + + allowed_days + } + + fn is_valid_day_in_year(&self, date: PrimitiveDateTime, valid_year_days: Vec) -> bool { + let valid_days = self.get_valid_days_in_year(date.year(), &valid_year_days); + + if valid_days.len() == 0 { + return true; + } + + let day_in_year = date.ordinal(); + + let is_valid = valid_days.contains(&day_in_year); + + return is_valid; + } + + fn finish_rules( + &self, + dates: Vec, + valid_months: Vec, + event_start_time: Option, + ) -> Vec { + let mut clean_dates; + + if valid_months.len() > 0 { + clean_dates = dates + .iter() + .filter(|date| valid_months.contains(&date.month().to_number())) + .map(|date| *date) + .collect(); + } else { + clean_dates = dates + }; + + if event_start_time.is_some() { + clean_dates = clean_dates + .iter() + .filter(|date| { + let date_unix_timestamp = date.assume_utc().unix_timestamp(); + date_unix_timestamp >= event_start_time.unwrap() + }) + .map(|date| *date) + .collect(); + } + + clean_dates.sort_by(|a, b| { + a.assume_utc() + .unix_timestamp() + .cmp(&b.assume_utc().unix_timestamp()) + }); + clean_dates.dedup(); + + clean_dates + } } #[cfg(test)] mod tests { - use time::{Date, Month, PrimitiveDateTime, Time}; - - use super::*; - - trait PrimitiveToDateTime { - fn to_date_time(&self) -> DateTime; - } - impl PrimitiveToDateTime for PrimitiveDateTime { - fn to_date_time(&self) -> DateTime { - DateTime::from_millis(self.assume_utc().unix_timestamp() as u64) - } - } - - #[test] - fn test_parse_weekly_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::January, 23).unwrap(), - time, - ); - let invalid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::March, 11).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![valid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::WEEKLY, - ), - vec![valid_date] - ); - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![invalid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::WEEKLY, - ), - vec![] - ); - } - - #[test] - fn test_parse_monthly_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::January, 23).unwrap(), - time, - ); - let invalid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::March, 11).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![valid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::MONTHLY, - ), - vec![valid_date] - ); - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![invalid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::MONTHLY, - ), - vec![] - ); - } - - #[test] - fn test_parse_yearly_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::January, 23).unwrap(), - time, - ); - let to_next_year = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::March, 11).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![valid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::ANNUALLY, - ), - vec![ - valid_date, - valid_date.replace_month(Month::February).unwrap(), - ] - ); - - // BYMONTH never limits on Yearly, just expands - assert_eq!( - event_recurrence.apply_month_rules( - &vec![to_next_year], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::ANNUALLY, - ), - vec![ - to_next_year - .replace_year(2025) - .unwrap() - .replace_month(Month::January) - .unwrap(), - to_next_year - .replace_year(2025) - .unwrap() - .replace_month(Month::February) - .unwrap(), - ] - ); - } - - #[test] - fn test_parse_daily_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::January, 23).unwrap(), - time, - ); - let invalid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2024, Month::March, 11).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![valid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::DAILY, - ), - vec![valid_date] - ); - - assert_eq!( - event_recurrence.apply_month_rules( - &vec![invalid_date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "1".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ], - &RepeatPeriod::DAILY, - ), - vec![] - ); - } - - #[test] - fn test_parse_positive_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 31).unwrap(), - time, - ); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2025, Month::January, 27).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new( - base_date.add(Duration::days(i)), - time, - )); - } - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_week_no_rules( - vec![valid_date], - &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "5".to_string(), - }, ], - Weekday::Monday, - ), - valid_dates - ); - } - - #[test] - fn test_parse_wkst_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 31).unwrap(), - time, - ); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2025, Month::January, 28).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new( - base_date.add(Duration::days(i)), - time, - )); - } - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_week_no_rules( - vec![valid_date], - &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "5".to_string(), - }, ], - Weekday::Tuesday, - ), - valid_dates - ); - } - - #[test] - fn test_parse_negative_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::December, 4).unwrap(), - time, - ); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2025, Month::November, 24).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new( - base_date.add(Duration::days(i)), - time, - )); - } - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_week_no_rules( - vec![valid_date], - &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "-5".to_string(), - }, ], - Weekday::Monday, - ), - valid_dates - ); - } - - #[test] - fn test_parse_edge_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2026, Month::December, 29).unwrap(), - time, - ); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2026, Month::December, 28).unwrap(); - for i in 0..4 { - valid_dates.push(PrimitiveDateTime::new( - base_date.add(Duration::days(i)), - time, - )); - } - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_week_no_rules( - vec![valid_date], - &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "-1".to_string(), - }, ], - Weekday::Monday, - ), - valid_dates - ); - } - - #[test] - fn test_parse_out_of_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 22).unwrap(), - time, - ); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2026, Month::January, 26).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new( - base_date.add(Duration::days(i)), - time, - )); - } - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_week_no_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "5".to_string(), - }, ], - Weekday::Monday, - ), - valid_dates - ); - } - - #[test] - fn test_parse_year_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 1).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_year_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "40".to_string(), - }], - false, - false, - ), - [date.replace_day(9).unwrap()] - ); - } - - #[test] - fn test_parse_year_day_keep_week() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 1).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_year_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "40".to_string(), - }], - true, - false, - ), - [] - ); - } - - #[test] - fn test_parse_year_day_keep_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 22).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_year_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "40".to_string(), - }], - true, - true, - ), - [] - ); - } - - #[test] - fn test_parse_out_of_year_year_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 22).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_year_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "40".to_string(), - }], - false, - false, - ), - [date.replace_year(2026).unwrap().replace_day(9).unwrap()] - ); - } - - #[test] - fn test_parse_negative_year_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 22).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_year_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "-1".to_string(), - }], - false, - false, - ), - [date - .replace_month(Month::December) - .unwrap() - .replace_day(31) - .unwrap()] - ); - } - - #[test] - fn test_parse_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 22).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_month_day_rules( - vec![date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "10".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "20".to_string(), - }, - ], - false, - ), - [date.replace_day(10).unwrap(), date.replace_day(20).unwrap()] - ); - } - - #[test] - fn test_parse_invalid_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 22).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_month_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "30".to_string(), - }, ], - false, - ), - [] - ); - } - - #[test] - fn test_parse_daily_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 20).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_month_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "20".to_string(), - }], - false, - ), - [date.replace_day(20).unwrap()] - ); - } - - #[test] - fn test_parse_negative_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_month_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "-1".to_string(), - }, ], - false, - ), - [date.replace_day(31).unwrap(), ] - ); - } - - #[test] - fn test_parse_invalid_date_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_month_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "32".to_string(), - }, ], - false, - ), - [] - ); - } - - #[test] - fn test_parse_by_day_daily() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }], - &RepeatPeriod::DAILY, - vec![], - Weekday::Monday, - false, - vec![], - vec![], - ), - [date] - ); - } - - #[test] - fn test_parse_by_day_daily_invalid() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 8).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }], - &RepeatPeriod::DAILY, - vec![], - Weekday::Monday, - false, - vec![], - vec![], - ), - [] - ); - } - - #[test] - fn test_parse_by_day_weekly() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 9).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "SA".to_string(), - }, - ], - &RepeatPeriod::WEEKLY, - vec![], - Weekday::Monday, - false, - vec![], - vec![], - ), - [date.replace_day(10).unwrap(), date.replace_day(11).unwrap()] - ); - } - - #[test] - fn test_parse_by_day_monthly() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 6).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - // Can be WEEKDAY + WEEK - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "MO".to_string(), - }, ], - &RepeatPeriod::MONTHLY, - vec![], - Weekday::Monday, - false, - vec![], - vec![], - ), - [ - date, - date.replace_day(13).unwrap(), - date.replace_day(20).unwrap(), - date.replace_day(27).unwrap() - ] - ); - } - - #[test] - fn test_parse_by_day_monthly_with_monthday() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 6).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - let rules = vec![ - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "MO".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "7".to_string(), - }, - ]; - let by_day_rules: Vec<&ByRule> = rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYDAY) - .collect(); - let by_month_day_rules: Vec<&ByRule> = rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) - .collect(); - - let valid_month_days: Vec = by_month_day_rules - .iter() - .clone() - .map(|&x| x.interval.parse::().unwrap()) - .collect(); - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &by_day_rules, - &RepeatPeriod::MONTHLY, - vec![], - Weekday::Monday, - false, - valid_month_days, - vec![], - ), - [] - ); - } - - #[test] - fn test_parse_by_day_monthly_with_week() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - // Can be WEEKDAY + WEEK - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2MO".to_string(), - }, ], - &RepeatPeriod::MONTHLY, - vec![], - Weekday::Monday, - false, - vec![], - vec![], - ), - [date.replace_day(13).unwrap()] - ); - } - - #[test] - fn test_parse_by_day_monthly_with_monthday_and_week() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 6).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - let rules = vec![ - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2MO".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "7".to_string(), - }, - ]; - let by_day_rules: Vec<&ByRule> = rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYDAY) - .collect(); - let by_month_day_rules: Vec<&ByRule> = rules - .iter() - .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) - .collect(); - - let valid_month_days: Vec = by_month_day_rules - .iter() - .clone() - .map(|&x| x.interval.parse::().unwrap()) - .collect(); - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &by_day_rules, - &RepeatPeriod::MONTHLY, - vec![], - Weekday::Monday, - false, - valid_month_days, - vec![], - ), - [] - ); - } - - #[test] - fn test_parse_by_day_yearly() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 6).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - let end_date = date.replace_year(2026).unwrap(); - let mut current_date = date; - let mut expected_dates: Vec = Vec::new(); - - while current_date.assume_utc().unix_timestamp() < end_date.assume_utc().unix_timestamp() { - expected_dates.push(current_date); - current_date = current_date.add(Duration::days(7)) - } - - // Can be WEEKDAY + WEEK - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "MO".to_string(), - }, ], - &RepeatPeriod::ANNUALLY, - vec![], - Weekday::Monday, - false, - vec![], - vec![], - ), - expected_dates - ); - } - - #[test] - fn test_parse_by_day_yearly_with_week() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - // Can be WEEKDAY + WEEK - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2MO".to_string(), - }, ], - &RepeatPeriod::ANNUALLY, - vec![], - Weekday::Monday, - false, - vec![], - vec![], - ), - [date.replace_day(13).unwrap(), ] - ); - } - - #[test] - fn test_parse_by_day_yearly_with_ordinal_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - // Can be WEEKDAY + WEEK - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![&ByRule { - by_rule: ByRuleType::BYDAY, - interval: "35".to_string(), - }, ], - &RepeatPeriod::ANNUALLY, - vec![], - Weekday::Monday, - false, - vec![], - vec![], - ), - [date - .replace_month(Month::February) - .unwrap() - .replace_day(4) - .unwrap(), ] - ); - } - - #[test] - fn test_parse_by_day_yearly_with_weekno() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 6).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "MO".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "6".to_string(), - }, - ], - &RepeatPeriod::ANNUALLY, - vec![], - Weekday::Monday, - true, - vec![], - vec![], - ), - [date] - ); - } - - #[test] - fn test_parse_by_day_yearly_with_unmatch_weekno() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "35".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "7".to_string(), - }, - ], - &RepeatPeriod::ANNUALLY, - vec![], - Weekday::Monday, - true, - vec![], - vec![], - ), - [] - ); - } - - #[test] - fn test_parse_by_day_yearly_with_invalid_rule() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::January, 10).unwrap(), - time, - ); - - let event_recurrence = EventFacade {}; - // Can be WEEKDAY + WEEK - - assert_eq!( - event_recurrence.apply_day_rules( - vec![date], - &vec![ - &ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2MO".to_string(), - }, - &ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "6".to_string(), - }, - ], - &RepeatPeriod::ANNUALLY, - vec![], - Weekday::Monday, - true, - vec![], - vec![], - ), - [] - ); - } - - #[test] - fn test_flow_with_by_month_daily() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::March, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::DAILY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "3".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "6".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances( - date.replace_month(Month::January).unwrap().to_date_time(), - repeat_rule.clone(), - ), - [] - ); - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.to_date_time()] - ); - assert_eq!( - event_recurrence.generate_future_instances( - date.replace_month(Month::February).unwrap().to_date_time(), - repeat_rule.clone(), - ), - [date.replace_month(Month::February).unwrap().to_date_time()] - ); - assert_eq!( - event_recurrence.generate_future_instances( - date.replace_month(Month::June).unwrap().to_date_time(), - repeat_rule.clone(), - ), - [date.replace_month(Month::June).unwrap().to_date_time()] - ); - } - - #[test] - fn test_flow_daily_with_by_month_and_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::DAILY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [] - ); - } - - #[test] - fn test_flow_daily_with_by_month_and_by_day_and_by_monthday() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 14).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::DAILY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "14".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.replace_day(14).unwrap().to_date_time()] - ); - assert_eq!( - event_recurrence - .generate_future_instances(date.replace_day(13).unwrap().to_date_time(), repeat_rule.clone()), - [] - ); - } - - #[test] - fn test_flow_weekly_with_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::WEEKLY, - by_rules: vec![ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.replace_day(10).unwrap().to_date_time(), ] - ); - assert_eq!( - event_recurrence.generate_future_instances( - date.replace_month(Month::January).unwrap().to_date_time(), - repeat_rule.clone(), - ), - [] - ); - } - - #[test] - fn test_flow_weekly_with_by_month_and_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::WEEKLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.replace_day(13).unwrap().to_date_time(), date.replace_day(14).unwrap().to_date_time()] - ); - } - - #[test] - fn test_flow_weekly_with_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::WEEKLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.replace_day(13).unwrap().to_date_time(), date.replace_day(14).unwrap().to_date_time()] - ); - } - - #[test] - fn test_flow_weekly_with_by_day_and_wkst() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::WEEKLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::WKST, - interval: "FR".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.replace_day(14).unwrap().to_date_time(), date.replace_day(20).unwrap().to_date_time()] - ); - } - - #[test] - fn test_flow_monthly_with_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [ - date.replace_day(14).unwrap().to_date_time(), - date.replace_day(21).unwrap().to_date_time(), - date.replace_day(28).unwrap().to_date_time() - ] - ); - } - - #[test] - fn test_flow_monthly_with_second_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ByRule { - by_rule: ByRuleType::BYDAY, - interval: "2FR".to_string(), - }], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.replace_day(14).unwrap().to_date_time(), ] - ); - } - - #[test] - fn test_flow_monthly_with_two_last_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "-1FR".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "-2FR".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.replace_day(21).unwrap().to_date_time(), date.replace_day(28).unwrap().to_date_time(), ] - ); - } - - #[test] - fn test_flow_monthly_with_by_month() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - let date_not_in_range = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::March, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.replace_day(10).unwrap().to_date_time(), ] - ); - assert_eq!( - event_recurrence.generate_future_instances(date_not_in_range.to_date_time(), repeat_rule.clone()), - [] - ); - } - - #[test] - fn test_flow_monthly_with_by_month_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "25".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "28".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.replace_day(25).unwrap().to_date_time(), date.replace_day(28).unwrap().to_date_time(), ] - ); - } - - #[test] - fn test_flow_monthly_with_by_month_day_and_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "25".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYMONTHDAY, - interval: "28".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.replace_day(28).unwrap().to_date_time()] - ); - } - - #[test] - fn test_flow_monthly_with_by_month_and_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [ - date.replace_day(13).unwrap().to_date_time(), - date.replace_day(14).unwrap().to_date_time(), - date.replace_day(20).unwrap().to_date_time(), - date.replace_day(21).unwrap().to_date_time(), - date.replace_day(27).unwrap().to_date_time(), - date.replace_day(28).unwrap().to_date_time() - ] - ); - } - - #[test] - fn test_flow_yearly_with_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let stop_condition = PrimitiveDateTime::new( - Date::from_calendar_date(2026, Month::February, 10).unwrap(), - time, - ); - let mut expected_dates: Vec = Vec::new(); - let mut current_date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 13).unwrap(), - time, - ); - - while current_date.assume_utc().unix_timestamp() - < stop_condition.assume_utc().unix_timestamp() - { - expected_dates.push(current_date.to_date_time()); - expected_dates.push(current_date.add(Duration::days(1)).to_date_time()); - - current_date = current_date.add(Duration::days(7)); - } - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::ANNUALLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - expected_dates - ); - } - - #[test] - fn test_flow_yearly_with_by_day_and_by_year_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::ANNUALLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYYEARDAY, - interval: "44".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.replace_day(13).unwrap().to_date_time()] - ); - - assert_eq!( - event_recurrence.generate_future_instances( - date.replace_month(Month::March).unwrap().to_date_time(), - repeat_rule.clone(), - ), - [] - ); - } - - #[test] - fn test_flow_yearly_with_by_week_no_and_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::ANNUALLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "8".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [date.replace_day(20).unwrap().to_date_time()] - ); - - assert_eq!( - event_recurrence.generate_future_instances( - date.replace_month(Month::March).unwrap().to_date_time(), - repeat_rule.clone(), - ), - [date.replace_year(2026).unwrap().replace_day(19).unwrap().to_date_time()] - ); - } - - #[test] - fn test_flow_yearly_with_negative_week_no() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let valid_date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::December, 4).unwrap(), - time, - ); - - let mut valid_dates: Vec = Vec::new(); - let base_date = Date::from_calendar_date(2025, Month::December, 1).unwrap(); - for i in 0..7 { - valid_dates.push(PrimitiveDateTime::new( - base_date.add(Duration::days(i)), - time, - )); - } - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::ANNUALLY, - by_rules: vec![ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "-5".to_string(), - }], - }; - - let event_recurrence = EventFacade {}; - - assert_eq!( - event_recurrence.generate_future_instances(valid_date.to_date_time(), repeat_rule), - [] - ); - } - - #[test] - fn test_flow_yearly_with_by_week_no_and_wkst() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::ANNUALLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYWEEKNO, - interval: "8".to_string(), - }, - ByRule { - by_rule: ByRuleType::WKST, - interval: "TU".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [ - date.replace_day(18).unwrap().to_date_time(), - date.replace_day(19).unwrap().to_date_time(), - date.replace_day(20).unwrap().to_date_time(), - date.replace_day(21).unwrap().to_date_time(), - date.replace_day(22).unwrap().to_date_time(), - date.replace_day(23).unwrap().to_date_time(), - date.replace_day(24).unwrap().to_date_time(), - ] - ); - } - - #[test] - fn test_flow_yearly_with_by_month_and_by_day() { - let time = Time::from_hms(13, 23, 00).unwrap(); - let date = PrimitiveDateTime::new( - Date::from_calendar_date(2025, Month::February, 10).unwrap(), - time, - ); - - let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::ANNUALLY, - by_rules: vec![ - ByRule { - by_rule: ByRuleType::BYMONTH, - interval: "2".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "TH".to_string(), - }, - ByRule { - by_rule: ByRuleType::BYDAY, - interval: "FR".to_string(), - }, - ], - }; - - let event_recurrence = EventFacade {}; - assert_eq!( - event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), - [ - date.replace_day(13).unwrap().to_date_time(), - date.replace_day(14).unwrap().to_date_time(), - date.replace_day(20).unwrap().to_date_time(), - date.replace_day(21).unwrap().to_date_time(), - date.replace_day(27).unwrap().to_date_time(), - date.replace_day(28).unwrap().to_date_time(), - date.replace_year(2026).unwrap().replace_day(5).unwrap().to_date_time(), - date.replace_year(2026).unwrap().replace_day(6).unwrap().to_date_time(), - ] - ); - } + use time::{Date, Month, PrimitiveDateTime, Time}; + + use super::*; + + trait PrimitiveToDateTime { + fn to_date_time(&self) -> DateTime; + } + impl PrimitiveToDateTime for PrimitiveDateTime { + fn to_date_time(&self) -> DateTime { + DateTime::from_millis(self.assume_utc().unix_timestamp() as u64) + } + } + + #[test] + fn test_parse_weekly_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 23).unwrap(), + time, + ); + let invalid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::March, 11).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![valid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::WEEKLY, + ), + vec![valid_date] + ); + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![invalid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::WEEKLY, + ), + vec![] + ); + } + + #[test] + fn test_parse_monthly_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 23).unwrap(), + time, + ); + let invalid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::March, 11).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![valid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::MONTHLY, + ), + vec![valid_date] + ); + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![invalid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::MONTHLY, + ), + vec![] + ); + } + + #[test] + fn test_parse_yearly_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 23).unwrap(), + time, + ); + let to_next_year = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::March, 11).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![valid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + ), + vec![ + valid_date, + valid_date.replace_month(Month::February).unwrap(), + ] + ); + + // BYMONTH never limits on Yearly, just expands + assert_eq!( + event_recurrence.apply_month_rules( + &vec![to_next_year], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + ), + vec![ + to_next_year + .replace_year(2025) + .unwrap() + .replace_month(Month::January) + .unwrap(), + to_next_year + .replace_year(2025) + .unwrap() + .replace_month(Month::February) + .unwrap(), + ] + ); + } + + #[test] + fn test_parse_daily_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::January, 23).unwrap(), + time, + ); + let invalid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2024, Month::March, 11).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![valid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::DAILY, + ), + vec![valid_date] + ); + + assert_eq!( + event_recurrence.apply_month_rules( + &vec![invalid_date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "1".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ], + &RepeatPeriod::DAILY, + ), + vec![] + ); + } + + #[test] + fn test_parse_positive_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 31).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::January, 27).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![valid_date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "5".to_string(), + },], + Weekday::Monday, + ), + valid_dates + ); + } + + #[test] + fn test_parse_wkst_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 31).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::January, 28).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![valid_date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "5".to_string(), + },], + Weekday::Tuesday, + ), + valid_dates + ); + } + + #[test] + fn test_parse_negative_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::December, 4).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::November, 24).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![valid_date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "-5".to_string(), + },], + Weekday::Monday, + ), + valid_dates + ); + } + + #[test] + fn test_parse_edge_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2026, Month::December, 29).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2026, Month::December, 28).unwrap(); + for i in 0..4 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![valid_date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "-1".to_string(), + },], + Weekday::Monday, + ), + valid_dates + ); + } + + #[test] + fn test_parse_out_of_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2026, Month::January, 26).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_week_no_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "5".to_string(), + },], + Weekday::Monday, + ), + valid_dates + ); + } + + #[test] + fn test_parse_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 1).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + }], + false, + false, + ), + [date.replace_day(9).unwrap()] + ); + } + + #[test] + fn test_parse_year_day_keep_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 1).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + }], + true, + false, + ), + [] + ); + } + + #[test] + fn test_parse_year_day_keep_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 22).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + }], + true, + true, + ), + [] + ); + } + + #[test] + fn test_parse_out_of_year_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "40".to_string(), + }], + false, + false, + ), + [date.replace_year(2026).unwrap().replace_day(9).unwrap()] + ); + } + + #[test] + fn test_parse_negative_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_year_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "-1".to_string(), + }], + false, + false, + ), + [date + .replace_month(Month::December) + .unwrap() + .replace_day(31) + .unwrap()] + ); + } + + #[test] + fn test_parse_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "10".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "20".to_string(), + }, + ], + false, + ), + [date.replace_day(10).unwrap(), date.replace_day(20).unwrap()] + ); + } + + #[test] + fn test_parse_invalid_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 22).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "30".to_string(), + },], + false, + ), + [] + ); + } + + #[test] + fn test_parse_daily_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 20).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "20".to_string(), + }], + false, + ), + [date.replace_day(20).unwrap()] + ); + } + + #[test] + fn test_parse_negative_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "-1".to_string(), + },], + false, + ), + [date.replace_day(31).unwrap(),] + ); + } + + #[test] + fn test_parse_invalid_date_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_month_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "32".to_string(), + },], + false, + ), + [] + ); + } + + #[test] + fn test_parse_by_day_daily() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }], + &RepeatPeriod::DAILY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [date] + ); + } + + #[test] + fn test_parse_by_day_daily_invalid() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 8).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }], + &RepeatPeriod::DAILY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [] + ); + } + + #[test] + fn test_parse_by_day_weekly() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 9).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "SA".to_string(), + }, + ], + &RepeatPeriod::WEEKLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [date.replace_day(10).unwrap(), date.replace_day(11).unwrap()] + ); + } + + #[test] + fn test_parse_by_day_monthly() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + },], + &RepeatPeriod::MONTHLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [ + date, + date.replace_day(13).unwrap(), + date.replace_day(20).unwrap(), + date.replace_day(27).unwrap() + ] + ); + } + + #[test] + fn test_parse_by_day_monthly_with_monthday() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + let rules = vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "7".to_string(), + }, + ]; + let by_day_rules: Vec<&ByRule> = rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYDAY) + .collect(); + let by_month_day_rules: Vec<&ByRule> = rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) + .collect(); + + let valid_month_days: Vec = by_month_day_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &by_day_rules, + &RepeatPeriod::MONTHLY, + vec![], + Weekday::Monday, + false, + valid_month_days, + vec![], + ), + [] + ); + } + + #[test] + fn test_parse_by_day_monthly_with_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + },], + &RepeatPeriod::MONTHLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [date.replace_day(13).unwrap()] + ); + } + + #[test] + fn test_parse_by_day_monthly_with_monthday_and_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + let rules = vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "7".to_string(), + }, + ]; + let by_day_rules: Vec<&ByRule> = rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYDAY) + .collect(); + let by_month_day_rules: Vec<&ByRule> = rules + .iter() + .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) + .collect(); + + let valid_month_days: Vec = by_month_day_rules + .iter() + .clone() + .map(|&x| x.interval.parse::().unwrap()) + .collect(); + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &by_day_rules, + &RepeatPeriod::MONTHLY, + vec![], + Weekday::Monday, + false, + valid_month_days, + vec![], + ), + [] + ); + } + + #[test] + fn test_parse_by_day_yearly() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + let end_date = date.replace_year(2026).unwrap(); + let mut current_date = date; + let mut expected_dates: Vec = Vec::new(); + + while current_date.assume_utc().unix_timestamp() < end_date.assume_utc().unix_timestamp() { + expected_dates.push(current_date); + current_date = current_date.add(Duration::days(7)) + } + + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + },], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + expected_dates + ); + } + + #[test] + fn test_parse_by_day_yearly_with_week() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + },], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [date.replace_day(13).unwrap(),] + ); + } + + #[test] + fn test_parse_by_day_yearly_with_ordinal_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![&ByRule { + by_rule: ByRuleType::BYDAY, + interval: "35".to_string(), + },], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + false, + vec![], + vec![], + ), + [date + .replace_month(Month::February) + .unwrap() + .replace_day(4) + .unwrap(),] + ); + } + + #[test] + fn test_parse_by_day_yearly_with_weekno() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 6).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "MO".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "6".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + true, + vec![], + vec![], + ), + [date] + ); + } + + #[test] + fn test_parse_by_day_yearly_with_unmatch_weekno() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "35".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "7".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + true, + vec![], + vec![], + ), + [] + ); + } + + #[test] + fn test_parse_by_day_yearly_with_invalid_rule() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::January, 10).unwrap(), + time, + ); + + let event_recurrence = EventFacade {}; + // Can be WEEKDAY + WEEK + + assert_eq!( + event_recurrence.apply_day_rules( + vec![date], + &vec![ + &ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2MO".to_string(), + }, + &ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "6".to_string(), + }, + ], + &RepeatPeriod::ANNUALLY, + vec![], + Weekday::Monday, + true, + vec![], + vec![], + ), + [] + ); + } + + #[test] + fn test_flow_with_by_month_daily() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::March, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::DAILY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "3".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "6".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::January).unwrap().to_date_time(), + repeat_rule.clone(), + ), + [] + ); + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.to_date_time()] + ); + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::February).unwrap().to_date_time(), + repeat_rule.clone(), + ), + [date.replace_month(Month::February).unwrap().to_date_time()] + ); + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::June).unwrap().to_date_time(), + repeat_rule.clone(), + ), + [date.replace_month(Month::June).unwrap().to_date_time()] + ); + } + + #[test] + fn test_flow_daily_with_by_month_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::DAILY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [] + ); + } + + #[test] + fn test_flow_daily_with_by_month_and_by_day_and_by_monthday() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 14).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::DAILY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "14".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(14).unwrap().to_date_time()] + ); + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_day(13).unwrap().to_date_time(), + repeat_rule.clone() + ), + [] + ); + } + + #[test] + fn test_flow_weekly_with_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::WEEKLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(10).unwrap().to_date_time(),] + ); + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::January).unwrap().to_date_time(), + repeat_rule.clone(), + ), + [] + ); + } + + #[test] + fn test_flow_weekly_with_by_month_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::WEEKLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(13).unwrap().to_date_time(), + date.replace_day(14).unwrap().to_date_time() + ] + ); + } + + #[test] + fn test_flow_weekly_with_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::WEEKLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(13).unwrap().to_date_time(), + date.replace_day(14).unwrap().to_date_time() + ] + ); + } + + #[test] + fn test_flow_weekly_with_by_day_and_wkst() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::WEEKLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::WKST, + interval: "FR".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(14).unwrap().to_date_time(), + date.replace_day(20).unwrap().to_date_time() + ] + ); + } + + #[test] + fn test_flow_monthly_with_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(14).unwrap().to_date_time(), + date.replace_day(21).unwrap().to_date_time(), + date.replace_day(28).unwrap().to_date_time() + ] + ); + } + + #[test] + fn test_flow_monthly_with_second_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYDAY, + interval: "2FR".to_string(), + }], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(14).unwrap().to_date_time(),] + ); + } + + #[test] + fn test_flow_monthly_with_two_last_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "-1FR".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "-2FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(21).unwrap().to_date_time(), + date.replace_day(28).unwrap().to_date_time(), + ] + ); + } + + #[test] + fn test_flow_monthly_with_by_month() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + let date_not_in_range = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::March, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(10).unwrap().to_date_time(),] + ); + assert_eq!( + event_recurrence + .generate_future_instances(date_not_in_range.to_date_time(), repeat_rule.clone()), + [] + ); + } + + #[test] + fn test_flow_monthly_with_by_month_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "25".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "28".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(25).unwrap().to_date_time(), + date.replace_day(28).unwrap().to_date_time(), + ] + ); + } + + #[test] + fn test_flow_monthly_with_by_month_day_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "25".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYMONTHDAY, + interval: "28".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(28).unwrap().to_date_time()] + ); + } + + #[test] + fn test_flow_monthly_with_by_month_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::MONTHLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(13).unwrap().to_date_time(), + date.replace_day(14).unwrap().to_date_time(), + date.replace_day(20).unwrap().to_date_time(), + date.replace_day(21).unwrap().to_date_time(), + date.replace_day(27).unwrap().to_date_time(), + date.replace_day(28).unwrap().to_date_time() + ] + ); + } + + #[test] + fn test_flow_yearly_with_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let stop_condition = PrimitiveDateTime::new( + Date::from_calendar_date(2026, Month::February, 10).unwrap(), + time, + ); + let mut expected_dates: Vec = Vec::new(); + let mut current_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 13).unwrap(), + time, + ); + + while current_date.assume_utc().unix_timestamp() + < stop_condition.assume_utc().unix_timestamp() + { + expected_dates.push(current_date.to_date_time()); + expected_dates.push(current_date.add(Duration::days(1)).to_date_time()); + + current_date = current_date.add(Duration::days(7)); + } + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + expected_dates + ); + } + + #[test] + fn test_flow_yearly_with_by_day_and_by_year_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYYEARDAY, + interval: "44".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(13).unwrap().to_date_time()] + ); + + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::March).unwrap().to_date_time(), + repeat_rule.clone(), + ), + [] + ); + } + + #[test] + fn test_flow_yearly_with_by_week_no_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "8".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [date.replace_day(20).unwrap().to_date_time()] + ); + + assert_eq!( + event_recurrence.generate_future_instances( + date.replace_month(Month::March).unwrap().to_date_time(), + repeat_rule.clone(), + ), + [date + .replace_year(2026) + .unwrap() + .replace_day(19) + .unwrap() + .to_date_time()] + ); + } + + #[test] + fn test_flow_yearly_with_negative_week_no() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let valid_date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::December, 4).unwrap(), + time, + ); + + let mut valid_dates: Vec = Vec::new(); + let base_date = Date::from_calendar_date(2025, Month::December, 1).unwrap(); + for i in 0..7 { + valid_dates.push(PrimitiveDateTime::new( + base_date.add(Duration::days(i)), + time, + )); + } + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "-5".to_string(), + }], + }; + + let event_recurrence = EventFacade {}; + + assert_eq!( + event_recurrence.generate_future_instances(valid_date.to_date_time(), repeat_rule), + [] + ); + } + + #[test] + fn test_flow_yearly_with_by_week_no_and_wkst() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYWEEKNO, + interval: "8".to_string(), + }, + ByRule { + by_rule: ByRuleType::WKST, + interval: "TU".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(18).unwrap().to_date_time(), + date.replace_day(19).unwrap().to_date_time(), + date.replace_day(20).unwrap().to_date_time(), + date.replace_day(21).unwrap().to_date_time(), + date.replace_day(22).unwrap().to_date_time(), + date.replace_day(23).unwrap().to_date_time(), + date.replace_day(24).unwrap().to_date_time(), + ] + ); + } + + #[test] + fn test_flow_yearly_with_by_month_and_by_day() { + let time = Time::from_hms(13, 23, 00).unwrap(); + let date = PrimitiveDateTime::new( + Date::from_calendar_date(2025, Month::February, 10).unwrap(), + time, + ); + + let repeat_rule = EventRepeatRule { + frequency: RepeatPeriod::ANNUALLY, + by_rules: vec![ + ByRule { + by_rule: ByRuleType::BYMONTH, + interval: "2".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "TH".to_string(), + }, + ByRule { + by_rule: ByRuleType::BYDAY, + interval: "FR".to_string(), + }, + ], + }; + + let event_recurrence = EventFacade {}; + assert_eq!( + event_recurrence.generate_future_instances(date.to_date_time(), repeat_rule.clone()), + [ + date.replace_day(13).unwrap().to_date_time(), + date.replace_day(14).unwrap().to_date_time(), + date.replace_day(20).unwrap().to_date_time(), + date.replace_day(21).unwrap().to_date_time(), + date.replace_day(27).unwrap().to_date_time(), + date.replace_day(28).unwrap().to_date_time(), + date.replace_year(2026) + .unwrap() + .replace_day(5) + .unwrap() + .to_date_time(), + date.replace_year(2026) + .unwrap() + .replace_day(6) + .unwrap() + .to_date_time(), + ] + ); + } } From e1fe4d966f7a77004a1d60b3e7e5a11d5de9a956 Mon Sep 17 00:00:00 2001 From: Murilo Pereira <34790144+murilopereirame@users.noreply.github.com> Date: Mon, 13 Jan 2025 08:44:21 +0100 Subject: [PATCH 22/29] [iOS] Integrates SDK event expansion during alarm scheduling --- .../Alarms/AlarmModel.swift | 69 +++++++++++++++---- .../Alarms/RepeatRule.swift | 9 +++ .../Notifications/EncryptedRepeatRule.swift | 61 ++++++++++++++++ .../TutanotaSharedFramework/Utils/Utils.swift | 4 +- 4 files changed, 129 insertions(+), 14 deletions(-) diff --git a/app-ios/TutanotaSharedFramework/Alarms/AlarmModel.swift b/app-ios/TutanotaSharedFramework/Alarms/AlarmModel.swift index d2b20739c81c..645d47628083 100644 --- a/app-ios/TutanotaSharedFramework/Alarms/AlarmModel.swift +++ b/app-ios/TutanotaSharedFramework/Alarms/AlarmModel.swift @@ -1,4 +1,5 @@ import Foundation +import tutasdk /// Identifier for when event will happen public struct EventOccurrence { @@ -91,7 +92,14 @@ public class AlarmModel: AlarmCalculator { cal.timeZone = isAllDayEvent ? localTimeZone : TimeZone(identifier: repeatRule.timeZone) ?? localTimeZone - return LazyEventSequence(calcEventStart: calcEventStart, endDate: endDate, repeatRule: repeatRule, cal: cal, calendarComponent: calendarUnit) + return LazyEventSequence( + calcEventStart: calcEventStart, + endDate: endDate, + repeatRule: repeatRule, + cal: cal, + calendarComponent: calendarUnit, + dateProvider: self.dateProvider + ) } static func alarmTime(trigger: AlarmInterval, eventTime: Date) -> Date { @@ -111,22 +119,48 @@ private struct LazyEventSequence: Sequence, IteratorProtocol { let repeatRule: RepeatRule let cal: Calendar let calendarComponent: Calendar.Component + let dateProvider: DateProvider + + var expandedEvents: [DateTime] = [] - fileprivate var ocurrenceNumber = 0 + fileprivate var intervalNumber = 0 + fileprivate var occurrenceNumber = 0 fileprivate var exclusionNumber = 0 mutating func next() -> EventOccurrence? { - if case let .count(n) = repeatRule.endCondition, ocurrenceNumber >= n { return nil } - let occurrenceDate = cal.date(byAdding: self.calendarComponent, value: repeatRule.interval * ocurrenceNumber, to: calcEventStart)! - if let endDate, occurrenceDate >= endDate { - return nil - } else { - let occurrence = EventOccurrence(occurrenceNumber: ocurrenceNumber, occurenceDate: occurrenceDate) - ocurrenceNumber += 1 + if case let .count(n) = repeatRule.endCondition, occurrenceNumber >= n { return nil } + + if expandedEvents.isEmpty { + let nextExpansionProgenitor = cal.date(byAdding: self.calendarComponent, value: repeatRule.interval * intervalNumber, to: calcEventStart)! + let progenitorTime = UInt64(nextExpansionProgenitor.timeIntervalSince1970) + let eventFacade = EventFacade() + let byRules = repeatRule.advancedRules.map { $0.toSDKRule() } + let generatedEvents = eventFacade.generateFutureInstances( + date: progenitorTime * 1000, + repeatRule: EventRepeatRule(frequency: repeatRule.frequency.toSDKPeriod(), byRules: byRules) + ) + self.expandedEvents.append(contentsOf: generatedEvents) + // Handle the event 0 + if self.intervalNumber == 0 && !self.expandedEvents.contains(progenitorTime) { expandedEvents.append(progenitorTime) } + + intervalNumber += 1 + } + + if let date = expandedEvents.popLast() { + occurrenceNumber += 1 + + if let endDate, date >= UInt64(endDate.timeIntervalSince1970) { return nil } - while exclusionNumber < repeatRule.excludedDates.count && repeatRule.excludedDates[exclusionNumber] < occurrenceDate { exclusionNumber += 1 } - if exclusionNumber < repeatRule.excludedDates.count && repeatRule.excludedDates[exclusionNumber] == occurrenceDate { return self.next() } - return occurrence + while exclusionNumber < repeatRule.excludedDates.count && UInt64(repeatRule.excludedDates[exclusionNumber].timeIntervalSince1970) < date { + exclusionNumber += 1 + } + if exclusionNumber < repeatRule.excludedDates.count && UInt64(repeatRule.excludedDates[exclusionNumber].timeIntervalSince1970) == date { + return self.next() + } + + return EventOccurrence(occurrenceNumber: occurrenceNumber, occurenceDate: Date(timeIntervalSince1970: Double(date))) + } else { + return self.next() } } } @@ -174,3 +208,14 @@ private func calendarUnit(for repeatPeriod: RepeatPeriod) -> Calendar.Component case .annually: return .year } } + +private extension RepeatPeriod { + func toSDKPeriod() -> tutasdk.RepeatPeriod { + switch self { + case .annually: return tutasdk.RepeatPeriod.annually + case .daily: return tutasdk.RepeatPeriod.daily + case .monthly: return tutasdk.RepeatPeriod.monthly + case .weekly: return tutasdk.RepeatPeriod.weekly + } + } +} diff --git a/app-ios/TutanotaSharedFramework/Alarms/RepeatRule.swift b/app-ios/TutanotaSharedFramework/Alarms/RepeatRule.swift index 76e76cdb7647..46e57ef90300 100644 --- a/app-ios/TutanotaSharedFramework/Alarms/RepeatRule.swift +++ b/app-ios/TutanotaSharedFramework/Alarms/RepeatRule.swift @@ -4,6 +4,7 @@ struct RepeatRule: Equatable { let timeZone: String let endCondition: RepeatEndCondition let excludedDates: [Date] + let advancedRules: [AdvancedRule] } extension RepeatRule { @@ -16,5 +17,13 @@ extension RepeatRule { self.endCondition = RepeatEndCondition(endType: endType, endValue: endValue ?? 0) let decryptedExclusions: [Date] = try encrypted.excludedDates.map { try decrypt(base64: $0.date, key: sessionKey) } self.excludedDates = decryptedExclusions + let advancedRules: [AdvancedRule] = try encrypted.advancedRules.map { + let decryptedType: String = try decrypt(base64: $0.ruleType, key: sessionKey) + let type = try ByRuleType(value: decryptedType) + + let interval: String = try decrypt(base64: $0.interval, key: sessionKey) + return AdvancedRule(ruleType: type, interval: interval) + } + self.advancedRules = advancedRules } } diff --git a/app-ios/TutanotaSharedFramework/Notifications/EncryptedRepeatRule.swift b/app-ios/TutanotaSharedFramework/Notifications/EncryptedRepeatRule.swift index c9e454a7f538..f2a4dec2eafc 100644 --- a/app-ios/TutanotaSharedFramework/Notifications/EncryptedRepeatRule.swift +++ b/app-ios/TutanotaSharedFramework/Notifications/EncryptedRepeatRule.swift @@ -1,4 +1,5 @@ import Foundation +import tutasdk public enum RepeatPeriod: Int, SimpleStringDecodable { case daily = 0 @@ -29,7 +30,60 @@ public enum RepeatEndCondition: Equatable { } } +public enum ByRuleType: String, Codable, Equatable, SimpleStringDecodable { + case byminute + case byhour + case byday + case bymonthday + case byyearday + case byweekno + case bymonth + case bysetpos + case wkst + + public init(value: String) throws { + switch value { + case "0": self = .byminute + case "1": self = .byhour + case "2": self = .byday + case "3": self = .bymonthday + case "4": self = .byyearday + case "5": self = .byweekno + case "6": self = .bymonth + case "7": self = .bysetpos + case "8": self = .wkst + default: throw TUTErrorFactory.createError("Invalid ByRuleType") + } + } +} + public struct EncryptedDateWrapper: Codable, Hashable { public let date: Base64 } +public struct EncryptedAdvancedRuleWrapper: Codable, Hashable { + public let ruleType: String + public let interval: String +} +public struct AdvancedRule: Codable, Hashable { + public let ruleType: ByRuleType + public let interval: String + + public func toSDKRule() -> ByRule { ByRule(byRule: self.ruleType.toSDKType(), interval: self.interval) } +} + +extension ByRuleType { + func toSDKType() -> tutasdk.ByRuleType { + switch self { + case .byminute: return tutasdk.ByRuleType.byminute + case .byhour: return tutasdk.ByRuleType.byhour + case .byday: return tutasdk.ByRuleType.byday + case .bymonth: return tutasdk.ByRuleType.bymonth + case .bymonthday: return tutasdk.ByRuleType.bymonthday + case .byyearday: return tutasdk.ByRuleType.byyearday + case .byweekno: return tutasdk.ByRuleType.byweekno + case .bysetpos: return tutasdk.ByRuleType.bysetpos + case .wkst: return tutasdk.ByRuleType.wkst + } + } +} public struct EncryptedRepeatRule: Codable, Hashable { public let frequency: Base64 @@ -38,6 +92,7 @@ public struct EncryptedRepeatRule: Codable, Hashable { public let endType: Base64 public let endValue: Base64? public let excludedDates: [EncryptedDateWrapper] + public let advancedRules: [EncryptedAdvancedRuleWrapper] public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -47,6 +102,12 @@ public struct EncryptedRepeatRule: Codable, Hashable { self.excludedDates = [EncryptedDateWrapper]() } + if let advancedRules = try container.decodeIfPresent([EncryptedAdvancedRuleWrapper].self, forKey: .advancedRules) { + self.advancedRules = advancedRules + } else { + self.advancedRules = [EncryptedAdvancedRuleWrapper]() + } + self.frequency = try container.decode(Base64.self, forKey: .frequency) self.interval = try container.decode(Base64.self, forKey: .interval) self.timeZone = try container.decode(Base64.self, forKey: .timeZone) diff --git a/app-ios/TutanotaSharedFramework/Utils/Utils.swift b/app-ios/TutanotaSharedFramework/Utils/Utils.swift index b01d9d83c8f4..c02fbe5f592b 100644 --- a/app-ios/TutanotaSharedFramework/Utils/Utils.swift +++ b/app-ios/TutanotaSharedFramework/Utils/Utils.swift @@ -5,12 +5,12 @@ public func translate(_ key: String, default defaultValue: String) -> String { } // // keep in sync with src/native/main/NativePushServiceApp.ts -let SYS_MODEL_VERSION = 99 +let SYS_MODEL_VERSION = 118 // api/entities/tutanota/ModelInfo.ts // FIXME there are at least 5 places needs manual sync for these version numbers. // Definitely need a script to automate. -public let TUTANOTA_MODEL_VERSION: UInt32 = 71 +public let TUTANOTA_MODEL_VERSION: UInt32 = 80 public func addSystemModelHeaders(to headers: inout [String: String]) { headers["v"] = String(SYS_MODEL_VERSION) } public func addTutanotaModelHeaders(to headers: inout [String: String]) { headers["v"] = String(TUTANOTA_MODEL_VERSION) } From c08d56f3385a395c8d4835158dd2bb55743b58d6 Mon Sep 17 00:00:00 2001 From: mup Date: Tue, 14 Jan 2025 13:55:10 +0100 Subject: [PATCH 23/29] Fixes BYSETPOS on Web/Desktop --- src/common/calendar/date/CalendarUtils.ts | 32 +++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 2bc2bf6845a8..8f8e81506445 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -880,6 +880,9 @@ const MAX_EVENT_ITERATIONS = 10000 * add the days the given {@param event} is happening on during the given {@param range} to {@param daysToEvents}. * * ignores repeat rules. + * @param daysToEvents + * @param event + * @param range * @param zone */ export function addDaysForEventInstance(daysToEvents: Map>, event: CalendarEvent, range: CalendarTimeRange, zone: string) { @@ -916,7 +919,7 @@ export function addDaysForEventInstance(daysToEvents: Map Number(value) < -366) || @@ -993,7 +995,6 @@ export function addDaysForRecurringEvent( ? repeatRule.excludedDates.map(({ date }) => createDateWrapper({ date: getAllDayDateForTimezone(date, timeZone) })) : repeatRule.excludedDates const generatedEvents = generateEventOccurrences(event, timeZone, new Date(range.end)) - const allEvents: Map = new Map() for (const { startTime, endTime } of generatedEvents) { if (startTime.getTime() > range.end) break @@ -1010,20 +1011,9 @@ export function addDaysForRecurringEvent( eventClone.startTime = new Date(startTime) eventClone.endTime = new Date(endTime) } - allEvents.set(eventClone.startTime.getTime(), eventClone) - } - } - const setPosRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYSETPOS) - const setPosRulesValues = setPosRules.map((rule) => rule.interval) - const shouldApplySetPos = isNotEmpty(setPosRules) && setPosRules.length < repeatRule.advancedRules.length - let eventCount = 0 - const events = Array.from(allEvents.values()) - for (const event of events) { - if (shouldApplySetPos && !bySetPosContainsEventOccurance(setPosRulesValues, event, ++eventCount, events)) { - continue + addDaysForEventInstance(daysToEvents, eventClone, range, timeZone) } - addDaysForEventInstance(daysToEvents, event, range, timeZone) } } @@ -1239,12 +1229,22 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa validMonths as MonthNumbers[], eventStartTime, ) + + const setPosRules = repeatRule.advancedRules.filter((rule) => rule.ruleType === ByRule.BYSETPOS) + const setPosRulesValues = setPosRules.map((rule) => rule.interval) + const shouldApplySetPos = isNotEmpty(setPosRules) && setPosRules.length < repeatRule.advancedRules.length + let eventCount = 0 + for (const event of events) { const newStartTime = event.toJSDate() const newEndTime = allDay ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) : DateTime.fromJSDate(newStartTime).plus(calcDuration).toJSDate() + if (shouldApplySetPos && !bySetPosContainsEventOccurance(setPosRulesValues, downcast(repeatRule?.frequency), ++eventCount, events)) { + continue + } + assertDateIsValid(newStartTime) assertDateIsValid(newEndTime) yield { startTime: newStartTime, endTime: newEndTime } From 5a7b97daa0380f64333249567a21a2aadd768cb1 Mon Sep 17 00:00:00 2001 From: mup Date: Tue, 14 Jan 2025 14:49:40 +0100 Subject: [PATCH 24/29] [Desktop] Integrates event expansion during alarm scheduling --- src/common/calendar/date/AlarmScheduler.ts | 6 +-- src/common/calendar/date/CalendarUtils.ts | 58 +++++++++++++++------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/common/calendar/date/AlarmScheduler.ts b/src/common/calendar/date/AlarmScheduler.ts index 5a462cf12bb4..0f4e0ce23bc5 100644 --- a/src/common/calendar/date/AlarmScheduler.ts +++ b/src/common/calendar/date/AlarmScheduler.ts @@ -41,13 +41,9 @@ export class AlarmScheduler { repeatTimeZone, event.startTime, event.endTime, - downcast(repeatRule.frequency), - Number(repeatRule.interval), - downcast(repeatRule.endType) || EndType.Never, - Number(repeatRule.endValue), - repeatRule.excludedDates.map(({ date }) => date), parseAlarmInterval(alarmInfo.trigger), calculationLocalZone, + repeatRule ) if (nextOccurrence) { diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 8f8e81506445..d9f9968ff27e 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -35,13 +35,14 @@ import { CalendarEventTypeRef, CalendarGroupRoot, CalendarRepeatRule, + createCalendarEvent, createCalendarRepeatRule, GroupSettings, UserSettingsGroupRoot, } from "../../api/entities/tutanota/TypeRefs.js" import { CalendarEventTimes, DAYS_SHIFTED_MS, generateEventElementId, isAllDayEvent, isAllDayEventByTimes } from "../../api/common/utils/CommonCalendarUtils" import { CalendarAdvancedRepeatRule, createDateWrapper, DateWrapper, GroupInfo, RepeatRule, User } from "../../api/entities/sys/TypeRefs.js" -import { isSameId } from "../../api/common/utils/EntityUtils" +import { isSameId, StrippedEntity } from "../../api/common/utils/EntityUtils" import type { Time } from "./Time.js" import { CalendarInfo } from "../../../calendar-app/calendar/model/CalendarModel" import { DateProvider } from "../../api/common/DateProvider" @@ -1339,38 +1340,59 @@ export function findNextAlarmOccurrence( timeZone: string, eventStart: Date, eventEnd: Date, - frequency: RepeatPeriod, - interval: number, - endType: EndType, - endValue: number, - exclusions: Array, alarmTrigger: AlarmInterval, localTimeZone: string, + repeatRule: RepeatRule, ): AlarmOccurrence | null { let occurrenceNumber = 0 + const exclusions = repeatRule.excludedDates.map(({ date }) => date) const isAllDayEvent = isAllDayEventByTimes(eventStart, eventEnd) const calcEventStart = isAllDayEvent ? getAllDayDateForTimezone(eventStart, localTimeZone) : eventStart assertDateIsValid(calcEventStart) - const endDate = endType === EndType.UntilDate ? (isAllDayEvent ? getAllDayDateForTimezone(new Date(endValue), localTimeZone) : new Date(endValue)) : null - while (endType !== EndType.Count || occurrenceNumber < endValue) { - const occurrenceDate = incrementByRepeatPeriod(calcEventStart, frequency, interval * occurrenceNumber, isAllDayEvent ? localTimeZone : timeZone) - if (endDate && occurrenceDate.getTime() >= endDate.getTime()) { + const endDate = + repeatRule.endType === EndType.UntilDate + ? isAllDayEvent + ? getAllDayDateForTimezone(new Date(Number(repeatRule.endValue)), localTimeZone) + : new Date(Number(repeatRule.endValue)) + : null + + while (repeatRule.endType !== EndType.Count || occurrenceNumber < Number(repeatRule.endValue)) { + const maxDate = incrementByRepeatPeriod( + calcEventStart, + downcast(repeatRule.frequency), + Number(repeatRule.interval) * (occurrenceNumber + 1), + isAllDayEvent ? localTimeZone : timeZone, + ) + + if (endDate && maxDate.getTime() >= endDate.getTime()) { return null } - if (!exclusions.some((d) => d.getTime() === occurrenceDate.getTime())) { - const alarmTime = calculateAlarmTime(occurrenceDate, alarmTrigger, localTimeZone) + const eventGenerator = generateEventOccurrences( + createCalendarEvent({ + startTime: eventStart, + endTime: eventEnd, + repeatRule, + } as StrippedEntity), + timeZone, + maxDate, + ) + + for (const { startTime, endTime } of eventGenerator) { + if (!exclusions.some((d) => d.getTime() === startTime.getTime())) { + const alarmTime = calculateAlarmTime(startTime, alarmTrigger, localTimeZone) - if (alarmTime >= now) { - return { - alarmTime, - occurrenceNumber: occurrenceNumber, - eventTime: occurrenceDate, + if (alarmTime >= now) { + return { + alarmTime, + occurrenceNumber: occurrenceNumber, + eventTime: startTime, + } } } + occurrenceNumber++ } - occurrenceNumber++ } return null } From 768599d1db977c34cfc73d2ce5d128f9a63a9fba Mon Sep 17 00:00:00 2001 From: Murilo Pereira <34790144+murilopereirame@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:57:37 +0100 Subject: [PATCH 25/29] [iOS] Adds BYSETPOS handling during alarm schedule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Dias --- .../Alarms/AlarmModel.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app-ios/TutanotaSharedFramework/Alarms/AlarmModel.swift b/app-ios/TutanotaSharedFramework/Alarms/AlarmModel.swift index 645d47628083..d3622639dc84 100644 --- a/app-ios/TutanotaSharedFramework/Alarms/AlarmModel.swift +++ b/app-ios/TutanotaSharedFramework/Alarms/AlarmModel.swift @@ -146,6 +146,24 @@ private struct LazyEventSequence: Sequence, IteratorProtocol { intervalNumber += 1 } + let setPosRules = repeatRule.advancedRules.filter { item in item.ruleType == ByRuleType.bysetpos } + .map { rule in + let parsedInterval = Int(string: rule.interval)! + + if parsedInterval < 0 { return expandedEvents.count - abs(parsedInterval) } + + return parsedInterval - 1 + } + .filter { interval in interval >= 0 && interval < repeatRule.frequency.getMaxDaysInPeriod() } + + self.expandedEvents = expandedEvents.enumerated() + .filter { (index, _) in + if !setPosRules.isEmpty && !setPosRules.contains(index) { return false } + + return true + } + .map { (_, event) in event } + if let date = expandedEvents.popLast() { occurrenceNumber += 1 @@ -154,6 +172,7 @@ private struct LazyEventSequence: Sequence, IteratorProtocol { while exclusionNumber < repeatRule.excludedDates.count && UInt64(repeatRule.excludedDates[exclusionNumber].timeIntervalSince1970) < date { exclusionNumber += 1 } + if exclusionNumber < repeatRule.excludedDates.count && UInt64(repeatRule.excludedDates[exclusionNumber].timeIntervalSince1970) == date { return self.next() } @@ -218,4 +237,13 @@ private extension RepeatPeriod { case .weekly: return tutasdk.RepeatPeriod.weekly } } + + func getMaxDaysInPeriod() -> Int { + switch self { + case .annually: return 366 + case .monthly: return 31 + case .weekly: return 7 + case .daily: return 1 + } + } } From 907342368f0e6b777104877b49fab88a6b51e50d Mon Sep 17 00:00:00 2001 From: mup Date: Wed, 15 Jan 2025 14:35:23 +0100 Subject: [PATCH 26/29] [Android] Fixes BYSETPOS during alarm schedule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Dias --- .../java/de/tutao/tutashared/alarms/AlarmModel.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmModel.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmModel.kt index 2a0ec3e18a9d..baa3f22dd7bb 100644 --- a/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmModel.kt +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmModel.kt @@ -9,6 +9,7 @@ import java.time.Instant import java.util.Calendar import java.util.Date import java.util.TimeZone +import kotlin.math.abs object AlarmModel { private const val OCCURRENCES_SCHEDULED_AHEAD = 10 @@ -81,11 +82,11 @@ object AlarmModel { // and ensures that 0 <= abs(SETPOS) < eventCount val parsedSetPos = setPosRules.map { if (it.interval.toInt() < 0) { - expandedEvents.count() - it.interval.toInt() + expandedEvents.count() - abs(it.interval.toInt()) } else { it.interval.toInt() - 1 } - }.filter { it < expandedEvents.count() && it >= 0 } + }.filter { it >= 0 && it < frequency.getMaxDaysInPeriod() } if (endType == EndType.UNTIL && calendar.timeInMillis >= endDate!!.time) { break @@ -211,4 +212,13 @@ object AlarmModel { RepeatPeriod.ANNUALLY -> de.tutao.tutasdk.RepeatPeriod.ANNUALLY } } + + private fun RepeatPeriod.getMaxDaysInPeriod() = run { + when (this) { + RepeatPeriod.DAILY -> 1 + RepeatPeriod.WEEKLY -> 7 + RepeatPeriod.MONTHLY -> 31 + RepeatPeriod.ANNUALLY -> 366 + } + } } \ No newline at end of file From ad422946dbefa58ec2dc8484d6ce1ab7ffcf2f0a Mon Sep 17 00:00:00 2001 From: mup Date: Thu, 16 Jan 2025 07:32:47 +0100 Subject: [PATCH 27/29] Adds info banner for unsupported rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some rules aren't supported during event creation/editing. This commit adds an info banner warning about losing rules if user edits and event with unsupported rules. Co-authored-by: André Dias --- .../CalendarEventWhenModel.ts | 6 +++- .../gui/eventeditor-view/RepeatRuleEditor.ts | 20 +++++++++++- src/common/misc/TranslationKey.ts | 29 +++++++++++++++++ src/mail-app/translations/en.ts | 31 +++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/calendar-app/calendar/gui/eventeditor-model/CalendarEventWhenModel.ts b/src/calendar-app/calendar/gui/eventeditor-model/CalendarEventWhenModel.ts index 381b0851bef0..1a12b90e59e1 100644 --- a/src/calendar-app/calendar/gui/eventeditor-model/CalendarEventWhenModel.ts +++ b/src/calendar-app/calendar/gui/eventeditor-model/CalendarEventWhenModel.ts @@ -19,7 +19,7 @@ import { incrementByRepeatPeriod, } from "../../../../common/calendar/date/CalendarUtils.js" import { assertNotNull, clone, filterInt, incrementDate, noOp, TIMESTAMP_ZERO_YEAR } from "@tutao/tutanota-utils" -import { CalendarEvent, CalendarRepeatRule } from "../../../../common/api/entities/tutanota/TypeRefs.js" +import { AdvancedRepeatRule, CalendarEvent, CalendarRepeatRule } from "../../../../common/api/entities/tutanota/TypeRefs.js" import { Stripped } from "../../../../common/api/common/utils/EntityUtils.js" import { EndType, RepeatPeriod } from "../../../../common/api/common/TutanotaConstants.js" import { createDateWrapper, createRepeatRule, RepeatRule } from "../../../../common/api/entities/sys/TypeRefs.js" @@ -256,6 +256,10 @@ export class CalendarEventWhenModel { return this.repeatRule ? (this.repeatRule.frequency as RepeatPeriod) : null } + get advancedRules(): AdvancedRepeatRule[] { + return this.repeatRule?.advancedRules ?? [] + } + set repeatPeriod(repeatPeriod: RepeatPeriod | null) { if (this.repeatRule?.frequency === repeatPeriod) { // repeat null => we will return if repeatPeriod is null diff --git a/src/calendar-app/calendar/gui/eventeditor-view/RepeatRuleEditor.ts b/src/calendar-app/calendar/gui/eventeditor-view/RepeatRuleEditor.ts index 0d695a47ef2f..4c22beb35ad2 100644 --- a/src/calendar-app/calendar/gui/eventeditor-view/RepeatRuleEditor.ts +++ b/src/calendar-app/calendar/gui/eventeditor-view/RepeatRuleEditor.ts @@ -15,6 +15,9 @@ import stream from "mithril/stream" import { Divider } from "../../../../common/gui/Divider.js" import { theme } from "../../../../common/gui/theme.js" import { isApp } from "../../../../common/api/common/Env.js" +import { ByRule } from "../../../../common/calendar/import/ImportExportUtils.js" +import { BannerType, InfoBanner, InfoBannerAttrs } from "../../../../common/gui/base/InfoBanner.js" +import { Icons } from "../../../../common/gui/base/icons/Icons.js" export type RepeatRuleEditorAttrs = { model: CalendarEventWhenModel @@ -30,7 +33,7 @@ export class RepeatRuleEditor implements Component { private repeatInterval: number = 0 private intervalOptions: stream = stream([]) private intervalExpanded: boolean = false - + private hasUnsupportedRules: boolean = false private numberValues: IntervalOption[] = createIntervalValues() private occurrencesOptions: stream = stream([]) @@ -47,6 +50,11 @@ export class RepeatRuleEditor implements Component { this.repeatInterval = attrs.model.repeatInterval this.repeatOccurrences = attrs.model.repeatEndOccurrences + this.hasUnsupportedRules = attrs.model.advancedRules.some((rule) => { + const isValidRule = + (attrs.model.repeatPeriod === RepeatPeriod.WEEKLY || attrs.model.repeatPeriod === RepeatPeriod.MONTHLY) && rule.ruleType === ByRule.BYDAY + return !isValidRule + }) } private getRepeatType(period: RepeatPeriod, interval: number, endTime: EndType) { @@ -57,6 +65,15 @@ export class RepeatRuleEditor implements Component { return period } + private renderUnsupportedAdvancedRulesWarning(): Children { + return m(InfoBanner, { + message: () => m(".small.selectable", lang.get("unsupportedAdvancedRules_msg")), + icon: Icons.Sync, + type: BannerType.Info, + buttons: [], + } satisfies InfoBannerAttrs) + } + view({ attrs }: Vnode): Children { const customRuleOptions = customFrequenciesOptions.map((option) => ({ ...option, @@ -72,6 +89,7 @@ export class RepeatRuleEditor implements Component { }, }, [ + this.hasUnsupportedRules ? this.renderUnsupportedAdvancedRulesWarning() : null, m( Card, { diff --git a/src/common/misc/TranslationKey.ts b/src/common/misc/TranslationKey.ts index 82836cb91d70..66756a713a61 100644 --- a/src/common/misc/TranslationKey.ts +++ b/src/common/misc/TranslationKey.ts @@ -1889,6 +1889,35 @@ export type TranslationKeyType = | "yourMessage_label" | "you_label" | "emptyString_msg" + | "monday_label" + | "tuesday_label" + | "wednesday_label" + | "thursday_label" + | "friday_label" + | "saturday_label" + | "sunday_label" + | "january_label" + | "february_label" + | "march_label" + | "april_label" + | "may_label" + | "june_label" + | "july_label" + | "august_label" + | "september_label" + | "october_label" + | "november_label" + | "december_label" + | "firstOfPeriod_label" + | "lastOfPeriod_label" + | "onNDayOfPeriod_label" + | "inMonths_label" + | "onDays_label" + | "inWeek_label" + | "afterStartOfPeriod_label" + | "and_label" + | "beforeEndOfPeriod_label" + | "unsupportedAdvancedRules_msg" // Put in temporarily, will be removed soon | "localAdminGroup_label" | "assignAdminRightsToLocallyAdministratedUserError_msg" diff --git a/src/mail-app/translations/en.ts b/src/mail-app/translations/en.ts index 0e8e115f8e3d..dba7c0023c2c 100644 --- a/src/mail-app/translations/en.ts +++ b/src/mail-app/translations/en.ts @@ -1905,6 +1905,37 @@ export default { "yourFolders_action": "YOUR FOLDERS", "yourMessage_label": "Your message", "you_label": "You", + "monday_label": "Monday", + "tuesday_label": "Tuesday", + "wednesday_label": "Wednesday", + "thursday_label": "Thursday", + "friday_label": "Friday", + "saturday_label": "Saturday", + "sunday_label": "Sunday", + "january_label": "January", + "february_label": "February", + "march_label": "March", + "april_label": "April", + "may_label": "May", + "june_label": "June", + "july_label": "July", + "august_label": "August", + "september_label": "September", + "october_label": "October", + "november_label": "November", + "december_label": "December", + "firstOfPeriod_label": "First {day} of the {period}", + "lastOfPeriod_label": "Last {day} of the {period}", + "onNDayOfPeriod_label": "on {day} of {period}", + "theYear_label": "the Year", + "theMonth_label": "the Month", + "inMonths_label": "in {months}", + "onDays_label": "on {days}", + "inWeek_label": "in weeks {weeks}", + "afterStartOfPeriod_label": "{days} occurrence after start of the {period}", + "and_label": "and", + "beforeEndOfPeriod_label": "{days} occurrence before end of the {period}", + "unsupportedAdvancedRules_msg": "This event contains one or more unsupported Advanced Recurrence Rules, any changes will result in the loss of these rules", // Put in temporarily, will be removed soon "localAdminGroup_label": "Local admin group", "assignAdminRightsToLocallyAdministratedUserError_msg": "You can't assign global admin rights to a locally administrated user.", From 7400adf509e0d70fd2e2fb2337a83336698170b5 Mon Sep 17 00:00:00 2001 From: Murilo Pereira <34790144+murilopereirame@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:08:44 +0100 Subject: [PATCH 28/29] Adds translations to Advanced Repeat Rules Co-authored-by: pas --- .../calendar/export/CalendarExporter.ts | 8 +- .../gui/eventpopup/EventPreviewView.ts | 111 +++++++++++++++++- src/common/calendar/date/AlarmScheduler.ts | 2 +- src/common/misc/TranslationKey.ts | 21 +--- src/mail-app/translations/de.ts | 15 +++ src/mail-app/translations/de_sie.ts | 15 +++ src/mail-app/translations/en.ts | 30 ++--- 7 files changed, 154 insertions(+), 48 deletions(-) diff --git a/src/calendar-app/calendar/export/CalendarExporter.ts b/src/calendar-app/calendar/export/CalendarExporter.ts index 82a3748d4e30..7f63569a2207 100644 --- a/src/calendar-app/calendar/export/CalendarExporter.ts +++ b/src/calendar-app/calendar/export/CalendarExporter.ts @@ -113,13 +113,13 @@ function serializeAdvancedRepeatRules(advancedRules: CalendarAdvancedRepeatRule[ if (isNotEmpty(advancedRules)) { const BYRULES = new Map() const byRuleValueToKey = reverse(ByRule) - advancedRules.forEach((r) => { + for (const r of advancedRules) { const type = byRuleValueToKey[r.ruleType as ByRule] BYRULES.set(type, BYRULES.get(type) ? `${BYRULES.get(type)},${r.interval}` : r.interval) - }) - BYRULES.forEach((interval, type) => { + } + for (const [interval, type] of BYRULES) { advancedRepeatRules += `;${type.toUpperCase()}=${interval}` - }) + } } return advancedRepeatRules diff --git a/src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts b/src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts index 6b59837d303b..5d9c0ff4fb51 100644 --- a/src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts +++ b/src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts @@ -1,4 +1,10 @@ -import type { CalendarEvent, CalendarEventAttendee, CalendarRepeatRule, EncryptedMailAddress } from "../../../../common/api/entities/tutanota/TypeRefs.js" +import type { + AdvancedRepeatRule, + CalendarEvent, + CalendarEventAttendee, + CalendarRepeatRule, + EncryptedMailAddress, +} from "../../../../common/api/entities/tutanota/TypeRefs.js" import { createCalendarEventAttendee, createEncryptedMailAddress } from "../../../../common/api/entities/tutanota/TypeRefs.js" import m, { Children, Component, Vnode } from "mithril" import { AllIcons, Icon, IconSize } from "../../../../common/gui/base/Icon.js" @@ -21,6 +27,7 @@ import { ExternalLink } from "../../../../common/gui/base/ExternalLink.js" import { createRepeatRuleFrequencyValues, formatEventDuration, getDisplayEventTitle, iconForAttendeeStatus } from "../CalendarGuiUtils.js" import { hasError } from "../../../../common/api/common/utils/ErrorUtils.js" +import { ByRule } from "../../../../common/calendar/import/ImportExportUtils.js" export type EventPreviewViewAttrs = { event: Omit @@ -229,20 +236,95 @@ export function formatRepetitionFrequency(repeatRule: RepeatRule): string | null const frequency = createRepeatRuleFrequencyValues().find((frequency) => frequency.value === repeatRule.frequency) if (frequency) { - return frequency.name + const freq = frequency.name + const readable = buildReadableAdvancedRepetitionRule(repeatRule.advancedRules, downcast(repeatRule.frequency)) + + return `${freq}. ${readable}`.trim() } } else { - return lang.get("repetition_msg", { + const repeatMessage = lang.get("repetition_msg", { "{interval}": repeatRule.interval, "{timeUnit}": getFrequencyTimeUnit(downcast(repeatRule.frequency)), }) + + const advancedRule = buildReadableAdvancedRepetitionRule(repeatRule.advancedRules, downcast(repeatRule.frequency)) + + return `${repeatMessage}. ${advancedRule}`.trim() } return null } +function buildReadableAdvancedRepetitionRule(advancedRule: AdvancedRepeatRule[], frequency: RepeatPeriod): string { + const hasInvalidRules = advancedRule.some( + (rule) => !((frequency === RepeatPeriod.WEEKLY || frequency === RepeatPeriod.MONTHLY) && rule.ruleType === ByRule.BYDAY), + ) + + let translationKey: TranslationKey = "withCustomRules_label" + if (hasInvalidRules) { + return lang.get(translationKey) + } + + const days: string[] = [] + + for (const item of advancedRule) { + switch (item.ruleType) { + case ByRule.BYDAY: + days.push(item.interval) + break + default: + return lang.get(translationKey) + } + } + + if (days.length === 0) return "" + + if (frequency === RepeatPeriod.MONTHLY) { + const ruleRegex = /^([-+]?\d{0,3})([a-zA-Z]{2})?$/g + + const parsedRuleValue = Array.from(days[0].matchAll(ruleRegex)).flat() + + const day = parseShortDay(parsedRuleValue[2] ?? "") + const leadingValue = Number.parseInt(parsedRuleValue[1]) + + if (leadingValue === 1) { + translationKey = "firstOfPeriod_label" + } else if (leadingValue === 2) { + translationKey = "secondOfPeriod_label" + } else if (leadingValue === -1) { + translationKey = "lastOfPeriod_label" + } else if (!Number.isNaN(leadingValue)) { + translationKey = "nthOfPeriod_label" + } + + return lang.get(translationKey, { + "{days}": day, + "{n}": leadingValue, + }) + } + + return lang.get("onDays_label", { + "{days}": joinWithAnd( + days.map((day) => parseShortDay(day)), + ", ", + lang.get("and_label"), + ), + }) +} + +function joinWithAnd(items: any[], separator: string, lastSeparator: string) { + if (items.length > 1) { + const last = items.pop() + const joinedString = items.join(separator) + + return `${joinedString} ${lastSeparator} ${last}` + } + + return items.join(separator) +} + /** - * @returns {string} The returned string includes a leading separator (", " or " "). + * @returns {string} The returned string includes a leading separator (", " or ""). */ export function formatRepetitionEnd(repeatRule: RepeatRule, isAllDay: boolean): string { switch (repeatRule.endType) { @@ -287,6 +369,27 @@ function getFrequencyTimeUnit(frequency: RepeatPeriod): string { } } +function parseShortDay(day: string) { + switch (day) { + case "MO": + return lang.get("monday_label") + case "TU": + return lang.get("tuesday_label") + case "WE": + return lang.get("wednesday_label") + case "TH": + return lang.get("thursday_label") + case "FR": + return lang.get("friday_label") + case "SA": + return lang.get("saturday_label") + case "SU": + return lang.get("sunday_label") + default: + return "" + } +} + function prepareAttendees(attendees: Array, organizer: EncryptedMailAddress | null): Array { // We copy the attendees array so that we can add the organizer, in the case that they are not already in attendees // This is just for display purposes. We need to copy because event.attendees is the source of truth for the event diff --git a/src/common/calendar/date/AlarmScheduler.ts b/src/common/calendar/date/AlarmScheduler.ts index 0f4e0ce23bc5..ee3c3f8068e4 100644 --- a/src/common/calendar/date/AlarmScheduler.ts +++ b/src/common/calendar/date/AlarmScheduler.ts @@ -43,7 +43,7 @@ export class AlarmScheduler { event.endTime, parseAlarmInterval(alarmInfo.trigger), calculationLocalZone, - repeatRule + repeatRule, ) if (nextOccurrence) { diff --git a/src/common/misc/TranslationKey.ts b/src/common/misc/TranslationKey.ts index 66756a713a61..9de1a41c04e4 100644 --- a/src/common/misc/TranslationKey.ts +++ b/src/common/misc/TranslationKey.ts @@ -1896,27 +1896,14 @@ export type TranslationKeyType = | "friday_label" | "saturday_label" | "sunday_label" - | "january_label" - | "february_label" - | "march_label" - | "april_label" - | "may_label" - | "june_label" - | "july_label" - | "august_label" - | "september_label" - | "october_label" - | "november_label" - | "december_label" | "firstOfPeriod_label" + | "secondOfPeriod_label" + | "thirdOfPeriod_label" + | "nthOfPeriod_label" | "lastOfPeriod_label" - | "onNDayOfPeriod_label" - | "inMonths_label" | "onDays_label" - | "inWeek_label" - | "afterStartOfPeriod_label" | "and_label" - | "beforeEndOfPeriod_label" + | "withCustomRules_label" | "unsupportedAdvancedRules_msg" // Put in temporarily, will be removed soon | "localAdminGroup_label" diff --git a/src/mail-app/translations/de.ts b/src/mail-app/translations/de.ts index e22af8128520..81f44faaa9b6 100644 --- a/src/mail-app/translations/de.ts +++ b/src/mail-app/translations/de.ts @@ -1909,6 +1909,21 @@ export default { "yourFolders_action": "DEINE ORDNER", "yourMessage_label": "Deine Nachricht", "you_label": "Du", + "monday_label": "Montag", + "tuesday_label": "Dienstag", + "wednesday_label": "Mittwoch", + "thursday_label": "Donnerstag", + "friday_label": "Freitag", + "saturday_label": "Samstag", + "sunday_label": "Sonntag", + "and_label": "und", + "firstOfPeriod_label": "Am ersten {day} des Monats", + "secondOfPeriod_label": "Am zweiten {day} des Monats", + "thirdOfPeriod_label": "Am dritten {day} des Monats", + "nthOfPeriod_label": "Am {n}-ten {day} des Monats", + "lastOfPeriod_label": "Am letzten {day} des Monats", + "withCustomRules_label": "Mit benutzerdefinierten Wiederholungsregeln", + "unsupportedAdvancedRules_msg": "Dieses Ereignis enthält eine oder mehrere nicht unterstützte erweiterte Wiederholungsregeln; jede Änderung führt zum Verlust dieser Regeln.", // Put in temporarily, will be removed soon "localAdminGroup_label": "Local admin group", "assignAdminRightsToLocallyAdministratedUserError_msg": "You can't assign global admin rights to a locally administrated user.", diff --git a/src/mail-app/translations/de_sie.ts b/src/mail-app/translations/de_sie.ts index f8fd7fec2f6a..d1617fb3d5fa 100644 --- a/src/mail-app/translations/de_sie.ts +++ b/src/mail-app/translations/de_sie.ts @@ -1909,6 +1909,21 @@ export default { "yourFolders_action": "Ihre ORDNER", "yourMessage_label": "Ihre Nachricht", "you_label": "Sie", + "monday_label": "Montag", + "tuesday_label": "Dienstag", + "wednesday_label": "Mittwoch", + "thursday_label": "Donnerstag", + "friday_label": "Freitag", + "saturday_label": "Samstag", + "sunday_label": "Sonntag", + "and_label": "und", + "firstOfPeriod_label": "Am ersten {day} des Monats", + "secondOfPeriod_label": "Am zweiten {day} des Monats", + "thirdOfPeriod_label": "Am dritten {day} des Monats", + "nthOfPeriod_label": "Am {n}-ten {day} des Monats", + "lastOfPeriod_label": "Am letzten {day} des Monats", + "withCustomRules_label": "Mit benutzerdefinierten Wiederholungsregeln", + "unsupportedAdvancedRules_msg": "Dieses Ereignis enthält eine oder mehrere nicht unterstützte erweiterte Wiederholungsregeln; jede Änderung führt zum Verlust dieser Regeln.", // Put in temporarily, will be removed soon "localAdminGroup_label": "Local admin group", "assignAdminRightsToLocallyAdministratedUserError_msg": "You can't assign global admin rights to a locally administrated user.", diff --git a/src/mail-app/translations/en.ts b/src/mail-app/translations/en.ts index dba7c0023c2c..a8547d9ffae4 100644 --- a/src/mail-app/translations/en.ts +++ b/src/mail-app/translations/en.ts @@ -1912,30 +1912,16 @@ export default { "friday_label": "Friday", "saturday_label": "Saturday", "sunday_label": "Sunday", - "january_label": "January", - "february_label": "February", - "march_label": "March", - "april_label": "April", - "may_label": "May", - "june_label": "June", - "july_label": "July", - "august_label": "August", - "september_label": "September", - "october_label": "October", - "november_label": "November", - "december_label": "December", - "firstOfPeriod_label": "First {day} of the {period}", - "lastOfPeriod_label": "Last {day} of the {period}", - "onNDayOfPeriod_label": "on {day} of {period}", - "theYear_label": "the Year", - "theMonth_label": "the Month", - "inMonths_label": "in {months}", - "onDays_label": "on {days}", - "inWeek_label": "in weeks {weeks}", - "afterStartOfPeriod_label": "{days} occurrence after start of the {period}", + "onDays_label": "On {days}", "and_label": "and", - "beforeEndOfPeriod_label": "{days} occurrence before end of the {period}", + "firstOfPeriod_label": "On first {day} of the month", + "secondOfPeriod_label": "On second {day} of the month", + "thirdOfPeriod_label": "On third {day} of the month", + "nthOfPeriod_label": "On {n}th {day} of the month", + "lastOfPeriod_label": "On last {day} of the month", + "withCustomRules_label": "With custom repeat rules", "unsupportedAdvancedRules_msg": "This event contains one or more unsupported Advanced Recurrence Rules, any changes will result in the loss of these rules", + "beforeEndOfPeriod_label": "{days} occurrence before end of the {period}", // Put in temporarily, will be removed soon "localAdminGroup_label": "Local admin group", "assignAdminRightsToLocallyAdministratedUserError_msg": "You can't assign global admin rights to a locally administrated user.", From e3da4fffaca3b8730e6e6f47b9fe1b038ca04e3d Mon Sep 17 00:00:00 2001 From: mup Date: Mon, 3 Feb 2025 10:28:07 +0100 Subject: [PATCH 29/29] WIP --- app-android/app/build.gradle | 2 +- app-android/calendar/build.gradle.kts | 2 +- .../de/tutao/tutashared/alarms/AlarmModel.kt | 4 +- .../tutashared/alarms/EncryptedRepeatRule.kt | 17 +- .../Notifications/EncryptedRepeatRule.swift | 16 +- .../calendar/export/CalendarExporter.ts | 10 +- .../calendar/export/CalendarParser.ts | 3 +- .../gui/eventeditor-view/RepeatRuleEditor.ts | 2 +- .../gui/eventpopup/EventPreviewView.ts | 3 +- src/common/calendar/date/CalendarUtils.ts | 96 ++-- .../calendar/import/ImportExportUtils.ts | 50 +- .../native/main/NativePushServiceApp.ts | 2 +- src/mail-app/translations/de.ts | 1 + src/mail-app/translations/de_sie.ts | 1 + src/mail-app/translations/en.ts | 1 - test/tests/calendar/CalendarUtilsTest.ts | 40 +- tuta-sdk/rust/Cargo.lock | 2 + tuta-sdk/rust/sdk/src/date/event_facade.rs | 534 ++++++++---------- 18 files changed, 344 insertions(+), 442 deletions(-) diff --git a/app-android/app/build.gradle b/app-android/app/build.gradle index 07b6ededdb08..bc6e7f31d1aa 100644 --- a/app-android/app/build.gradle +++ b/app-android/app/build.gradle @@ -83,7 +83,7 @@ android { buildTypes.each { it.buildConfigField 'String', 'FILE_PROVIDER_AUTHORITY', '"' + it.manifestPlaceholders['contentProviderAuthority'] + '"' // keep in sync with src/native/main/NativePushServiceApp.ts - it.buildConfigField 'String', "SYS_MODEL_VERSION", '"118"' + it.buildConfigField 'String', "SYS_MODEL_VERSION", '"119"' it.buildConfigField 'String', "TUTANOTA_MODEL_VERSION", '"80"' it.buildConfigField 'String', 'RES_ADDRESS', '"tutanota"' } diff --git a/app-android/calendar/build.gradle.kts b/app-android/calendar/build.gradle.kts index fd25022080e3..a3d01124e0b8 100644 --- a/app-android/calendar/build.gradle.kts +++ b/app-android/calendar/build.gradle.kts @@ -123,7 +123,7 @@ android { "\"" + it.manifestPlaceholders["contentProviderAuthority"] + "\"" ) // keep in sync with src/native/main/NativePushServiceApp.ts - it.buildConfigField("String", "SYS_MODEL_VERSION", "\"118\"") + it.buildConfigField("String", "SYS_MODEL_VERSION", "\"119\"") it.buildConfigField("String", "TUTANOTA_MODEL_VERSION", "\"80\"") it.buildConfigField("String", "RES_ADDRESS", "\"tutanota\"") } diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmModel.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmModel.kt index baa3f22dd7bb..f340a0a27433 100644 --- a/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmModel.kt +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/AlarmModel.kt @@ -53,7 +53,7 @@ object AlarmModel { null } val calendar = Calendar.getInstance(if (isAllDayEvent) localTimeZone else timeZone) - val setPosRules = byRules.filter { rule -> rule.byRule == ByRuleType.BYSETPOS } + val setPosRules = byRules.filter { rule -> rule.byRule == ByRuleType.BY_SET_POS } val eventFacade = EventFacade() var occurrences = 0 @@ -69,7 +69,7 @@ object AlarmModel { incrementByRepeatPeriod(calendar, frequency, interval * intervalOccurrences) var expandedEvents: List = eventFacade.generateFutureInstances( - calendar.timeInMillis.toULong(), + (calendar.timeInMillis / 1000).toULong(), EventRepeatRule(frequency.toSdkPeriod(), byRules) ) diff --git a/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/EncryptedRepeatRule.kt b/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/EncryptedRepeatRule.kt index 6f9ee31256c8..63dfb5b9668d 100644 --- a/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/EncryptedRepeatRule.kt +++ b/app-android/tutashared/src/main/java/de/tutao/tutashared/alarms/EncryptedRepeatRule.kt @@ -1,6 +1,5 @@ package de.tutao.tutashared.alarms -import android.util.Log import androidx.room.TypeConverter import androidx.room.TypeConverters import de.tutao.tutasdk.ByRule @@ -85,14 +84,14 @@ fun EncryptedRepeatRule.decrypt(crypto: AndroidNativeCryptoFacade, sessionKey: B fun ByRuleType.Companion.fromValue(value: Int) = run { when (value) { - 0 -> ByRuleType.BYMINUTE - 1 -> ByRuleType.BYHOUR - 2 -> ByRuleType.BYDAY - 3 -> ByRuleType.BYMONTHDAY - 4 -> ByRuleType.BYYEARDAY - 5 -> ByRuleType.BYWEEKNO - 6 -> ByRuleType.BYMONTH - 7 -> ByRuleType.BYSETPOS + 0 -> ByRuleType.BY_MINUTE + 1 -> ByRuleType.BY_HOUR + 2 -> ByRuleType.BY_DAY + 3 -> ByRuleType.BY_MONTHDAY + 4 -> ByRuleType.BY_YEAR_DAY + 5 -> ByRuleType.BY_WEEK_NO + 6 -> ByRuleType.BY_MONTH + 7 -> ByRuleType.BY_SET_POS 8 -> ByRuleType.WKST else -> { diff --git a/app-ios/TutanotaSharedFramework/Notifications/EncryptedRepeatRule.swift b/app-ios/TutanotaSharedFramework/Notifications/EncryptedRepeatRule.swift index f2a4dec2eafc..d94252113ee9 100644 --- a/app-ios/TutanotaSharedFramework/Notifications/EncryptedRepeatRule.swift +++ b/app-ios/TutanotaSharedFramework/Notifications/EncryptedRepeatRule.swift @@ -72,14 +72,14 @@ public struct AdvancedRule: Codable, Hashable { extension ByRuleType { func toSDKType() -> tutasdk.ByRuleType { switch self { - case .byminute: return tutasdk.ByRuleType.byminute - case .byhour: return tutasdk.ByRuleType.byhour - case .byday: return tutasdk.ByRuleType.byday - case .bymonth: return tutasdk.ByRuleType.bymonth - case .bymonthday: return tutasdk.ByRuleType.bymonthday - case .byyearday: return tutasdk.ByRuleType.byyearday - case .byweekno: return tutasdk.ByRuleType.byweekno - case .bysetpos: return tutasdk.ByRuleType.bysetpos + case .byminute: return tutasdk.ByRuleType.byMinute + case .byhour: return tutasdk.ByRuleType.byHour + case .byday: return tutasdk.ByRuleType.byDay + case .bymonth: return tutasdk.ByRuleType.byMonth + case .bymonthday: return tutasdk.ByRuleType.byMonthday + case .byyearday: return tutasdk.ByRuleType.byYearDay + case .byweekno: return tutasdk.ByRuleType.byWeekNo + case .bysetpos: return tutasdk.ByRuleType.bySetPos case .wkst: return tutasdk.ByRuleType.wkst } } diff --git a/src/calendar-app/calendar/export/CalendarExporter.ts b/src/calendar-app/calendar/export/CalendarExporter.ts index 7f63569a2207..bb6dd583b7df 100644 --- a/src/calendar-app/calendar/export/CalendarExporter.ts +++ b/src/calendar-app/calendar/export/CalendarExporter.ts @@ -10,7 +10,7 @@ import { import { assertNotNull, downcast, incrementDate, isNotEmpty, mapAndFilterNull, neverNull, pad, stringToUtf8Uint8Array } from "@tutao/tutanota-utils" import { calendarAttendeeStatusToParstat, iCalReplacements, repeatPeriodToIcalFrequency } from "./CalendarParser" import { getAllDayDateLocal, isAllDayEvent } from "../../../common/api/common/utils/CommonCalendarUtils" -import { AlarmIntervalUnit, generateUid, getTimeZone, parseAlarmInterval } from "../../../common/calendar/date/CalendarUtils" +import { AlarmIntervalUnit, ByRule, generateUid, getTimeZone, parseAlarmInterval } from "../../../common/calendar/date/CalendarUtils" import type { CalendarEvent } from "../../../common/api/entities/tutanota/TypeRefs.js" import { createFile } from "../../../common/api/entities/tutanota/TypeRefs.js" import { convertToDataFile, DataFile } from "../../../common/api/common/DataFile" @@ -18,7 +18,6 @@ import type { CalendarAdvancedRepeatRule, DateWrapper, RepeatRule, UserAlarmInfo import { DateTime } from "luxon" import { getLetId } from "../../../common/api/common/utils/EntityUtils" import { CALENDAR_MIME_TYPE } from "../../../common/file/FileController.js" -import { ByRule } from "../../../common/calendar/import/ImportExportUtils.js" /** create an ical data file that can be attached to an invitation/update/cancellation/response mail */ export function makeInvitationCalendarFile(event: CalendarEvent, method: CalendarMethod, now: Date, zone: string): DataFile { @@ -165,8 +164,11 @@ export function serializeRepeatRule(repeatRule: RepeatRule | null, isAllDayEvent const advancedRepeatRules = serializeAdvancedRepeatRules(repeatRule.advancedRules) return [ - `RRULE:FREQ=${repeatPeriodToIcalFrequency(assertEnumValue(RepeatPeriod, repeatRule.frequency))}` + `;INTERVAL=${repeatRule.interval}` + endType, - ].concat(advancedRepeatRules, excludedDates) + `RRULE:FREQ=${repeatPeriodToIcalFrequency(assertEnumValue(RepeatPeriod, repeatRule.frequency))}` + + `;INTERVAL=${repeatRule.interval}` + + endType + + advancedRepeatRules.trim(), + ].concat(excludedDates) } else { return [] } diff --git a/src/calendar-app/calendar/export/CalendarParser.ts b/src/calendar-app/calendar/export/CalendarParser.ts index e9cb12b1fe33..11dc23497fdc 100644 --- a/src/calendar-app/calendar/export/CalendarParser.ts +++ b/src/calendar-app/calendar/export/CalendarParser.ts @@ -28,10 +28,9 @@ import WindowsZones from "./WindowsZones" import type { ParsedCalendarData } from "../../../common/calendar/import/CalendarImporter.js" import { isMailAddress } from "../../../common/misc/FormatValidator" import { CalendarAttendeeStatus, CalendarMethod, EndType, RepeatPeriod, reverse } from "../../../common/api/common/TutanotaConstants" -import { AlarmInterval, AlarmIntervalUnit } from "../../../common/calendar/date/CalendarUtils.js" +import { AlarmInterval, AlarmIntervalUnit, BYRULE_MAP } from "../../../common/calendar/date/CalendarUtils.js" import { AlarmInfoTemplate } from "../../../common/api/worker/facades/lazy/CalendarFacade.js" import { serializeAlarmInterval } from "../../../common/api/common/utils/CommonCalendarUtils.js" -import { BYRULE_MAP } from "../../../common/calendar/import/ImportExportUtils.js" function parseDateString(dateString: string): { year: number diff --git a/src/calendar-app/calendar/gui/eventeditor-view/RepeatRuleEditor.ts b/src/calendar-app/calendar/gui/eventeditor-view/RepeatRuleEditor.ts index 4c22beb35ad2..babb1980d2b4 100644 --- a/src/calendar-app/calendar/gui/eventeditor-view/RepeatRuleEditor.ts +++ b/src/calendar-app/calendar/gui/eventeditor-view/RepeatRuleEditor.ts @@ -15,9 +15,9 @@ import stream from "mithril/stream" import { Divider } from "../../../../common/gui/Divider.js" import { theme } from "../../../../common/gui/theme.js" import { isApp } from "../../../../common/api/common/Env.js" -import { ByRule } from "../../../../common/calendar/import/ImportExportUtils.js" import { BannerType, InfoBanner, InfoBannerAttrs } from "../../../../common/gui/base/InfoBanner.js" import { Icons } from "../../../../common/gui/base/icons/Icons.js" +import { ByRule } from "../../../../common/calendar/date/CalendarUtils.js" export type RepeatRuleEditorAttrs = { model: CalendarEventWhenModel diff --git a/src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts b/src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts index 5d9c0ff4fb51..8250587e4db8 100644 --- a/src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts +++ b/src/calendar-app/calendar/gui/eventpopup/EventPreviewView.ts @@ -11,7 +11,7 @@ import { AllIcons, Icon, IconSize } from "../../../../common/gui/base/Icon.js" import { theme } from "../../../../common/gui/theme.js" import { BootIcons } from "../../../../common/gui/base/icons/BootIcons.js" import { Icons } from "../../../../common/gui/base/icons/Icons.js" -import { getRepeatEndTimeForDisplay, getTimeZone } from "../../../../common/calendar/date/CalendarUtils.js" +import { ByRule, getRepeatEndTimeForDisplay, getTimeZone } from "../../../../common/calendar/date/CalendarUtils.js" import { CalendarAttendeeStatus, EndType, getAttendeeStatus, RepeatPeriod } from "../../../../common/api/common/TutanotaConstants.js" import { downcast, memoized } from "@tutao/tutanota-utils" import { lang, TranslationKey } from "../../../../common/misc/LanguageViewModel.js" @@ -27,7 +27,6 @@ import { ExternalLink } from "../../../../common/gui/base/ExternalLink.js" import { createRepeatRuleFrequencyValues, formatEventDuration, getDisplayEventTitle, iconForAttendeeStatus } from "../CalendarGuiUtils.js" import { hasError } from "../../../../common/api/common/utils/ErrorUtils.js" -import { ByRule } from "../../../../common/calendar/import/ImportExportUtils.js" export type EventPreviewViewAttrs = { event: Omit diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index d9f9968ff27e..6f9d935fece3 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -6,6 +6,7 @@ import { downcast, filterInt, findAllAndRemove, + freezeMap, getFirstOrThrow, getFromMap, getStartOfDay, @@ -51,7 +52,6 @@ import { CalendarEventUidIndexEntry } from "../../api/worker/facades/lazy/Calend import { ParserError } from "../../misc/parsing/ParserCombinator.js" import { LoginController } from "../../api/main/LoginController.js" import { BirthdayEventRegistry } from "./CalendarEventsRepository.js" -import { ByRule } from "../import/ImportExportUtils.js" export type CalendarTimeRange = { start: number @@ -180,45 +180,6 @@ const WEEKDAY_TO_NUMBER = { SU: 7, } as Record -function applyMinuteRules(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[]): DateTime[] { - if (parsedRules.length === 0) { - return dates - } - - const newDates: DateTime[] = [] - for (const date of dates) { - for (const rule of parsedRules) { - newDates.push( - date.set({ - //FIXME Check if rule accepts negative values - minute: Number.parseInt(rule.interval), - }), - ) - } - } - - return newDates -} - -function applyHourRules(dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[]) { - if (parsedRules.length === 0) { - return dates - } - - const newDates: DateTime[] = [] - for (const date of dates) { - for (const rule of parsedRules) { - newDates.push( - date.set({ - hour: Number.parseInt(rule.interval), - }), - ) - } - } - - return newDates -} - function applyByDayRules( dates: DateTime[], parsedRules: CalendarAdvancedRepeatRule[], @@ -1237,6 +1198,11 @@ function* generateEventOccurrences(event: CalendarEvent, timeZone: string, maxDa let eventCount = 0 for (const event of events) { + if (iteration === 1 && event.toJSDate().getTime() === eventStartTime.getTime()) { + // Already yielded + continue + } + const newStartTime = event.toJSDate() const newEndTime = allDay ? incrementByRepeatPeriod(newStartTime, RepeatPeriod.DAILY, calcDuration, repeatTimeZone) @@ -1284,7 +1250,7 @@ export function calendarEventHasMoreThanOneOccurrencesLeft({ progenitor, altered return true } else { // we need to count occurrences and match them up against altered instances & exclusions. - const excludedTimestamps = excludedDates.map(({ date }) => date.getTime()) + const excludedTimestamps = excludedDates.map(({ date }) => date.getTime()).sort() let i = 0 // in our model, we have an extra exclusion for each altered instance. this code // assumes that this invariant is upheld here and does not match each recurrenceId @@ -1675,3 +1641,51 @@ export function extractContactIdFromEvent(id: string | null | undefined): string return decodeBase64("utf-8", id) } + +export enum ByRule { + BYMINUTE = "0", + BYHOUR = "1", + BYDAY = "2", + BYMONTHDAY = "3", + BYYEARDAY = "4", + BYWEEKNO = "5", + BYMONTH = "6", + BYSETPOS = "7", + WKST = "8", +} + +export const BYRULE_MAP = freezeMap( + new Map([ + ["BYMINUTE", ByRule.BYMINUTE], + ["BYHOUR", ByRule.BYHOUR], + ["BYDAY", ByRule.BYDAY], + ["BYMONTHDAY", ByRule.BYMONTHDAY], + ["BYYEARDAY", ByRule.BYYEARDAY], + ["BYWEEKNO", ByRule.BYWEEKNO], + ["BYMONTH", ByRule.BYMONTH], + ["BYSETPOS", ByRule.BYSETPOS], + ["WKST", ByRule.WKST], + ]), +) + +export const enum WeekDaysJsValue { + SU, + MO, + TU, + WE, + TH, + FR, + SA, +} + +export const BYRULE_WEEKDAYS_JS_VALUE = freezeMap( + new Map([ + ["SU", WeekDaysJsValue.SU], + ["MO", WeekDaysJsValue.MO], + ["TU", WeekDaysJsValue.TU], + ["WE", WeekDaysJsValue.WE], + ["TH", WeekDaysJsValue.TH], + ["FR", WeekDaysJsValue.FR], + ["SA", WeekDaysJsValue.SA], + ]), +) diff --git a/src/common/calendar/import/ImportExportUtils.ts b/src/common/calendar/import/ImportExportUtils.ts index 6a7f6c312829..0b924963080a 100644 --- a/src/common/calendar/import/ImportExportUtils.ts +++ b/src/common/calendar/import/ImportExportUtils.ts @@ -2,7 +2,7 @@ import { CalendarEvent, CalendarGroupRoot } from "../../api/entities/tutanota/Ty import type { AlarmInfoTemplate } from "../../api/worker/facades/lazy/CalendarFacade.js" import { assignEventId, CalendarEventValidity, checkEventValidity, getTimeZone } from "../date/CalendarUtils.js" import { ParsedCalendarData, ParsedEvent } from "./CalendarImporter.js" -import { freezeMap, getFromMap, groupBy, insertIntoSortedArray } from "@tutao/tutanota-utils" +import { getFromMap, groupBy, insertIntoSortedArray } from "@tutao/tutanota-utils" import { generateEventElementId } from "../../api/common/utils/CommonCalendarUtils.js" import { createDateWrapper } from "../../api/entities/sys/TypeRefs.js" import { parseCalendarEvents, parseICalendar } from "../../../calendar-app/calendar/export/CalendarParser.js" @@ -152,51 +152,3 @@ export function checkURLString(url: string): TranslationKey | URL { export function hasValidProtocol(url: URL, validProtocols: string[]) { return validProtocols.includes(url.protocol) } - -export enum ByRule { - BYMINUTE = "0", - BYHOUR = "1", - BYDAY = "2", - BYMONTHDAY = "3", - BYYEARDAY = "4", - BYWEEKNO = "5", - BYMONTH = "6", - BYSETPOS = "7", - WKST = "8", -} - -export const BYRULE_MAP = freezeMap( - new Map([ - ["BYMINUTE", ByRule.BYMINUTE], - ["BYHOUR", ByRule.BYHOUR], - ["BYDAY", ByRule.BYDAY], - ["BYMONTHDAY", ByRule.BYMONTHDAY], - ["BYYEARDAY", ByRule.BYYEARDAY], - ["BYWEEKNO", ByRule.BYWEEKNO], - ["BYMONTH", ByRule.BYMONTH], - ["BYSETPOS", ByRule.BYSETPOS], - ["WKST", ByRule.WKST], - ]), -) - -export const enum WeekDaysJsValue { - SU, - MO, - TU, - WE, - TH, - FR, - SA, -} - -export const BYRULE_WEEKDAYS_JS_VALUE = freezeMap( - new Map([ - ["SU", WeekDaysJsValue.SU], - ["MO", WeekDaysJsValue.MO], - ["TU", WeekDaysJsValue.TU], - ["WE", WeekDaysJsValue.WE], - ["TH", WeekDaysJsValue.TH], - ["FR", WeekDaysJsValue.FR], - ["SA", WeekDaysJsValue.SA], - ]), -) diff --git a/src/common/native/main/NativePushServiceApp.ts b/src/common/native/main/NativePushServiceApp.ts index 1fe4113aa4a1..9a29b89ecfe0 100644 --- a/src/common/native/main/NativePushServiceApp.ts +++ b/src/common/native/main/NativePushServiceApp.ts @@ -21,7 +21,7 @@ import { AppType } from "../../misc/ClientConstants.js" // keep in sync with SYS_MODEL_VERSION in app-android/app/build.gradle // keep in sync with SYS_MODEL_VERSION in app-android/calendar/build.gradle.kts // keep in sync with app-ios/TutanotaSharedFramework/Utils/Utils.swift -const MOBILE_SYS_MODEL_VERSION = 118 +const MOBILE_SYS_MODEL_VERSION = 119 function effectiveModelVersion(): number { // on desktop we use generated classes diff --git a/src/mail-app/translations/de.ts b/src/mail-app/translations/de.ts index 81f44faaa9b6..e04e9c582114 100644 --- a/src/mail-app/translations/de.ts +++ b/src/mail-app/translations/de.ts @@ -1924,6 +1924,7 @@ export default { "lastOfPeriod_label": "Am letzten {day} des Monats", "withCustomRules_label": "Mit benutzerdefinierten Wiederholungsregeln", "unsupportedAdvancedRules_msg": "Dieses Ereignis enthält eine oder mehrere nicht unterstützte erweiterte Wiederholungsregeln; jede Änderung führt zum Verlust dieser Regeln.", + "onDays_label": "Am {days}", // Put in temporarily, will be removed soon "localAdminGroup_label": "Local admin group", "assignAdminRightsToLocallyAdministratedUserError_msg": "You can't assign global admin rights to a locally administrated user.", diff --git a/src/mail-app/translations/de_sie.ts b/src/mail-app/translations/de_sie.ts index d1617fb3d5fa..d147893027ac 100644 --- a/src/mail-app/translations/de_sie.ts +++ b/src/mail-app/translations/de_sie.ts @@ -1924,6 +1924,7 @@ export default { "lastOfPeriod_label": "Am letzten {day} des Monats", "withCustomRules_label": "Mit benutzerdefinierten Wiederholungsregeln", "unsupportedAdvancedRules_msg": "Dieses Ereignis enthält eine oder mehrere nicht unterstützte erweiterte Wiederholungsregeln; jede Änderung führt zum Verlust dieser Regeln.", + "onDays_label": "Am {days}", // Put in temporarily, will be removed soon "localAdminGroup_label": "Local admin group", "assignAdminRightsToLocallyAdministratedUserError_msg": "You can't assign global admin rights to a locally administrated user.", diff --git a/src/mail-app/translations/en.ts b/src/mail-app/translations/en.ts index a8547d9ffae4..d9dab731fd58 100644 --- a/src/mail-app/translations/en.ts +++ b/src/mail-app/translations/en.ts @@ -1921,7 +1921,6 @@ export default { "lastOfPeriod_label": "On last {day} of the month", "withCustomRules_label": "With custom repeat rules", "unsupportedAdvancedRules_msg": "This event contains one or more unsupported Advanced Recurrence Rules, any changes will result in the loss of these rules", - "beforeEndOfPeriod_label": "{days} occurrence before end of the {period}", // Put in temporarily, will be removed soon "localAdminGroup_label": "Local admin group", "assignAdminRightsToLocallyAdministratedUserError_msg": "You can't assign global admin rights to a locally administrated user.", diff --git a/test/tests/calendar/CalendarUtilsTest.ts b/test/tests/calendar/CalendarUtilsTest.ts index 65036800cb25..32adcabae27d 100644 --- a/test/tests/calendar/CalendarUtilsTest.ts +++ b/test/tests/calendar/CalendarUtilsTest.ts @@ -27,7 +27,7 @@ import { StandardAlarmInterval, } from "../../../src/common/calendar/date/CalendarUtils.js" import { lang } from "../../../src/common/misc/LanguageViewModel.js" -import { DateWrapperTypeRef, GroupMembershipTypeRef, GroupTypeRef, UserTypeRef } from "../../../src/common/api/entities/sys/TypeRefs.js" +import { DateWrapperTypeRef, GroupMembershipTypeRef, GroupTypeRef, RepeatRule, UserTypeRef } from "../../../src/common/api/entities/sys/TypeRefs.js" import { AccountType, EndType, GroupType, RepeatPeriod, ShareCapability } from "../../../src/common/api/common/TutanotaConstants.js" import { timeStringFromParts } from "../../../src/common/misc/Formatter.js" import { DateTime } from "luxon" @@ -551,14 +551,18 @@ o.spec("calendar utils tests", function () { timeZone, eventStart, eventEnd, - RepeatPeriod.WEEKLY, - 1, - EndType.Never, - 0, - [], StandardAlarmInterval.ONE_HOUR, timeZone, 10, + createCalendarRepeatRule({ + timeZone: timeZone, + frequency: RepeatPeriod.WEEKLY, + interval: String(1), + endValue: null, + endType: "0", + excludedDates: [], + advancedRules: [], + }), ) o(occurrences.slice(0, 4)).deepEquals([ DateTime.fromObject( @@ -637,14 +641,18 @@ o.spec("calendar utils tests", function () { repeatRuleTimeZone, eventStart, eventEnd, - RepeatPeriod.DAILY, - 1, - EndType.UntilDate, - repeatEnd.getTime(), - [], StandardAlarmInterval.ONE_DAY, timeZone, 10, + createCalendarRepeatRule({ + timeZone: repeatRuleTimeZone, + frequency: RepeatPeriod.DAILY, + interval: String(1), + endValue: repeatEnd.getTime().toString(), + endType: EndType.UntilDate, + excludedDates: [], + advancedRules: [], + }), ) o(occurrences).deepEquals([ DateTime.fromObject( @@ -1914,21 +1922,15 @@ function iterateAlarmOccurrences( timeZone: string, eventStart: Date, eventEnd: Date, - repeatPeriod: RepeatPeriod, - interval: number, - endType: EndType, - endValue: number, - exclusions: Array, alarmInterval: AlarmInterval, calculationZone: string, maxOccurrences: number, + repeatRule: RepeatRule, ): Date[] { const occurrences: Date[] = [] while (occurrences.length < maxOccurrences) { - const next: AlarmOccurrence = neverNull( - findNextAlarmOccurrence(now, timeZone, eventStart, eventEnd, repeatPeriod, interval, endType, endValue, exclusions, alarmInterval, calculationZone), - ) + const next: AlarmOccurrence = neverNull(findNextAlarmOccurrence(now, timeZone, eventStart, eventEnd, alarmInterval, calculationZone, repeatRule)) if (next) { occurrences.push(next.alarmTime) diff --git a/tuta-sdk/rust/Cargo.lock b/tuta-sdk/rust/Cargo.lock index 80f0642b5cc1..193d455c6a4e 100644 --- a/tuta-sdk/rust/Cargo.lock +++ b/tuta-sdk/rust/Cargo.lock @@ -593,6 +593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -2240,6 +2241,7 @@ dependencies = [ "sha3", "simple_logger", "thiserror 2.0.11", + "time", "tokio", "tuta-sdk", "uniffi", diff --git a/tuta-sdk/rust/sdk/src/date/event_facade.rs b/tuta-sdk/rust/sdk/src/date/event_facade.rs index d902608dc03f..84e21141caf3 100644 --- a/tuta-sdk/rust/sdk/src/date/event_facade.rs +++ b/tuta-sdk/rust/sdk/src/date/event_facade.rs @@ -8,75 +8,23 @@ use crate::date::DateTime; #[derive(uniffi::Enum, PartialEq, Copy, Clone)] pub enum ByRuleType { - BYMINUTE, - BYHOUR, - BYDAY, - BYMONTHDAY, - BYYEARDAY, - BYWEEKNO, - BYMONTH, - BYSETPOS, - WKST, -} - -impl ByRuleType { - fn value(&self) -> &str { - match *self { - ByRuleType::BYMINUTE => "0", - ByRuleType::BYHOUR => "1", - ByRuleType::BYDAY => "2", - ByRuleType::BYMONTHDAY => "3", - ByRuleType::BYYEARDAY => "4", - ByRuleType::BYWEEKNO => "5", - ByRuleType::BYMONTH => "6", - ByRuleType::BYSETPOS => "7", - ByRuleType::WKST => "8", - } - } - - fn from_str(value: &str) -> ByRuleType { - match value { - "0" => ByRuleType::BYMINUTE, - "1" => ByRuleType::BYHOUR, - "2" => ByRuleType::BYDAY, - "3" => ByRuleType::BYMONTHDAY, - "4" => ByRuleType::BYYEARDAY, - "5" => ByRuleType::BYWEEKNO, - "6" => ByRuleType::BYMONTH, - "7" => ByRuleType::BYSETPOS, - "8" => ByRuleType::WKST, - _ => panic!("Invalid ByRule {value}"), - } - } + ByMinute, + ByHour, + ByDay, + ByMonthday, + ByYearDay, + ByWeekNo, + ByMonth, + BySetPos, + Wkst, } #[derive(uniffi::Enum, PartialEq, Copy, Clone)] pub enum RepeatPeriod { - DAILY, - WEEKLY, - MONTHLY, - ANNUALLY, -} - -impl RepeatPeriod { - fn value(&self) -> &str { - match *self { - RepeatPeriod::DAILY => "0", - RepeatPeriod::WEEKLY => "1", - RepeatPeriod::MONTHLY => "2", - RepeatPeriod::ANNUALLY => "3", - } - } - - fn from_str(value: &str) -> RepeatPeriod { - match value { - "0" => RepeatPeriod::DAILY, - "1" => RepeatPeriod::WEEKLY, - "2" => RepeatPeriod::MONTHLY, - "3" => RepeatPeriod::ANNUALLY, - _ => panic!("Invalid RepeatPeriod {value}"), - } - } + Daily, + Weekly, + Monthly, + Annually, } #[derive(Clone, uniffi::Record)] @@ -177,44 +125,45 @@ impl EventFacade { date: DateTime, repeat_rule: EventRepeatRule, ) -> Vec { - let parsed_date: OffsetDateTime = date.as_system_time().into(); + let Ok(parsed_date) = OffsetDateTime::from_unix_timestamp(date.as_millis() as i64) else { + return Vec::new(); + }; + let date = PrimitiveDateTime::new(parsed_date.date(), parsed_date.time()); let by_month_rules: Vec<&ByRule> = repeat_rule .by_rules .iter() - .filter(|&x| x.by_rule == ByRuleType::BYMONTH) + .filter(|&x| x.by_rule == ByRuleType::ByMonth) .collect(); let by_day_rules: Vec<&ByRule> = repeat_rule .by_rules .iter() - .filter(|&x| x.by_rule == ByRuleType::BYDAY) + .filter(|&x| x.by_rule == ByRuleType::ByDay) .collect(); let by_month_day_rules: Vec<&ByRule> = repeat_rule .by_rules .iter() - .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) + .filter(|&x| x.by_rule == ByRuleType::ByMonthday) .collect(); let by_year_day_rules: Vec<&ByRule> = repeat_rule .by_rules .iter() - .filter(|&x| x.by_rule == ByRuleType::BYYEARDAY) + .filter(|&x| x.by_rule == ByRuleType::ByYearDay) .collect(); let by_week_no_rules: Vec<&ByRule> = repeat_rule .by_rules .iter() - .filter(|&x| x.by_rule == ByRuleType::BYWEEKNO) + .filter(|&x| x.by_rule == ByRuleType::ByWeekNo) .collect(); - let week_start: Weekday; - - if repeat_rule.frequency == RepeatPeriod::ANNUALLY - || repeat_rule.frequency == RepeatPeriod::WEEKLY + let week_start: Weekday = if repeat_rule.frequency == RepeatPeriod::Annually + || repeat_rule.frequency == RepeatPeriod::Weekly { - week_start = match repeat_rule + match repeat_rule .by_rules .iter() - .find(|&x| x.by_rule == ByRuleType::WKST) + .find(|&x| x.by_rule == ByRuleType::Wkst) { Some(rule) => match rule.interval.as_str() { "MO" => Weekday::Monday, @@ -227,10 +176,10 @@ impl EventFacade { _ => Weekday::Monday, }, None => Weekday::Monday, - }; + } } else { - week_start = Weekday::Monday - } + Weekday::Monday + }; let valid_months: Vec = by_month_rules .iter() @@ -252,19 +201,19 @@ impl EventFacade { self.apply_month_rules(&vec![date], &by_month_rules, &repeat_rule.frequency); let week_no_applied_events: Vec = - if repeat_rule.frequency == RepeatPeriod::ANNUALLY { + if repeat_rule.frequency == RepeatPeriod::Annually { self.apply_week_no_rules(month_applied_events, &by_week_no_rules, week_start) } else { month_applied_events }; let year_day_applied_events: Vec = - if repeat_rule.frequency == RepeatPeriod::ANNUALLY { + if repeat_rule.frequency == RepeatPeriod::Annually { self.apply_year_day_rules( week_no_applied_events, &by_year_day_rules, - by_week_no_rules.len() > 0, - by_month_rules.len() > 0, + !by_week_no_rules.is_empty(), + !by_month_rules.is_empty(), ) } else { week_no_applied_events @@ -273,7 +222,7 @@ impl EventFacade { let month_day_applied_events: Vec = self.apply_month_day_rules( year_day_applied_events, &by_month_day_rules, - &repeat_rule.frequency == &RepeatPeriod::DAILY, + repeat_rule.frequency == RepeatPeriod::Daily, ); let day_applied_events: Vec = self.apply_day_rules( month_day_applied_events, @@ -281,7 +230,7 @@ impl EventFacade { &repeat_rule.frequency, valid_months.clone(), week_start, - by_week_no_rules.len() > 0, + !by_week_no_rules.is_empty(), valid_month_days, valid_year_days, ); @@ -305,7 +254,7 @@ impl EventFacade { rules: &Vec<&ByRule>, frequency: &RepeatPeriod, ) -> Vec { - if rules.len() == 0 { + if rules.is_empty() { return dates.clone(); } @@ -318,7 +267,7 @@ impl EventFacade { _ => continue, }; - if frequency == &RepeatPeriod::WEEKLY { + if frequency == &RepeatPeriod::Weekly { let week_start = PrimitiveDateTime::new( Date::from_iso_week_date(date.year(), date.iso_week(), Weekday::Monday) .unwrap(), @@ -339,22 +288,19 @@ impl EventFacade { let is_target_month = week_end_month == target_month || week_start_month == target_month; - if week_start_year == week_end_year + if (week_start_year == week_end_year && week_start_month < week_end_month - && is_target_month + && is_target_month) + || week_start_year < week_end_year && is_target_month { - new_dates.push(date.clone()); - continue; - } else if week_start_year < week_end_year && is_target_month { - new_dates.push(date.clone()); + new_dates.push(*date); continue; } - } else if frequency == &RepeatPeriod::ANNUALLY { - let new_date = - match date.clone().replace_month(Month::from_number(target_month)) { - Ok(dt) => dt, - _ => continue, - }; + } else if frequency == &RepeatPeriod::Annually { + let Ok(new_date) = (*date).replace_month(Month::from_number(target_month)) + else { + continue; + }; let years_to_add = if date.year() == new_date.year() && date.month().to_number() > target_month @@ -375,7 +321,7 @@ impl EventFacade { } if date.month().to_number() == target_month { - new_dates.push(date.clone()); + new_dates.push(*date); } } } @@ -389,7 +335,7 @@ impl EventFacade { rules: &Vec<&ByRule>, week_start: Weekday, ) -> Vec { - if rules.len() == 0 { + if rules.is_empty() { return dates.clone(); } @@ -402,7 +348,7 @@ impl EventFacade { _ => continue, }; - let mut new_date = date.clone(); + let mut new_date = *date; let total_weeks = weeks_in_year(date.year()); @@ -452,7 +398,7 @@ impl EventFacade { evaluate_same_week: bool, evaluate_same_month: bool, ) -> Vec { - if rules.len() == 0 { + if rules.is_empty() { return dates.clone(); } @@ -513,7 +459,7 @@ impl EventFacade { rules: &Vec<&ByRule>, is_daily_event: bool, ) -> Vec { - if rules.len() == 0 { + if rules.is_empty() { return dates.clone(); } @@ -529,10 +475,10 @@ impl EventFacade { date.month().length(date.year()) as i8 - target_day.unsigned_abs() as i8 + 1; if is_daily_event { - if target_day.is_positive() && date.day() == target_day.unsigned_abs() { - new_dates.push(date.clone()); - } else if target_day.is_negative() && days_diff == date.day() as i8 { - new_dates.push(date.clone()); + if (target_day.is_positive() && date.day() == target_day.unsigned_abs()) + || (target_day.is_negative() && days_diff == date.day() as i8) + { + new_dates.push(*date); } continue; @@ -540,18 +486,16 @@ impl EventFacade { if target_day >= 0 && target_day.unsigned_abs() <= date.month().length(date.year()) { - let date = match date.replace_day(target_day.unsigned_abs()) { - Ok(date) => date, - _ => continue, + let Ok(date) = date.replace_day(target_day.unsigned_abs()) else { + continue; }; new_dates.push(date); } else if days_diff > 0 && target_day.unsigned_abs() <= date.month().length(date.year()) { - let date = match date.replace_day(days_diff.unsigned_abs()) { - Ok(date) => date, - _ => continue, + let Ok(date) = date.replace_day(days_diff.unsigned_abs()) else { + continue; }; new_dates.push(date); @@ -573,7 +517,7 @@ impl EventFacade { valid_month_days: Vec, valid_year_days: Vec, ) -> Vec { - if rules.len() == 0 { + if rules.is_empty() { return dates.clone(); } @@ -588,12 +532,12 @@ impl EventFacade { let target_week_day = parsed_rule.get(2); let leading_value = parsed_rule.get(1); - if frequency == &RepeatPeriod::DAILY + if frequency == &RepeatPeriod::Daily && target_week_day.is_some() && date.weekday() == Weekday::from_short(target_week_day.unwrap().as_str()) { - new_dates.push(date.clone()) - } else if frequency == &RepeatPeriod::WEEKLY && target_week_day.is_some() { + new_dates.push(*date) + } else if frequency == &RepeatPeriod::Weekly && target_week_day.is_some() { let mut new_date = date.replace_date( Date::from_iso_week_date( date.year(), @@ -619,19 +563,17 @@ impl EventFacade { new_date = new_date.add(Duration::weeks(1)); } - if valid_months.len() == 0 + if valid_months.is_empty() || valid_months.contains(&new_date.month().to_number()) { new_dates.push(new_date) } - } else if frequency == &RepeatPeriod::MONTHLY && target_week_day.is_some() { + } else if frequency == &RepeatPeriod::Monthly && target_week_day.is_some() { let mut allowed_days: Vec = Vec::new(); - let week_change = - match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { - Ok(val) => val, - _ => 0, - }; + let week_change = leading_value + .map_or(Ok(0), |m| m.as_str().parse::()) + .unwrap_or_default(); let base_date = date.replace_day(1).unwrap(); let stop_condition = @@ -649,7 +591,7 @@ impl EventFacade { } let is_allowed_in_month_day = |day: u8| -> bool { - if allowed_days.len() == 0 { + if allowed_days.is_empty() { return true; } @@ -721,25 +663,20 @@ impl EventFacade { if new_date.assume_utc().unix_timestamp() >= base_date.assume_utc().unix_timestamp() && is_allowed_in_month_day(new_date.day()) + && ((!valid_months.is_empty() + && valid_months.contains(&new_date.month().to_number())) + || valid_months.is_empty()) { - if valid_months.len() > 0 - && valid_months.contains(&new_date.month().to_number()) - { - new_dates.push(new_date) - } else if valid_months.len() == 0 { - new_dates.push(new_date) - } + new_dates.push(new_date) } current_date = new_date.add(Duration::days(7)); } } - } else if frequency == &RepeatPeriod::ANNUALLY { - let week_change = - match leading_value.map_or(Ok(0), |m| m.as_str().parse::()) { - Ok(val) => val, - _ => 0, - }; + } else if frequency == &RepeatPeriod::Annually { + let week_change = leading_value + .map_or(Ok(0), |m| m.as_str().parse::()) + .unwrap_or_default(); if has_week_no && week_change != 0 { println!( @@ -751,7 +688,7 @@ impl EventFacade { if week_change != 0 && !has_week_no { let mut new_date: PrimitiveDateTime; - if !target_week_day.is_some() { + if target_week_day.is_none() { if week_change > 0 { new_date = date .replace_day(1) @@ -806,7 +743,7 @@ impl EventFacade { new_dates.push(new_date) } } else if has_week_no { - if !target_week_day.is_some() { + if target_week_day.is_none() { continue; } @@ -835,20 +772,17 @@ impl EventFacade { new_dates.push(new_date); } } else { - if !target_week_day.is_some() { + if target_week_day.is_none() { continue; } let day_one = date.replace_day(1).unwrap(); let parsed_weekday = Weekday::from_short(target_week_day.unwrap().as_str()); - let stop_date = match Date::from_calendar_date( - date.year() + 1, - date.month(), - date.day(), - ) { - Ok(date) => date, - _ => continue, + let Ok(stop_date) = + Date::from_calendar_date(date.year() + 1, date.month(), date.day()) + else { + continue; }; let stop_condition = date.replace_date(stop_date); @@ -880,11 +814,11 @@ impl EventFacade { } } - if frequency == &RepeatPeriod::ANNUALLY { + if frequency == &RepeatPeriod::Annually { return new_dates .iter() .filter(|date| self.is_valid_day_in_year(**date, valid_year_days.clone())) - .map(|date| *date) + .copied() .collect(); } @@ -899,7 +833,7 @@ impl EventFacade { for allowed_day in valid_year_days { if allowed_day > &0 { - allowed_days.push(allowed_day.abs() as u16); + allowed_days.push(allowed_day.unsigned_abs()); continue; } @@ -913,15 +847,13 @@ impl EventFacade { fn is_valid_day_in_year(&self, date: PrimitiveDateTime, valid_year_days: Vec) -> bool { let valid_days = self.get_valid_days_in_year(date.year(), &valid_year_days); - if valid_days.len() == 0 { + if valid_days.is_empty() { return true; } let day_in_year = date.ordinal(); - let is_valid = valid_days.contains(&day_in_year); - - return is_valid; + valid_days.contains(&day_in_year) } fn finish_rules( @@ -932,11 +864,11 @@ impl EventFacade { ) -> Vec { let mut clean_dates; - if valid_months.len() > 0 { + if !valid_months.is_empty() { clean_dates = dates .iter() .filter(|date| valid_months.contains(&date.month().to_number())) - .map(|date| *date) + .copied() .collect(); } else { clean_dates = dates @@ -949,7 +881,7 @@ impl EventFacade { let date_unix_timestamp = date.assume_utc().unix_timestamp(); date_unix_timestamp >= event_start_time.unwrap() }) - .map(|date| *date) + .copied() .collect(); } @@ -998,15 +930,15 @@ mod tests { &vec![valid_date], &vec![ &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "1".to_string(), }, &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ], - &RepeatPeriod::WEEKLY, + &RepeatPeriod::Weekly, ), vec![valid_date] ); @@ -1016,15 +948,15 @@ mod tests { &vec![invalid_date], &vec![ &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "1".to_string(), }, &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ], - &RepeatPeriod::WEEKLY, + &RepeatPeriod::Weekly, ), vec![] ); @@ -1049,15 +981,15 @@ mod tests { &vec![valid_date], &vec![ &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "1".to_string(), }, &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ], - &RepeatPeriod::MONTHLY, + &RepeatPeriod::Monthly, ), vec![valid_date] ); @@ -1067,15 +999,15 @@ mod tests { &vec![invalid_date], &vec![ &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "1".to_string(), }, &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ], - &RepeatPeriod::MONTHLY, + &RepeatPeriod::Monthly, ), vec![] ); @@ -1100,15 +1032,15 @@ mod tests { &vec![valid_date], &vec![ &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "1".to_string(), }, &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ], - &RepeatPeriod::ANNUALLY, + &RepeatPeriod::Annually, ), vec![ valid_date, @@ -1122,15 +1054,15 @@ mod tests { &vec![to_next_year], &vec![ &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "1".to_string(), }, &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ], - &RepeatPeriod::ANNUALLY, + &RepeatPeriod::Annually, ), vec![ to_next_year @@ -1166,15 +1098,15 @@ mod tests { &vec![valid_date], &vec![ &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "1".to_string(), }, &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ], - &RepeatPeriod::DAILY, + &RepeatPeriod::Daily, ), vec![valid_date] ); @@ -1184,15 +1116,15 @@ mod tests { &vec![invalid_date], &vec![ &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "1".to_string(), }, &ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ], - &RepeatPeriod::DAILY, + &RepeatPeriod::Daily, ), vec![] ); @@ -1221,7 +1153,7 @@ mod tests { event_recurrence.apply_week_no_rules( vec![valid_date], &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, + by_rule: ByRuleType::ByWeekNo, interval: "5".to_string(), },], Weekday::Monday, @@ -1253,7 +1185,7 @@ mod tests { event_recurrence.apply_week_no_rules( vec![valid_date], &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, + by_rule: ByRuleType::ByWeekNo, interval: "5".to_string(), },], Weekday::Tuesday, @@ -1285,7 +1217,7 @@ mod tests { event_recurrence.apply_week_no_rules( vec![valid_date], &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, + by_rule: ByRuleType::ByWeekNo, interval: "-5".to_string(), },], Weekday::Monday, @@ -1317,7 +1249,7 @@ mod tests { event_recurrence.apply_week_no_rules( vec![valid_date], &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, + by_rule: ByRuleType::ByWeekNo, interval: "-1".to_string(), },], Weekday::Monday, @@ -1349,7 +1281,7 @@ mod tests { event_recurrence.apply_week_no_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYWEEKNO, + by_rule: ByRuleType::ByWeekNo, interval: "5".to_string(), },], Weekday::Monday, @@ -1372,7 +1304,7 @@ mod tests { event_recurrence.apply_year_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, + by_rule: ByRuleType::ByYearDay, interval: "40".to_string(), }], false, @@ -1396,7 +1328,7 @@ mod tests { event_recurrence.apply_year_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, + by_rule: ByRuleType::ByYearDay, interval: "40".to_string(), }], true, @@ -1420,7 +1352,7 @@ mod tests { event_recurrence.apply_year_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, + by_rule: ByRuleType::ByYearDay, interval: "40".to_string(), }], true, @@ -1444,7 +1376,7 @@ mod tests { event_recurrence.apply_year_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, + by_rule: ByRuleType::ByYearDay, interval: "40".to_string(), }], false, @@ -1468,7 +1400,7 @@ mod tests { event_recurrence.apply_year_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYYEARDAY, + by_rule: ByRuleType::ByYearDay, interval: "-1".to_string(), }], false, @@ -1497,11 +1429,11 @@ mod tests { vec![date], &vec![ &ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "10".to_string(), }, &ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "20".to_string(), }, ], @@ -1525,7 +1457,7 @@ mod tests { event_recurrence.apply_month_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "30".to_string(), },], false, @@ -1548,7 +1480,7 @@ mod tests { event_recurrence.apply_month_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "20".to_string(), }], false, @@ -1571,7 +1503,7 @@ mod tests { event_recurrence.apply_month_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "-1".to_string(), },], false, @@ -1594,7 +1526,7 @@ mod tests { event_recurrence.apply_month_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "32".to_string(), },], false, @@ -1617,10 +1549,10 @@ mod tests { event_recurrence.apply_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }], - &RepeatPeriod::DAILY, + &RepeatPeriod::Daily, vec![], Weekday::Monday, false, @@ -1645,10 +1577,10 @@ mod tests { event_recurrence.apply_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }], - &RepeatPeriod::DAILY, + &RepeatPeriod::Daily, vec![], Weekday::Monday, false, @@ -1674,15 +1606,15 @@ mod tests { vec![date], &vec![ &ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }, &ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "SA".to_string(), }, ], - &RepeatPeriod::WEEKLY, + &RepeatPeriod::Weekly, vec![], Weekday::Monday, false, @@ -1708,10 +1640,10 @@ mod tests { event_recurrence.apply_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "MO".to_string(), },], - &RepeatPeriod::MONTHLY, + &RepeatPeriod::Monthly, vec![], Weekday::Monday, false, @@ -1739,21 +1671,21 @@ mod tests { let rules = vec![ ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "MO".to_string(), }, ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "7".to_string(), }, ]; let by_day_rules: Vec<&ByRule> = rules .iter() - .filter(|&x| x.by_rule == ByRuleType::BYDAY) + .filter(|&x| x.by_rule == ByRuleType::ByDay) .collect(); let by_month_day_rules: Vec<&ByRule> = rules .iter() - .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) + .filter(|&x| x.by_rule == ByRuleType::ByMonthday) .collect(); let valid_month_days: Vec = by_month_day_rules @@ -1766,7 +1698,7 @@ mod tests { event_recurrence.apply_day_rules( vec![date], &by_day_rules, - &RepeatPeriod::MONTHLY, + &RepeatPeriod::Monthly, vec![], Weekday::Monday, false, @@ -1792,10 +1724,10 @@ mod tests { event_recurrence.apply_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "2MO".to_string(), },], - &RepeatPeriod::MONTHLY, + &RepeatPeriod::Monthly, vec![], Weekday::Monday, false, @@ -1818,21 +1750,21 @@ mod tests { let rules = vec![ ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "2MO".to_string(), }, ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "7".to_string(), }, ]; let by_day_rules: Vec<&ByRule> = rules .iter() - .filter(|&x| x.by_rule == ByRuleType::BYDAY) + .filter(|&x| x.by_rule == ByRuleType::ByDay) .collect(); let by_month_day_rules: Vec<&ByRule> = rules .iter() - .filter(|&x| x.by_rule == ByRuleType::BYMONTHDAY) + .filter(|&x| x.by_rule == ByRuleType::ByMonthday) .collect(); let valid_month_days: Vec = by_month_day_rules @@ -1845,7 +1777,7 @@ mod tests { event_recurrence.apply_day_rules( vec![date], &by_day_rules, - &RepeatPeriod::MONTHLY, + &RepeatPeriod::Monthly, vec![], Weekday::Monday, false, @@ -1880,10 +1812,10 @@ mod tests { event_recurrence.apply_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "MO".to_string(), },], - &RepeatPeriod::ANNUALLY, + &RepeatPeriod::Annually, vec![], Weekday::Monday, false, @@ -1909,10 +1841,10 @@ mod tests { event_recurrence.apply_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "2MO".to_string(), },], - &RepeatPeriod::ANNUALLY, + &RepeatPeriod::Annually, vec![], Weekday::Monday, false, @@ -1938,10 +1870,10 @@ mod tests { event_recurrence.apply_day_rules( vec![date], &vec![&ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "35".to_string(), },], - &RepeatPeriod::ANNUALLY, + &RepeatPeriod::Annually, vec![], Weekday::Monday, false, @@ -1971,15 +1903,15 @@ mod tests { vec![date], &vec![ &ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "MO".to_string(), }, &ByRule { - by_rule: ByRuleType::BYWEEKNO, + by_rule: ByRuleType::ByWeekNo, interval: "6".to_string(), }, ], - &RepeatPeriod::ANNUALLY, + &RepeatPeriod::Annually, vec![], Weekday::Monday, true, @@ -2005,15 +1937,15 @@ mod tests { vec![date], &vec![ &ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "35".to_string(), }, &ByRule { - by_rule: ByRuleType::BYWEEKNO, + by_rule: ByRuleType::ByWeekNo, interval: "7".to_string(), }, ], - &RepeatPeriod::ANNUALLY, + &RepeatPeriod::Annually, vec![], Weekday::Monday, true, @@ -2040,15 +1972,15 @@ mod tests { vec![date], &vec![ &ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "2MO".to_string(), }, &ByRule { - by_rule: ByRuleType::BYWEEKNO, + by_rule: ByRuleType::ByWeekNo, interval: "6".to_string(), }, ], - &RepeatPeriod::ANNUALLY, + &RepeatPeriod::Annually, vec![], Weekday::Monday, true, @@ -2068,18 +2000,18 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::DAILY, + frequency: RepeatPeriod::Daily, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "3".to_string(), }, ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "6".to_string(), }, ], @@ -2122,18 +2054,18 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::DAILY, + frequency: RepeatPeriod::Daily, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "TH".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }, ], @@ -2155,22 +2087,22 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::DAILY, + frequency: RepeatPeriod::Daily, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "14".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "TH".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }, ], @@ -2184,7 +2116,7 @@ mod tests { assert_eq!( event_recurrence.generate_future_instances( date.replace_day(13).unwrap().to_date_time(), - repeat_rule.clone() + repeat_rule.clone(), ), [] ); @@ -2199,9 +2131,9 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::WEEKLY, + frequency: RepeatPeriod::Weekly, by_rules: vec![ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }], }; @@ -2229,18 +2161,18 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::WEEKLY, + frequency: RepeatPeriod::Weekly, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "TH".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }, ], @@ -2265,14 +2197,14 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::WEEKLY, + frequency: RepeatPeriod::Weekly, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "TH".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }, ], @@ -2297,18 +2229,18 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::WEEKLY, + frequency: RepeatPeriod::Weekly, by_rules: vec![ ByRule { - by_rule: ByRuleType::WKST, + by_rule: ByRuleType::Wkst, interval: "FR".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "TH".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }, ], @@ -2333,9 +2265,9 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, + frequency: RepeatPeriod::Monthly, by_rules: vec![ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }], }; @@ -2360,9 +2292,9 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, + frequency: RepeatPeriod::Monthly, by_rules: vec![ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "2FR".to_string(), }], }; @@ -2383,14 +2315,14 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, + frequency: RepeatPeriod::Monthly, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "-1FR".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "-2FR".to_string(), }, ], @@ -2419,9 +2351,9 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, + frequency: RepeatPeriod::Monthly, by_rules: vec![ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }], }; @@ -2447,14 +2379,14 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, + frequency: RepeatPeriod::Monthly, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "25".to_string(), }, ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "28".to_string(), }, ], @@ -2479,18 +2411,18 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, + frequency: RepeatPeriod::Monthly, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "25".to_string(), }, ByRule { - by_rule: ByRuleType::BYMONTHDAY, + by_rule: ByRuleType::ByMonthday, interval: "28".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }, ], @@ -2512,18 +2444,18 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::MONTHLY, + frequency: RepeatPeriod::Monthly, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "TH".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }, ], @@ -2571,14 +2503,14 @@ mod tests { } let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::ANNUALLY, + frequency: RepeatPeriod::Annually, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "TH".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }, ], @@ -2600,14 +2532,14 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::ANNUALLY, + frequency: RepeatPeriod::Annually, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYYEARDAY, + by_rule: ByRuleType::ByYearDay, interval: "44".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "TH".to_string(), }, ], @@ -2637,14 +2569,14 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::ANNUALLY, + frequency: RepeatPeriod::Annually, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYWEEKNO, + by_rule: ByRuleType::ByWeekNo, interval: "8".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "TH".to_string(), }, ], @@ -2688,9 +2620,9 @@ mod tests { } let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::ANNUALLY, + frequency: RepeatPeriod::Annually, by_rules: vec![ByRule { - by_rule: ByRuleType::BYWEEKNO, + by_rule: ByRuleType::ByWeekNo, interval: "-5".to_string(), }], }; @@ -2712,14 +2644,14 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::ANNUALLY, + frequency: RepeatPeriod::Annually, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYWEEKNO, + by_rule: ByRuleType::ByWeekNo, interval: "8".to_string(), }, ByRule { - by_rule: ByRuleType::WKST, + by_rule: ByRuleType::Wkst, interval: "TU".to_string(), }, ], @@ -2749,18 +2681,18 @@ mod tests { ); let repeat_rule = EventRepeatRule { - frequency: RepeatPeriod::ANNUALLY, + frequency: RepeatPeriod::Annually, by_rules: vec![ ByRule { - by_rule: ByRuleType::BYMONTH, + by_rule: ByRuleType::ByMonth, interval: "2".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "TH".to_string(), }, ByRule { - by_rule: ByRuleType::BYDAY, + by_rule: ByRuleType::ByDay, interval: "FR".to_string(), }, ],