fix(recurrence): honor BYDAY for weekly RRULE expansion
CI / test (push) Successful in 8m33s
CI / deploy-ift (push) Successful in 3m26s
CI / e2e-ift (push) Successful in 3m44s
CI / deploy-stage (push) Successful in 2m14s
CI / e2e-stage (push) Successful in 1m21s

Expand weekdays in the DTSTART week and advance by Interval weeks from Monday;
add unit coverage for MO/WE/FR and mid-week starts.
This commit is contained in:
2026-08-01 18:17:23 +03:00
parent 0e4e886cd4
commit 0a5e86c4a0
2 changed files with 88 additions and 10 deletions
+63 -10
View File
@@ -106,15 +106,21 @@ generate_by_freq(Current, ?FREQ_WEEKLY, Interval, ByDay, EndBoundary, MaxCount,
case should_stop(Current, EndBoundary, MaxCount, Count) of
true -> lists:reverse(Acc);
false ->
% Если указаны дни недели, генерируем только в эти дни
NewOccurrences = case ByDay of
[] -> [Current];
Days -> filter_by_weekday(Current, Days)
end,
{NewOccurrences, Next} =
case ByDay of
[] ->
{[Current], add_weeks(Current, Interval)};
Days ->
%% BYDAY: expand weekdays in Current's week (on/after Current),
%% then jump Interval weeks from that week's Monday.
Occs = [O || O <- filter_by_weekday(Current, Days), O =< EndBoundary],
{Occs, add_weeks(week_start_monday(Current), Interval)}
end,
%% Prepend reversed batch so final lists:reverse/1 yields chronological order.
generate_by_freq(
add_weeks(Current, Interval),
Next,
?FREQ_WEEKLY, Interval, ByDay, EndBoundary, MaxCount,
Count + 1, NewOccurrences ++ Acc
Count + 1, lists:reverse(NewOccurrences) ++ Acc
)
end;
@@ -151,9 +157,56 @@ add_months({{Y, M, D}, Time}, N) ->
NewDay = minus(D, calendar:last_day_of_the_month(NewYear, NewMonth)),
{{NewYear, NewMonth, NewDay}, Time}.
filter_by_weekday(DateTime, _Days) ->
% Упрощённая версия — всегда возвращаем текущую дату
% В полной версии нужно проверять день недели
%% BYDAY codes (RFC 5545): MO..SU. calendar:day_of_the_week = 1=Mon .. 7=Sun.
week_start_monday({{Y, M, D}, Time}) ->
Dow = calendar:day_of_the_week({Y, M, D}),
add_days({{Y, M, D}, Time}, -(Dow - 1)).
byday_to_dow(<<"MO">>) -> 1;
byday_to_dow(<<"TU">>) -> 2;
byday_to_dow(<<"WE">>) -> 3;
byday_to_dow(<<"TH">>) -> 4;
byday_to_dow(<<"FR">>) -> 5;
byday_to_dow(<<"SA">>) -> 6;
byday_to_dow(<<"SU">>) -> 7;
byday_to_dow("MO") -> 1;
byday_to_dow("TU") -> 2;
byday_to_dow("WE") -> 3;
byday_to_dow("TH") -> 4;
byday_to_dow("FR") -> 5;
byday_to_dow("SA") -> 6;
byday_to_dow("SU") -> 7;
byday_to_dow(mo) -> 1;
byday_to_dow(tu) -> 2;
byday_to_dow(we) -> 3;
byday_to_dow(th) -> 4;
byday_to_dow(fr) -> 5;
byday_to_dow(sa) -> 6;
byday_to_dow(su) -> 7;
byday_to_dow(_) -> undefined.
%% Occurrences for BYDAY in the week of Current, same time-of-day.
%% Only dates on or after Current (so DTSTART mid-week does not emit earlier weekdays).
filter_by_weekday(Current, Days) when is_list(Days) ->
Monday = week_start_monday(Current),
Occs =
lists:foldl(
fun(DayCode, Acc) ->
case byday_to_dow(DayCode) of
undefined -> Acc;
TargetDow ->
Occ = add_days(Monday, TargetDow - 1),
case Occ >= Current of
true -> [Occ | Acc];
false -> Acc
end
end
end,
[],
Days
),
lists:sort(Occs);
filter_by_weekday(DateTime, _) ->
[DateTime].
filter_by_month_day(DateTime, _Days) ->