@tmlmobilidade/dates 20260302.1453.8 → 20260305.1602.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/calendar/index.d.ts +4 -0
  2. package/dist/calendar/index.js +4 -0
  3. package/dist/calendar/parameters/index.d.ts +1 -0
  4. package/dist/calendar/parameters/index.js +1 -0
  5. package/dist/calendar/parameters/stops-parameters.d.ts +28 -0
  6. package/dist/calendar/parameters/stops-parameters.js +80 -0
  7. package/dist/{period-utils.d.ts → calendar/periods/index.d.ts} +1 -1
  8. package/dist/{period-utils.js → calendar/periods/index.js} +1 -1
  9. package/dist/calendar/rules/calculation/collectors.d.ts +47 -0
  10. package/dist/calendar/rules/calculation/collectors.js +70 -0
  11. package/dist/calendar/rules/calculation/filters.d.ts +66 -0
  12. package/dist/calendar/rules/calculation/filters.js +162 -0
  13. package/dist/calendar/rules/calculation/index.d.ts +41 -0
  14. package/dist/calendar/rules/calculation/index.js +87 -0
  15. package/dist/calendar/rules/calculation/matchers.d.ts +39 -0
  16. package/dist/calendar/rules/calculation/matchers.js +60 -0
  17. package/dist/calendar/rules/calculation/types.d.ts +22 -0
  18. package/dist/calendar/rules/calculation/types.js +1 -0
  19. package/dist/calendar/rules/formatting/common.d.ts +35 -0
  20. package/dist/calendar/rules/formatting/common.js +94 -0
  21. package/dist/calendar/rules/formatting/parameter-summary.d.ts +17 -0
  22. package/dist/calendar/rules/formatting/parameter-summary.js +50 -0
  23. package/dist/calendar/rules/formatting/rule-summary.d.ts +40 -0
  24. package/dist/calendar/rules/formatting/rule-summary.js +149 -0
  25. package/dist/calendar/rules/index.d.ts +8 -0
  26. package/dist/calendar/rules/index.js +8 -0
  27. package/dist/calendar/rules/preview/affectedDates.d.ts +30 -0
  28. package/dist/calendar/rules/preview/affectedDates.js +71 -0
  29. package/dist/calendar/rules/preview/buildDayDetails.d.ts +11 -0
  30. package/dist/calendar/rules/preview/buildDayDetails.js +109 -0
  31. package/dist/calendar/rules/preview/computeRuleTimePoints.d.ts +13 -0
  32. package/dist/calendar/rules/preview/computeRuleTimePoints.js +92 -0
  33. package/dist/calendar/rules/preview/types.d.ts +27 -0
  34. package/dist/calendar/rules/preview/types.js +1 -0
  35. package/dist/calendar/rules/utils/date.d.ts +35 -0
  36. package/dist/calendar/rules/utils/date.js +58 -0
  37. package/dist/{calendar.d.ts → calendar/utils/index.d.ts} +2 -2
  38. package/dist/{calendar.js → calendar/utils/index.js} +1 -1
  39. package/dist/day-periods.d.ts +6 -0
  40. package/dist/day-periods.js +33 -0
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.js +8 -2
  43. package/package.json +2 -2
