Stage 3.3

This commit is contained in:
2026-04-20 14:04:30 +03:00
parent 5291f70337
commit 42a047a938
9 changed files with 988 additions and 29 deletions
+160
View File
@@ -0,0 +1,160 @@
-module(logic_recurrence).
-include("records.hrl").
-export([parse_rrule/1, generate_occurrences/3]).
-export([validate_rrule/1]).
%% Типы частоты повторения
-define(FREQ_DAILY, <<"DAILY">>).
-define(FREQ_WEEKLY, <<"WEEKLY">>).
-define(FREQ_MONTHLY, <<"MONTHLY">>).
%% Парсинг RRULE из JSON
parse_rrule(RRuleMap) when is_map(RRuleMap) ->
Freq = maps:get(<<"freq">>, RRuleMap, maps:get(freq, RRuleMap, undefined)),
Interval = maps:get(<<"interval">>, RRuleMap, maps:get(interval, RRuleMap, undefined)),
Until = maps:get(<<"until">>, RRuleMap, maps:get(until, RRuleMap, undefined)),
Count = maps:get(<<"count">>, RRuleMap, maps:get(count, RRuleMap, undefined)),
ByDay = maps:get(<<"byday">>, RRuleMap, maps:get(byday, RRuleMap, [])),
{ok, #{
freq => Freq,
interval => Interval,
until => Until,
count => Count,
byday => ByDay
}}.
%% Валидация RRULE
validate_rrule(RRule) ->
try
% Поддерживаем и атомы, и бинарные ключи
Freq = get_freq(RRule),
Interval = get_interval(RRule),
ValidFreq = (Freq =:= ?FREQ_DAILY orelse
Freq =:= ?FREQ_WEEKLY orelse
Freq =:= ?FREQ_MONTHLY orelse
Freq =:= <<"DAILY">> orelse
Freq =:= <<"WEEKLY">> orelse
Freq =:= <<"MONTHLY">>),
ValidInterval = is_integer(Interval) andalso Interval >= 1,
ValidFreq andalso ValidInterval
catch
_:_ -> false
end.
get_freq(#{freq := Freq}) -> Freq;
get_freq(#{<<"freq">> := Freq}) -> Freq.
get_interval(#{interval := Interval}) -> Interval;
get_interval(#{<<"interval">> := Interval}) -> Interval.
%% Генерация вхождений в заданном диапазоне
generate_occurrences(StartTime, RRule, RangeEnd) ->
Freq = normalize_freq(get_freq(RRule)),
Interval = get_interval(RRule),
UntilRaw = maps:get(until, RRule, maps:get(<<"until">>, RRule, undefined)),
Count = maps:get(count, RRule, maps:get(<<"count">>, RRule, undefined)),
ByDay = maps:get(byday, RRule, maps:get(<<"byday">>, RRule, [])),
EndBoundary = case UntilRaw of
undefined -> RangeEnd;
U when is_binary(U) ->
{ok, UntilDt} = parse_datetime_str(U),
erlang:min(UntilDt, RangeEnd)
end,
generate_by_freq(StartTime, Freq, Interval, ByDay, EndBoundary, Count, 0, []).
normalize_freq(<<"DAILY">>) -> ?FREQ_DAILY;
normalize_freq(<<"WEEKLY">>) -> ?FREQ_WEEKLY;
normalize_freq(<<"MONTHLY">>) -> ?FREQ_MONTHLY;
normalize_freq(Freq) when is_atom(Freq) -> Freq.
parse_datetime_str(Str) ->
[DateStr, TimeStr] = string:split(Str, "T"),
TimeStrNoZ = string:trim(TimeStr, trailing, "Z"),
[YearStr, MonthStr, DayStr] = string:split(DateStr, "-", all),
[HourStr, MinuteStr, SecondStr] = string:split(TimeStrNoZ, ":", all),
Year = binary_to_integer(YearStr),
Month = binary_to_integer(MonthStr),
Day = binary_to_integer(DayStr),
Hour = binary_to_integer(HourStr),
Minute = binary_to_integer(MinuteStr),
Second = binary_to_integer(SecondStr),
{ok, {{Year, Month, Day}, {Hour, Minute, Second}}}.
minus(Dt1, Dt2) when Dt1 < Dt2 -> Dt1;
minus(_, Dt2) -> Dt2.
%% Генерация по частоте
generate_by_freq(Current, ?FREQ_DAILY, Interval, _ByDay, EndBoundary, MaxCount, Count, Acc) ->
case should_stop(Current, EndBoundary, MaxCount, Count) of
true -> lists:reverse(Acc);
false ->
generate_by_freq(
add_days(Current, Interval),
?FREQ_DAILY, Interval, _ByDay, EndBoundary, MaxCount,
Count + 1, [Current | Acc]
)
end;
generate_by_freq(Current, ?FREQ_WEEKLY, Interval, ByDay, EndBoundary, MaxCount, Count, Acc) ->
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,
generate_by_freq(
add_weeks(Current, Interval),
?FREQ_WEEKLY, Interval, ByDay, EndBoundary, MaxCount,
Count + 1, NewOccurrences ++ Acc
)
end;
generate_by_freq(Current, ?FREQ_MONTHLY, Interval, ByDay, EndBoundary, MaxCount, Count, Acc) ->
case should_stop(Current, EndBoundary, MaxCount, Count) of
true -> lists:reverse(Acc);
false ->
NewOccurrences = case ByDay of
[] -> [Current];
_ -> filter_by_month_day(Current, ByDay)
end,
generate_by_freq(
add_months(Current, Interval),
?FREQ_MONTHLY, Interval, ByDay, EndBoundary, MaxCount,
Count + 1, NewOccurrences ++ Acc
)
end.
%% Вспомогательные функции
should_stop(Current, EndBoundary, MaxCount, Count) ->
(Current > EndBoundary) orelse (MaxCount =/= undefined andalso Count >= MaxCount).
add_days({{Y, M, D}, Time}, N) ->
Days = calendar:date_to_gregorian_days(Y, M, D) + N,
{calendar:gregorian_days_to_date(Days), Time}.
add_weeks(DateTime, N) ->
add_days(DateTime, N * 7).
add_months({{Y, M, D}, Time}, N) ->
TotalMonths = Y * 12 + M - 1 + N,
NewYear = TotalMonths div 12,
NewMonth = TotalMonths rem 12 + 1,
NewDay = minus(D, calendar:last_day_of_the_month(NewYear, NewMonth)),
{{NewYear, NewMonth, NewDay}, Time}.
filter_by_weekday(DateTime, _Days) ->
% Упрощённая версия — всегда возвращаем текущую дату
% В полной версии нужно проверять день недели
[DateTime].
filter_by_month_day(DateTime, _Days) ->
[DateTime].