@@ -0,0 +1,4 @@
1
+ export * from './parameters/index.js';
2
+ export * from './periods/index.js';
3
+ export * from './rules/index.js';
4
+ export * from './utils/index.js';
@@ -0,0 +1,4 @@
1
+ export * from './parameters/index.js';
2
+ export * from './periods/index.js';
3
+ export * from './rules/index.js';
4
+ export * from './utils/index.js';
@@ -0,0 +1 @@
1
+ export * from './stops-parameters.js';
@@ -0,0 +1 @@
1
+ export * from './stops-parameters.js';
@@ -0,0 +1,28 @@
1
+ import { type Path, type StopsParameter } from '@tmlmobilidade/types';
2
+ export type MergedPathItem = Path & {
3
+ avg_speed?: null | number;
4
+ dwell_time?: null | number;
5
+ };
6
+ export interface SegmentTravelTimes {
7
+ segmentTravelSeconds: {
8
+ formatted: Array<string>;
9
+ raw: Array<null | number>;
10
+ };
11
+ stopDwellSeconds: {
12
+ formatted: Array<string>;
13
+ raw: Array<null | number>;
14
+ };
15
+ totalTripSecondsWithoutStops: {
16
+ formatted: string;
17
+ raw: number;
18
+ };
19
+ totalTripSecondsWithStops: {
20
+ formatted: string;
21
+ raw: number;
22
+ };
23
+ }
24
+ export declare function toNumberOrNull(value: unknown): null | number;
25
+ export declare function computeSegmentSeconds(distanceMeters: null | number, speedKmh: null | number): number;
26
+ export declare function formatSecondsToTime(timeInSeconds: null | number): string;
27
+ export declare function computeSegmentTravelTimes(mergedPath: MergedPathItem[]): SegmentTravelTimes;
28
+ export declare function getMergedPath(basePath: Path[], newPath: StopsParameter['path']): MergedPathItem[];
@@ -0,0 +1,80 @@
1
+ /* * */
2
+ /* * */
3
+ export function toNumberOrNull(value) {
4
+ if (value === null || value === undefined)
5
+ return null;
6
+ if (typeof value === 'number')
7
+ return Number.isFinite(value) ? value : null;
8
+ if (typeof value === 'string') {
9
+ const v = Number(value.replace(',', '.'));
10
+ return Number.isFinite(v) ? v : null;
11
+ }
12
+ return null;
13
+ }
14
+ export function computeSegmentSeconds(distanceMeters, speedKmh) {
15
+ if (!distanceMeters || !speedKmh)
16
+ return 0;
17
+ // km/h -> m/s
18
+ const speedMs = speedKmh * 1000 / 3600;
19
+ return Math.round(distanceMeters / speedMs) || 0;
20
+ }
21
+ export function formatSecondsToTime(timeInSeconds) {
22
+ if (timeInSeconds === null || timeInSeconds === undefined)
23
+ return '•••';
24
+ if (timeInSeconds < 60) {
25
+ return timeInSeconds + ' seg';
26
+ }
27
+ if (timeInSeconds < 3600) {
28
+ const minutes = Math.floor(timeInSeconds / 60);
29
+ const seconds = timeInSeconds % 60;
30
+ return `${minutes} min ${seconds} seg`;
31
+ }
32
+ const hours = Math.floor(timeInSeconds / 3600);
33
+ const minutes = Math.floor((timeInSeconds % 3600) / 60);
34
+ const seconds = timeInSeconds % 60;
35
+ return `${hours} h ${minutes} min ${seconds} seg`;
36
+ }
37
+ export function computeSegmentTravelTimes(mergedPath) {
38
+ const segmentTravelSeconds = [];
39
+ const stopDwellSeconds = [];
40
+ for (let i = 0; i < mergedPath.length; i++) {
41
+ const row = mergedPath[i];
42
+ // Dwell applies at the stop itself
43
+ const dwell = toNumberOrNull(row.dwell_time);
44
+ stopDwellSeconds.push(dwell && dwell > 0 ? Math.round(dwell) : null);
45
+ // Segment travel time applies from previous stop -> current stop
46
+ if (i === 0) {
47
+ segmentTravelSeconds.push(null);
48
+ continue;
49
+ }
50
+ const distanceMeters = toNumberOrNull(row.distance_delta);
51
+ const speedKmh = toNumberOrNull(row.avg_speed);
52
+ segmentTravelSeconds.push(computeSegmentSeconds(distanceMeters, speedKmh));
53
+ }
54
+ const totalTripSecondsWithoutStops = segmentTravelSeconds
55
+ .filter((v) => typeof v === 'number')
56
+ .reduce((a, b) => a + b, 0);
57
+ const totalStopSeconds = stopDwellSeconds
58
+ .filter((v) => typeof v === 'number')
59
+ .reduce((a, b) => a + b, 0);
60
+ const totalTripSecondsWithStops = totalTripSecondsWithoutStops + totalStopSeconds;
61
+ return {
62
+ segmentTravelSeconds: { formatted: segmentTravelSeconds.map(s => formatSecondsToTime(s)), raw: segmentTravelSeconds },
63
+ stopDwellSeconds: { formatted: stopDwellSeconds.map(s => formatSecondsToTime(s)), raw: stopDwellSeconds },
64
+ totalTripSecondsWithoutStops: { formatted: formatSecondsToTime(totalTripSecondsWithoutStops), raw: totalTripSecondsWithoutStops },
65
+ totalTripSecondsWithStops: { formatted: formatSecondsToTime(totalTripSecondsWithStops), raw: totalTripSecondsWithStops },
66
+ };
67
+ }
68
+ export function getMergedPath(basePath, newPath) {
69
+ const editedByStopId = new Map((newPath || [])
70
+ .filter(p => p?.stop_id)
71
+ .map(p => [p.stop_id, p]));
72
+ return basePath.map((baseItem) => {
73
+ const edit = editedByStopId.get(baseItem.stop_id);
74
+ return {
75
+ ...baseItem,
76
+ avg_speed: edit?.avg_speed,
77
+ dwell_time: edit?.dwell_time,
78
+ };
79
+ });
80
+ }
@@ -1,5 +1,5 @@
1
+ import { CalendarKey } from '../utils/index.js';
1
2
  import { OperationalDate } from '@tmlmobilidade/types';
2
- import { CalendarKey } from './calendar.js';
3
3
  /**
4
4
  * Converts a date range into an array of CalendarKey strings (YYYY-MM-DD).
5
5
  * Returned in sorted order (ascending).
@@ -1,5 +1,5 @@
1
1
  /* * */
2
- import { calendarKey, datesFromCalendarKey, keyToYYYYMMDD } from './calendar.js';
2
+ import { calendarKey, datesFromCalendarKey, keyToYYYYMMDD } from '../utils/index.js';
3
3
  /* * */
4
4
  /**
5
5
  * Converts a date range into an array of CalendarKey strings (YYYY-MM-DD).
@@ -0,0 +1,47 @@
1
+ import type { DayContext } from './types.js';
2
+ import { EventReplacementRule, ManualRule } from '@tmlmobilidade/types';
3
+ /**
4
+ * Collects time points from manual INCLUDE rules that match a given day context.
5
+ *
6
+ * This is the first phase of rule application for normal (non-replacement) days.
7
+ * Finds all manual rules with operating_mode='include' that match both the weekday
8
+ * and period ID, then aggregates their time points.
9
+ *
10
+ * @param manualRules - Array of all manual rules to check
11
+ * @param ctx - The day context (weekday + period) to match against
12
+ * @returns Object containing the applied rule IDs and the collected time points
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const result = collectManualIncludes(rules, { weekday: 1, yearPeriodId: 'school' });
17
+ * // result = { appliedRuleIds: ['rule1', 'rule2'], timepoints: Set(['08:00', '09:00']) }
18
+ * ```
19
+ */
20
+ export declare function collectManualIncludes(manualRules: ManualRule[], ctx: DayContext): {
21
+ appliedRuleIds: string[];
22
+ timepoints: Set<string>;
23
+ };
24
+ /**
25
+ * Collects time points from manual INCLUDE rules that intersect with a replacement rule.
26
+ *
27
+ * This is the first phase of rule application for days controlled by an event replacement.
28
+ * Uses intersection-based matching: if a manual rule matches ANY of the weekdays/periods
29
+ * targeted by the replacement, its time points are included.
30
+ *
31
+ * The replacement rule itself is always marked as applied (if it has an ID).
32
+ *
33
+ * @param replacement - The event replacement rule controlling this date
34
+ * @param manualRules - Array of all manual rules to check for intersection
35
+ * @returns Object containing the applied rule IDs (including replacement) and collected time points
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * const replacement = { weekdays: [1], year_period_ids: ['school'], _id: 'event1' };
40
+ * const result = collectReplacementManualIncludes(replacement, manualRules);
41
+ * // result.appliedRuleIds will always include 'event1'
42
+ * ```
43
+ */
44
+ export declare function collectReplacementManualIncludes(replacement: EventReplacementRule, manualRules: ManualRule[]): {
45
+ appliedRuleIds: string[];
46
+ timepoints: Set<string>;
47
+ };
@@ -0,0 +1,70 @@
1
+ import { manualRuleMatchesContext, manualRuleMatchesReplacement } from './matchers.js';
2
+ /**
3
+ * Collects time points from manual INCLUDE rules that match a given day context.
4
+ *
5
+ * This is the first phase of rule application for normal (non-replacement) days.
6
+ * Finds all manual rules with operating_mode='include' that match both the weekday
7
+ * and period ID, then aggregates their time points.
8
+ *
9
+ * @param manualRules - Array of all manual rules to check
10
+ * @param ctx - The day context (weekday + period) to match against
11
+ * @returns Object containing the applied rule IDs and the collected time points
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const result = collectManualIncludes(rules, { weekday: 1, yearPeriodId: 'school' });
16
+ * // result = { appliedRuleIds: ['rule1', 'rule2'], timepoints: Set(['08:00', '09:00']) }
17
+ * ```
18
+ */
19
+ export function collectManualIncludes(manualRules, ctx) {
20
+ const timepoints = new Set();
21
+ const appliedRuleIds = [];
22
+ for (const r of manualRules) {
23
+ if (r.operating_mode !== 'include')
24
+ continue;
25
+ if (!manualRuleMatchesContext(r, ctx))
26
+ continue;
27
+ appliedRuleIds.push(r._id);
28
+ for (const tp of r.timepoints ?? [])
29
+ timepoints.add(tp);
30
+ }
31
+ return { appliedRuleIds, timepoints };
32
+ }
33
+ /**
34
+ * Collects time points from manual INCLUDE rules that intersect with a replacement rule.
35
+ *
36
+ * This is the first phase of rule application for days controlled by an event replacement.
37
+ * Uses intersection-based matching: if a manual rule matches ANY of the weekdays/periods
38
+ * targeted by the replacement, its time points are included.
39
+ *
40
+ * The replacement rule itself is always marked as applied (if it has an ID).
41
+ *
42
+ * @param replacement - The event replacement rule controlling this date
43
+ * @param manualRules - Array of all manual rules to check for intersection
44
+ * @returns Object containing the applied rule IDs (including replacement) and collected time points
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const replacement = { weekdays: [1], year_period_ids: ['school'], _id: 'event1' };
49
+ * const result = collectReplacementManualIncludes(replacement, manualRules);
50
+ * // result.appliedRuleIds will always include 'event1'
51
+ * ```
52
+ */
53
+ export function collectReplacementManualIncludes(replacement, manualRules) {
54
+ const timepoints = new Set();
55
+ const appliedRuleIds = [];
56
+ // Always mark replacement as applied if it has an id (optional in schema)
57
+ if (replacement._id)
58
+ appliedRuleIds.push(replacement._id);
59
+ for (const r of manualRules) {
60
+ if (r.operating_mode !== 'include')
61
+ continue;
62
+ if (!manualRuleMatchesReplacement(r, replacement))
63
+ continue;
64
+ if (r._id)
65
+ appliedRuleIds.push(r._id);
66
+ for (const tp of r.timepoints)
67
+ timepoints.add(tp);
68
+ }
69
+ return { appliedRuleIds, timepoints };
70
+ }
@@ -0,0 +1,66 @@
1
+ import type { DayContext } from './types.js';
2
+ import { type EventReplacementRule, type ManualRule, type OperationalDate, type ScheduleRule } from '@tmlmobilidade/types';
3
+ /**
4
+ * Removes time points based on manual EXCLUDE rules that match a given day context.
5
+ *
6
+ * This is the second phase of rule application for normal days. Finds all manual rules
7
+ * with operating_mode='exclude' that match the weekday and period, then removes their
8
+ * time points from the collected set.
9
+ *
10
+ * Mutates the timepoints Set and appliedRuleIds array in place.
11
+ *
12
+ * @param timepoints - Set of time points to filter (mutated in place)
13
+ * @param appliedRuleIds - Array to track which rules were applied (mutated in place)
14
+ * @param manualRules - Array of all manual rules to check
15
+ * @param ctx - The day context (weekday + period) to match against
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const timepoints = new Set(['08:00', '09:00', '10:00']);
20
+ * const appliedRuleIds = ['rule1'];
21
+ * applyManualExcludes(timepoints, appliedRuleIds, rules, { weekday: 1, yearPeriodId: 'school' });
22
+ * // timepoints might now be Set(['08:00', '10:00']) if a rule excluded '09:00'
23
+ * ```
24
+ */
25
+ export declare function applyManualExcludes(timepoints: Set<string>, appliedRuleIds: string[], manualRules: ManualRule[], ctx: DayContext): void;
26
+ /**
27
+ * Removes time points based on manual EXCLUDE rules that intersect with a replacement rule.
28
+ *
29
+ * This is the second phase of rule application for replacement days. Uses intersection
30
+ * matching: if an exclude rule matches ANY of the weekdays/periods targeted by the
31
+ * replacement, its time points are removed.
32
+ *
33
+ * Mutates the timepoints Set and appliedRuleIds array in place.
34
+ *
35
+ * @param timepoints - Set of time points to filter (mutated in place)
36
+ * @param appliedRuleIds - Array to track which rules were applied (mutated in place)
37
+ * @param replacement - The event replacement rule controlling this date
38
+ * @param manualRules - Array of all manual rules to check for intersection
39
+ */
40
+ export declare function applyReplacementManualExcludes(timepoints: Set<string>, appliedRuleIds: string[], replacement: EventReplacementRule, manualRules: ManualRule[]): void;
41
+ /**
42
+ * Applies event restriction rules to remove time points for a specific date.
43
+ *
44
+ * Event restrictions can work in three modes:
45
+ * 1. all_day: Removes all time points (complete service suspension)
46
+ * 2. explicit timepoints: Removes only the specified times (UI-generated)
47
+ * 3. time window: Removes times within start_time to end_time range (supports midnight crossing)
48
+ *
49
+ * This is the final filtering phase after manual includes/excludes have been processed.
50
+ *
51
+ * Mutates the timepoints Set and appliedRuleIds array in place.
52
+ *
53
+ * @param date - The operational date to check restrictions for
54
+ * @param timepoints - Set of time points to filter (mutated in place)
55
+ * @param appliedRuleIds - Array to track which rules were applied (mutated in place)
56
+ * @param rules - Array of all schedule rules (filters for event_restriction type)
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * const timepoints = new Set(['08:00', '09:00', '10:00']);
61
+ * const appliedRuleIds = [];
62
+ * applyEventRestrictions('2026-12-25', timepoints, appliedRuleIds, rules);
63
+ * // If there's an all_day restriction, timepoints is now Set()
64
+ * ```
65
+ */
66
+ export declare function applyEventRestrictions(date: OperationalDate, timepoints: Set<string>, appliedRuleIds: string[], rules: ScheduleRule[]): void;
@@ -0,0 +1,162 @@
1
+ import { manualRuleMatchesContext, manualRuleMatchesReplacement } from './matchers.js';
2
+ /**
3
+ * Removes time points based on manual EXCLUDE rules that match a given day context.
4
+ *
5
+ * This is the second phase of rule application for normal days. Finds all manual rules
6
+ * with operating_mode='exclude' that match the weekday and period, then removes their
7
+ * time points from the collected set.
8
+ *
9
+ * Mutates the timepoints Set and appliedRuleIds array in place.
10
+ *
11
+ * @param timepoints - Set of time points to filter (mutated in place)
12
+ * @param appliedRuleIds - Array to track which rules were applied (mutated in place)
13
+ * @param manualRules - Array of all manual rules to check
14
+ * @param ctx - The day context (weekday + period) to match against
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const timepoints = new Set(['08:00', '09:00', '10:00']);
19
+ * const appliedRuleIds = ['rule1'];
20
+ * applyManualExcludes(timepoints, appliedRuleIds, rules, { weekday: 1, yearPeriodId: 'school' });
21
+ * // timepoints might now be Set(['08:00', '10:00']) if a rule excluded '09:00'
22
+ * ```
23
+ */
24
+ export function applyManualExcludes(timepoints, appliedRuleIds, manualRules, ctx) {
25
+ for (const r of manualRules) {
26
+ if (r.operating_mode !== 'exclude')
27
+ continue;
28
+ if (!manualRuleMatchesContext(r, ctx))
29
+ continue;
30
+ appliedRuleIds.push(r._id);
31
+ // If exclude rule has no timepoints, treat as "exclude nothing"
32
+ // (If you want "exclude whole day", add an explicit flag later.)
33
+ for (const tp of r.timepoints ?? [])
34
+ timepoints.delete(tp);
35
+ }
36
+ }
37
+ /**
38
+ * Removes time points based on manual EXCLUDE rules that intersect with a replacement rule.
39
+ *
40
+ * This is the second phase of rule application for replacement days. Uses intersection
41
+ * matching: if an exclude rule matches ANY of the weekdays/periods targeted by the
42
+ * replacement, its time points are removed.
43
+ *
44
+ * Mutates the timepoints Set and appliedRuleIds array in place.
45
+ *
46
+ * @param timepoints - Set of time points to filter (mutated in place)
47
+ * @param appliedRuleIds - Array to track which rules were applied (mutated in place)
48
+ * @param replacement - The event replacement rule controlling this date
49
+ * @param manualRules - Array of all manual rules to check for intersection
50
+ */
51
+ export function applyReplacementManualExcludes(timepoints, appliedRuleIds, replacement, manualRules) {
52
+ for (const r of manualRules) {
53
+ if (r.operating_mode !== 'exclude')
54
+ continue;
55
+ if (!manualRuleMatchesReplacement(r, replacement))
56
+ continue;
57
+ if (r._id)
58
+ appliedRuleIds.push(r._id);
59
+ for (const tp of r.timepoints)
60
+ timepoints.delete(tp);
61
+ }
62
+ }
63
+ /**
64
+ * Converts HH:MM time string to total minutes since midnight.
65
+ * Helper function for time window calculations.
66
+ *
67
+ * @param hhmm - Time string in HH:MM format (validated by Zod)
68
+ * @returns Total minutes (e.g., "09:30" → 570)
69
+ */
70
+ function hhmmToMinutes(hhmm) {
71
+ const [h, m] = hhmm.split(':');
72
+ return Number(h) * 60 + Number(m);
73
+ }
74
+ /**
75
+ * Removes all time points that fall within a specified time window.
76
+ *
77
+ * Handles midnight crossing: if end < start, the window wraps around midnight
78
+ * and removes times in [start, 24:00) ∪ [00:00, end).
79
+ *
80
+ * Window is inclusive of start, exclusive of end: [start, end)
81
+ *
82
+ * Mutates the timepoints Set in place.
83
+ *
84
+ * @param timepoints - Set of time points to filter (mutated in place)
85
+ * @param startHHMM - Window start time in HH:MM format
86
+ * @param endHHMM - Window end time in HH:MM format
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const times = new Set(['08:00', '12:00', '22:00', '01:00']);
91
+ * // Remove times between 22:00 and 02:00 (crosses midnight)
92
+ * removeTimePointsByWindow(times, '22:00', '02:00');
93
+ * // times is now Set(['08:00', '12:00'])
94
+ * ```
95
+ */
96
+ function removeTimePointsByWindow(timepoints, startHHMM, endHHMM) {
97
+ const start = hhmmToMinutes(startHHMM);
98
+ const end = hhmmToMinutes(endHHMM);
99
+ const crossesMidnight = end < start;
100
+ for (const tp of Array.from(timepoints)) {
101
+ const t = hhmmToMinutes(tp);
102
+ const inWindow = crossesMidnight
103
+ ? (t >= start || t < end)
104
+ : (t >= start && t < end);
105
+ if (inWindow)
106
+ timepoints.delete(tp);
107
+ }
108
+ }
109
+ /**
110
+ * Applies event restriction rules to remove time points for a specific date.
111
+ *
112
+ * Event restrictions can work in three modes:
113
+ * 1. all_day: Removes all time points (complete service suspension)
114
+ * 2. explicit timepoints: Removes only the specified times (UI-generated)
115
+ * 3. time window: Removes times within start_time to end_time range (supports midnight crossing)
116
+ *
117
+ * This is the final filtering phase after manual includes/excludes have been processed.
118
+ *
119
+ * Mutates the timepoints Set and appliedRuleIds array in place.
120
+ *
121
+ * @param date - The operational date to check restrictions for
122
+ * @param timepoints - Set of time points to filter (mutated in place)
123
+ * @param appliedRuleIds - Array to track which rules were applied (mutated in place)
124
+ * @param rules - Array of all schedule rules (filters for event_restriction type)
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * const timepoints = new Set(['08:00', '09:00', '10:00']);
129
+ * const appliedRuleIds = [];
130
+ * applyEventRestrictions('2026-12-25', timepoints, appliedRuleIds, rules);
131
+ * // If there's an all_day restriction, timepoints is now Set()
132
+ * ```
133
+ */
134
+ export function applyEventRestrictions(date, timepoints, appliedRuleIds, rules) {
135
+ for (const r of rules) {
136
+ if (r.kind !== 'event_restriction')
137
+ continue;
138
+ if (!r.dates?.includes(date))
139
+ continue;
140
+ if (r._id)
141
+ appliedRuleIds.push(r._id);
142
+ // 1) all day kills everything
143
+ if (r?.all_day) {
144
+ timepoints.clear();
145
+ continue;
146
+ }
147
+ // 2) explicit timepoints removal (UI generated)
148
+ if (r.timepoints?.length) {
149
+ for (const tp of r.timepoints)
150
+ timepoints.delete(tp);
151
+ continue;
152
+ }
153
+ // 3) fallback: compute from window
154
+ // If start/end exist, remove anything within that time window.
155
+ // If somehow missing, do nothing.
156
+ const start = r?.start_time;
157
+ const end = r?.end_time;
158
+ if (start && end) {
159
+ removeTimePointsByWindow(timepoints, start, end);
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,41 @@
1
+ import type { RuleApplication } from './types.js';
2
+ import type { Event, OperationalDate, ScheduleRule, YearPeriod } from '@tmlmobilidade/types';
3
+ /**
4
+ * Calculates which time points are active for each date in a range, based on scheduling rules.
5
+ *
6
+ * This is the core scheduling algorithm that processes three types of rules:
7
+ * 1. Manual rules (include/exclude) - Define base schedules for weekday/period combinations
8
+ * 2. Event replacement rules - Override base schedules for specific dates
9
+ * 3. Event restriction rules - Remove specific time points or time windows from schedules
10
+ *
11
+ * Algorithm flow for each date:
12
+ * - Check for event replacement rule
13
+ * - If found: Use intersection-based matching with manual rules
14
+ * - Otherwise: Use context-based matching (weekday + period)
15
+ * - Collect INCLUDE time points
16
+ * - Apply EXCLUDE time points
17
+ * - Apply event restrictions (by date)
18
+ * - Return final time points and applied rule IDs
19
+ *
20
+ * @param rules - All scheduling rules to process (manual, replacement, restriction)
21
+ * @param dateRange - Array of operational dates to calculate schedules for
22
+ * @param periods - YearPeriod definitions (school term, summer, etc.) for context resolution
23
+ * @returns Map from CalendarKey to RuleApplication with active time points and metadata
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const rules = [...]; // Manual, replacement, and restriction rules
28
+ * const dates = ['2026-02-01', '2026-02-02', ...];
29
+ * const periods = [...]; // YearPeriod definitions
30
+ *
31
+ * const result = computeActiveTimePoints(rules, dates, periods);
32
+ * const feb1 = result.get(calendarKey(Dates.fromOperationalDate('2026-02-01')));
33
+ * // feb1 = {
34
+ * // timepoints: ['08:00', '09:00', '10:00'],
35
+ * // appliedRuleIds: ['rule1', 'rule2']
36
+ * // }
37
+ * ```
38
+ */
39
+ export declare function computeActiveRules(date: OperationalDate, rules: ScheduleRule[], periods: YearPeriod[], options?: {
40
+ events?: Event[];
41
+ }): RuleApplication;
@@ -0,0 +1,87 @@
1
+ import { calendarKey, calendarWeekday } from '../../utils/index.js';
2
+ import { Dates } from '../../../dates.js';
3
+ import { findReplacementForDate, getActivePeriodId } from '../utils/date.js';
4
+ import { collectManualIncludes, collectReplacementManualIncludes } from './collectors.js';
5
+ import { applyEventRestrictions, applyManualExcludes, applyReplacementManualExcludes } from './filters.js';
6
+ /* * */
7
+ /**
8
+ * Calculates which time points are active for each date in a range, based on scheduling rules.
9
+ *
10
+ * This is the core scheduling algorithm that processes three types of rules:
11
+ * 1. Manual rules (include/exclude) - Define base schedules for weekday/period combinations
12
+ * 2. Event replacement rules - Override base schedules for specific dates
13
+ * 3. Event restriction rules - Remove specific time points or time windows from schedules
14
+ *
15
+ * Algorithm flow for each date:
16
+ * - Check for event replacement rule
17
+ * - If found: Use intersection-based matching with manual rules
18
+ * - Otherwise: Use context-based matching (weekday + period)
19
+ * - Collect INCLUDE time points
20
+ * - Apply EXCLUDE time points
21
+ * - Apply event restrictions (by date)
22
+ * - Return final time points and applied rule IDs
23
+ *
24
+ * @param rules - All scheduling rules to process (manual, replacement, restriction)
25
+ * @param dateRange - Array of operational dates to calculate schedules for
26
+ * @param periods - YearPeriod definitions (school term, summer, etc.) for context resolution
27
+ * @returns Map from CalendarKey to RuleApplication with active time points and metadata
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const rules = [...]; // Manual, replacement, and restriction rules
32
+ * const dates = ['2026-02-01', '2026-02-02', ...];
33
+ * const periods = [...]; // YearPeriod definitions
34
+ *
35
+ * const result = computeActiveTimePoints(rules, dates, periods);
36
+ * const feb1 = result.get(calendarKey(Dates.fromOperationalDate('2026-02-01')));
37
+ * // feb1 = {
38
+ * // timepoints: ['08:00', '09:00', '10:00'],
39
+ * // appliedRuleIds: ['rule1', 'rule2']
40
+ * // }
41
+ * ```
42
+ */
43
+ export function computeActiveRules(date, rules, periods, options) {
44
+ const manualRules = rules.filter((r) => r.kind === 'manual');
45
+ const restrictionRules = rules.filter(r => r.kind === 'event_restriction');
46
+ const replacementRules = rules.filter((r) => r.kind === 'event_replacement');
47
+ const filteredManualRules = manualRules.filter((rule) => {
48
+ if (!rule.event_id)
49
+ return true;
50
+ const event = options?.events?.find(e => e._id === rule.event_id);
51
+ if (!event?.dates?.length)
52
+ return false;
53
+ return event.dates.includes(date);
54
+ });
55
+ const key = calendarKey(Dates.fromOperationalDate(date, 'Europe/Lisbon'));
56
+ const replacement = findReplacementForDate(date, replacementRules);
57
+ // 1) Base timepoints
58
+ let timepoints;
59
+ let appliedRuleIds;
60
+ if (replacement) {
61
+ // Replacement mode: match manuals by intersection (Option A)
62
+ const base = collectReplacementManualIncludes(replacement, filteredManualRules);
63
+ timepoints = base.timepoints;
64
+ appliedRuleIds = base.appliedRuleIds;
65
+ // Apply manual excludes *also* by intersection with replacement targets
66
+ applyReplacementManualExcludes(timepoints, appliedRuleIds, replacement, filteredManualRules);
67
+ }
68
+ else {
69
+ // Normal mode: day resolves to a single weekday + single yearPeriodId
70
+ const ctx = {
71
+ weekday: calendarWeekday(key),
72
+ yearPeriodId: getActivePeriodId(date, periods),
73
+ };
74
+ const base = collectManualIncludes(filteredManualRules, ctx);
75
+ timepoints = base.timepoints;
76
+ appliedRuleIds = base.appliedRuleIds;
77
+ applyManualExcludes(timepoints, appliedRuleIds, filteredManualRules, ctx);
78
+ }
79
+ // 2) Event restrictions always apply by date
80
+ applyEventRestrictions(date, timepoints, appliedRuleIds, restrictionRules);
81
+ const result = {
82
+ appliedRuleIds,
83
+ timepoints: Array.from(timepoints).sort() || [],
84
+ };
85
+ return result;
86
+ }
87
+ /* * */
@@ -0,0 +1,39 @@
1
+ import type { DayContext } from './types.js';
2
+ import { EventReplacementRule, ManualRule } from '@tmlmobilidade/types';
3
+ /**
4
+ * Determines if a manual rule matches the given day context.
5
+ *
6
+ * Uses strict AND logic: the rule must include BOTH the specified weekday
7
+ * AND the period ID to match. This is used for normal (non-replacement) days.
8
+ *
9
+ * @param rule - The manual rule to check
10
+ * @param ctx - The day context containing the weekday and period ID
11
+ * @returns True if the rule applies to this day context
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const rule = { weekdays: [1, 2, 3], year_period_ids: ['school'], ... };
16
+ * const ctx = { weekday: 1, yearPeriodId: 'school' };
17
+ * manualRuleMatchesContext(rule, ctx); // true
18
+ * ```
19
+ */
20
+ export declare function manualRuleMatchesContext(rule: ManualRule, ctx: DayContext): boolean;
21
+ /**
22
+ * Determines if a manual rule intersects with an event replacement rule's targets.
23
+ *
24
+ * Uses OR logic via set intersection: if the rule matches ANY weekday or period
25
+ * that the replacement targets, it's considered a match. This is used when a
26
+ * replacement rule overrides the normal schedule for certain weekdays/periods.
27
+ *
28
+ * @param rule - The manual rule to check
29
+ * @param replacement - The event replacement rule defining override targets
30
+ * @returns True if the rule intersects with the replacement's targets
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const rule = { weekdays: [1, 2], year_period_ids: ['school'], ... };
35
+ * const replacement = { weekdays: [1], year_period_ids: ['school', 'summer'], ... };
36
+ * manualRuleMatchesReplacement(rule, replacement); // true (weekday 1 matches)
37
+ * ```
38
+ */
39
+ export declare function manualRuleMatchesReplacement(rule: ManualRule, replacement: EventReplacementRule): boolean;