@tmlmobilidade/dates 20260329.1522.22 → 20260330.1735.58
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.
- package/dist/calendar/rules/calculation/index.d.ts +2 -2
- package/dist/calendar/rules/calculation/index.js +3 -3
- package/dist/calendar/rules/formatting/rule-summary.d.ts +14 -0
- package/dist/calendar/rules/formatting/rule-summary.js +80 -0
- package/dist/calendar/rules/index.d.ts +1 -0
- package/dist/calendar/rules/index.js +1 -0
- package/dist/calendar/rules/merging/index.d.ts +5 -0
- package/dist/calendar/rules/merging/index.js +81 -0
- package/dist/calendar/rules/preview/affectedDates.d.ts +3 -1
- package/dist/calendar/rules/preview/affectedDates.js +2 -2
- package/dist/calendar/rules/preview/buildDayDetails.d.ts +2 -2
- package/dist/calendar/rules/preview/buildDayDetails.js +4 -4
- package/dist/calendar/rules/preview/computeRuleTimePoints.d.ts +2 -2
- package/dist/calendar/rules/preview/computeRuleTimePoints.js +17 -24
- package/dist/calendar/utils/index.d.ts +3 -2
- package/dist/calendar/utils/index.js +10 -2
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RuleApplication } from './types.js';
|
|
2
|
-
import type { Event, OperationalDate, ScheduleRule, YearPeriod } from '@tmlmobilidade/types';
|
|
2
|
+
import type { Event, Holiday, OperationalDate, ScheduleRule, YearPeriod } from '@tmlmobilidade/types';
|
|
3
3
|
/**
|
|
4
4
|
* Calculates which time points are active for each date in a range, based on scheduling rules.
|
|
5
5
|
*
|
|
@@ -36,6 +36,6 @@ import type { Event, OperationalDate, ScheduleRule, YearPeriod } from '@tmlmobil
|
|
|
36
36
|
* // }
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
|
-
export declare function computeActiveRules(date: OperationalDate, rules: ScheduleRule[], periods: YearPeriod[], options?: {
|
|
39
|
+
export declare function computeActiveRules(date: OperationalDate, rules: ScheduleRule[], periods: YearPeriod[], holidays: Holiday[], options?: {
|
|
40
40
|
events?: Event[];
|
|
41
41
|
}): RuleApplication;
|
|
@@ -40,7 +40,7 @@ import { applyEventRestrictions, applyManualExcludes, applyReplacementManualExcl
|
|
|
40
40
|
* // }
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
|
-
export function computeActiveRules(date, rules, periods, options) {
|
|
43
|
+
export function computeActiveRules(date, rules, periods, holidays, options) {
|
|
44
44
|
const manualRules = rules.filter((r) => r.kind === 'manual');
|
|
45
45
|
const restrictionRules = rules.filter(r => r.kind === 'event_restriction');
|
|
46
46
|
const replacementRules = rules.filter((r) => r.kind === 'event_replacement');
|
|
@@ -61,7 +61,7 @@ export function computeActiveRules(date, rules, periods, options) {
|
|
|
61
61
|
// When same_weekday is true, each event date functions as its own actual weekday
|
|
62
62
|
// within the replacement's target periods, rather than all dates acting as one fixed weekday.
|
|
63
63
|
const effectiveReplacement = replacement.same_weekday
|
|
64
|
-
? { ...replacement, weekdays: [calendarWeekday(key)] }
|
|
64
|
+
? { ...replacement, weekdays: [calendarWeekday(key, holidays)] }
|
|
65
65
|
: replacement;
|
|
66
66
|
// Replacement mode: match manuals by intersection (Option A)
|
|
67
67
|
const base = collectReplacementManualIncludes(effectiveReplacement, filteredManualRules);
|
|
@@ -73,7 +73,7 @@ export function computeActiveRules(date, rules, periods, options) {
|
|
|
73
73
|
else {
|
|
74
74
|
// Normal mode: day resolves to a single weekday + single yearPeriodId
|
|
75
75
|
const ctx = {
|
|
76
|
-
weekday: calendarWeekday(key),
|
|
76
|
+
weekday: calendarWeekday(key, holidays),
|
|
77
77
|
yearPeriodId: getActivePeriodId(date, periods),
|
|
78
78
|
};
|
|
79
79
|
const base = collectManualIncludes(filteredManualRules, ctx);
|
|
@@ -38,3 +38,17 @@ export declare function buildRuleSummary(rule: ScheduleRule, options: {
|
|
|
38
38
|
events?: Event[];
|
|
39
39
|
periods?: YearPeriod[];
|
|
40
40
|
}): RuleSummary;
|
|
41
|
+
/**
|
|
42
|
+
* GTFS-oriented rule token:
|
|
43
|
+
* - FER_DU
|
|
44
|
+
* - VER_SAB
|
|
45
|
+
* - ESC_DOM
|
|
46
|
+
* - ALL
|
|
47
|
+
* - ALL_DU
|
|
48
|
+
* - VER-FER_SAB-DOM
|
|
49
|
+
* - event title for event rules (temporary)
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildRuleSummaryGtfs(rule: ScheduleRule, options: {
|
|
52
|
+
events?: Event[];
|
|
53
|
+
periods?: YearPeriod[];
|
|
54
|
+
}): string;
|
|
@@ -175,3 +175,83 @@ function buildRuleSummaryTooltip(rule, options) {
|
|
|
175
175
|
}
|
|
176
176
|
return '';
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* TEMP:
|
|
180
|
+
* Maps year period ids/names into GTFS abbreviations.
|
|
181
|
+
* Replace with a persisted abbreviation/code field when available.
|
|
182
|
+
*/
|
|
183
|
+
function mapPeriodsToGtfsAbbreviation(periodIds) {
|
|
184
|
+
if (!periodIds?.length)
|
|
185
|
+
return 'ALL';
|
|
186
|
+
const map = {
|
|
187
|
+
'2KIUJ': 'FER',
|
|
188
|
+
'99H2R': 'ESC',
|
|
189
|
+
'UW2U0': 'VER',
|
|
190
|
+
};
|
|
191
|
+
const abbreviations = periodIds.map((id) => {
|
|
192
|
+
const abbr = map[id];
|
|
193
|
+
if (!abbr)
|
|
194
|
+
throw new Error(`Unknown period id: ${id}`);
|
|
195
|
+
return abbr;
|
|
196
|
+
});
|
|
197
|
+
const unique = [...new Set(abbreviations)];
|
|
198
|
+
const allSet = new Set(['ESC', 'FER', 'VER']);
|
|
199
|
+
// If all three are present, return 'ALL'
|
|
200
|
+
if (unique.length === 3 && unique.every(x => allSet.has(x))) {
|
|
201
|
+
return 'ALL';
|
|
202
|
+
}
|
|
203
|
+
return unique.join('-');
|
|
204
|
+
}
|
|
205
|
+
function mapWeekdaysToGtfsAbbreviation(weekdays) {
|
|
206
|
+
if (!weekdays?.length)
|
|
207
|
+
return 'ALL';
|
|
208
|
+
const sorted = [...new Set(weekdays)].sort((a, b) => a - b);
|
|
209
|
+
const joined = sorted.join('-');
|
|
210
|
+
// Special cases
|
|
211
|
+
if (joined === '1-2-3-4-5')
|
|
212
|
+
return 'DU';
|
|
213
|
+
if (joined === '1-2-3-4-5-6-7')
|
|
214
|
+
return 'ALL';
|
|
215
|
+
const map = {
|
|
216
|
+
1: 'SEG',
|
|
217
|
+
2: 'TER',
|
|
218
|
+
3: 'QUA',
|
|
219
|
+
4: 'QUI',
|
|
220
|
+
5: 'SEX',
|
|
221
|
+
6: 'SAB',
|
|
222
|
+
7: 'DOM',
|
|
223
|
+
};
|
|
224
|
+
return sorted.map(day => map[day] || String(day)).join('-');
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* GTFS-oriented rule token:
|
|
228
|
+
* - FER_DU
|
|
229
|
+
* - VER_SAB
|
|
230
|
+
* - ESC_DOM
|
|
231
|
+
* - ALL
|
|
232
|
+
* - ALL_DU
|
|
233
|
+
* - VER-FER_SAB-DOM
|
|
234
|
+
* - event title for event rules (temporary)
|
|
235
|
+
*/
|
|
236
|
+
export function buildRuleSummaryGtfs(rule, options) {
|
|
237
|
+
if (isEventRestriction(rule) || isEventReplacement(rule)) {
|
|
238
|
+
return rule.event?.title ?? rule.name ?? rule._id;
|
|
239
|
+
}
|
|
240
|
+
if (rule.kind === 'manual' && rule.event_id) {
|
|
241
|
+
return getEventForManualRule(rule, options?.events)?.title ?? rule.name ?? rule._id;
|
|
242
|
+
}
|
|
243
|
+
const periodIds = rule.year_period_ids ?? [];
|
|
244
|
+
const weekdays = rule.weekdays ?? [];
|
|
245
|
+
const periodPart = mapPeriodsToGtfsAbbreviation(periodIds);
|
|
246
|
+
const weekdayPart = mapWeekdaysToGtfsAbbreviation(weekdays);
|
|
247
|
+
if (periodPart === 'ALL' && weekdayPart === 'ALL') {
|
|
248
|
+
return 'ALL';
|
|
249
|
+
}
|
|
250
|
+
if (periodPart === 'ALL') {
|
|
251
|
+
return `ALL_${weekdayPart}`;
|
|
252
|
+
}
|
|
253
|
+
if (weekdayPart === 'ALL') {
|
|
254
|
+
return periodPart;
|
|
255
|
+
}
|
|
256
|
+
return `${periodPart}_${weekdayPart}`;
|
|
257
|
+
}
|
|
@@ -2,6 +2,7 @@ export * from './calculation/index.js';
|
|
|
2
2
|
export * from './calculation/types.js';
|
|
3
3
|
export * from './formatting/parameter-summary.js';
|
|
4
4
|
export * from './formatting/rule-summary.js';
|
|
5
|
+
export * from './merging/index.js';
|
|
5
6
|
export * from './preview/affectedDates.js';
|
|
6
7
|
export * from './preview/buildDayDetails.js';
|
|
7
8
|
export * from './preview/computeRuleTimePoints.js';
|
|
@@ -2,6 +2,7 @@ export * from './calculation/index.js';
|
|
|
2
2
|
export * from './calculation/types.js';
|
|
3
3
|
export * from './formatting/parameter-summary.js';
|
|
4
4
|
export * from './formatting/rule-summary.js';
|
|
5
|
+
export * from './merging/index.js';
|
|
5
6
|
export * from './preview/affectedDates.js';
|
|
6
7
|
export * from './preview/buildDayDetails.js';
|
|
7
8
|
export * from './preview/computeRuleTimePoints.js';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
function eventRuleAffectsLine(rule, lineId) {
|
|
2
|
+
switch (rule.lines_mode) {
|
|
3
|
+
case 'all':
|
|
4
|
+
return true;
|
|
5
|
+
case 'exclude':
|
|
6
|
+
return !(rule.lines_to_exclude ?? []).includes(lineId);
|
|
7
|
+
case 'include':
|
|
8
|
+
return (rule.lines_to_include ?? []).includes(lineId);
|
|
9
|
+
default:
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function buildEventDerivedRestriction(args) {
|
|
14
|
+
const { event, pattern, rule } = args;
|
|
15
|
+
if (!rule.dates?.length)
|
|
16
|
+
return null;
|
|
17
|
+
if (!eventRuleAffectsLine(rule, pattern.line_id))
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
_id: `event:${event._id}:rule:${rule._id || 'unnamed'}`,
|
|
21
|
+
all_day: rule.all_day,
|
|
22
|
+
dates: rule.dates,
|
|
23
|
+
end_time: rule.end_time,
|
|
24
|
+
event: {
|
|
25
|
+
id: event._id,
|
|
26
|
+
title: event.title,
|
|
27
|
+
},
|
|
28
|
+
kind: 'event_restriction',
|
|
29
|
+
lines_mode: rule.lines_mode,
|
|
30
|
+
lines_to_exclude: rule.lines_to_exclude,
|
|
31
|
+
lines_to_include: rule.lines_to_include,
|
|
32
|
+
name: rule.name,
|
|
33
|
+
start_time: rule.start_time,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function buildEventDerivedReplacement(args) {
|
|
37
|
+
const { event, pattern, rule } = args;
|
|
38
|
+
if (!rule.dates?.length)
|
|
39
|
+
return null;
|
|
40
|
+
if (!eventRuleAffectsLine(rule, pattern.line_id))
|
|
41
|
+
return null;
|
|
42
|
+
if (!rule.weekdays?.length && !rule.year_period_ids?.length)
|
|
43
|
+
return null;
|
|
44
|
+
return {
|
|
45
|
+
_id: `event:${event._id}:rule:${rule._id || 'unnamed'}`,
|
|
46
|
+
dates: rule.dates,
|
|
47
|
+
event: {
|
|
48
|
+
id: event._id,
|
|
49
|
+
title: event.title,
|
|
50
|
+
},
|
|
51
|
+
kind: 'event_replacement',
|
|
52
|
+
lines_mode: rule.lines_mode,
|
|
53
|
+
lines_to_exclude: rule.lines_to_exclude,
|
|
54
|
+
lines_to_include: rule.lines_to_include,
|
|
55
|
+
name: rule.name,
|
|
56
|
+
same_weekday: rule.same_weekday,
|
|
57
|
+
weekdays: rule.weekdays,
|
|
58
|
+
year_period_ids: rule.year_period_ids,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Merges persisted pattern rules with event-derived rules relevant to this pattern.
|
|
63
|
+
*/
|
|
64
|
+
export function resolvePatternRules(pattern, events) {
|
|
65
|
+
const derivedRules = [];
|
|
66
|
+
for (const event of events) {
|
|
67
|
+
for (const rule of event.rules ?? []) {
|
|
68
|
+
if (rule.kind === 'event_restriction') {
|
|
69
|
+
const derived = buildEventDerivedRestriction({ event, pattern, rule });
|
|
70
|
+
if (derived)
|
|
71
|
+
derivedRules.push(derived);
|
|
72
|
+
}
|
|
73
|
+
else if (rule.kind === 'event_replacement') {
|
|
74
|
+
const derived = buildEventDerivedReplacement({ event, pattern, rule });
|
|
75
|
+
if (derived)
|
|
76
|
+
derivedRules.push(derived);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return [...(pattern.rules ?? []), ...derivedRules];
|
|
81
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Event, ManualRule, YearPeriod } from '@tmlmobilidade/types';
|
|
1
|
+
import { Event, Holiday, ManualRule, YearPeriod } from '@tmlmobilidade/types';
|
|
2
2
|
/**
|
|
3
3
|
* Context for computing which dates a manual rule affects within a calendar range.
|
|
4
4
|
*/
|
|
@@ -7,6 +7,8 @@ interface CalendarContext {
|
|
|
7
7
|
endDate: Date;
|
|
8
8
|
/** Optional events list for event_id matching */
|
|
9
9
|
events?: Event[];
|
|
10
|
+
/** Available holidays for accurate weekday calculations */
|
|
11
|
+
holidays: Holiday[];
|
|
10
12
|
/** Available periods for matching rule criteria */
|
|
11
13
|
periods: YearPeriod[];
|
|
12
14
|
/** Start date of the calendar range */
|
|
@@ -16,7 +16,7 @@ function isInPeriod(rule, key, ctx) {
|
|
|
16
16
|
* Checks both period membership and weekday matching.
|
|
17
17
|
*/
|
|
18
18
|
function ruleAppliesToCivilKey(rule, key, ctx) {
|
|
19
|
-
const weekday = calendarWeekday(key);
|
|
19
|
+
const weekday = calendarWeekday(key, ctx.holidays);
|
|
20
20
|
// 1) YearPeriod required
|
|
21
21
|
if (!isInPeriod(rule, key, ctx))
|
|
22
22
|
return false;
|
|
@@ -55,7 +55,7 @@ export function getManualRuleAffectedDates(rule, ctx) {
|
|
|
55
55
|
if (key >= from && key <= to) {
|
|
56
56
|
// If weekdays are specified, narrow event dates to matching weekdays only
|
|
57
57
|
if (rule.weekdays?.length) {
|
|
58
|
-
const weekday = calendarWeekday(key);
|
|
58
|
+
const weekday = calendarWeekday(key, ctx.holidays);
|
|
59
59
|
if (!rule.weekdays.includes(weekday))
|
|
60
60
|
continue;
|
|
61
61
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { CalendarKey } from '../../utils/index.js';
|
|
2
2
|
import { Dates } from '../../../dates.js';
|
|
3
|
-
import { type Event, type ScheduleRule, type YearPeriod } from '@tmlmobilidade/types';
|
|
3
|
+
import { type Event, Holiday, type ScheduleRule, type YearPeriod } from '@tmlmobilidade/types';
|
|
4
4
|
import { DayScheduleDetail } from './types.js';
|
|
5
5
|
/**
|
|
6
6
|
* Build schedule details for all affected days in a date range.
|
|
7
7
|
* Only includes days that have active timepoints or applied rules.
|
|
8
8
|
*/
|
|
9
|
-
export declare function buildAffectedDaysDetails(startDate: Dates, endDate: Dates, allRules: ScheduleRule[], periods: YearPeriod[], options?: {
|
|
9
|
+
export declare function buildAffectedDaysDetails(startDate: Dates, endDate: Dates, allRules: ScheduleRule[], periods: YearPeriod[], holidays: Holiday[], options?: {
|
|
10
10
|
events?: Event[];
|
|
11
11
|
}): Map<CalendarKey, DayScheduleDetail>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { calendarKey, datesFromCalendarKey } from '../../utils/index.js';
|
|
2
2
|
import { timeToMinutes } from '@tmlmobilidade/types';
|
|
3
3
|
import { computeActiveRules } from '../calculation/index.js';
|
|
4
|
-
function buildDayScheduleDetail(key, allRules, periods, events) {
|
|
4
|
+
function buildDayScheduleDetail(key, allRules, periods, holidays, events) {
|
|
5
5
|
const date = datesFromCalendarKey(key);
|
|
6
|
-
const { appliedRuleIds, timepoints: finalTimePoints } = computeActiveRules(date.operational_date, allRules, periods, { events });
|
|
6
|
+
const { appliedRuleIds, timepoints: finalTimePoints } = computeActiveRules(date.operational_date, allRules, periods, holidays, { events });
|
|
7
7
|
const appliedRules = appliedRuleIds
|
|
8
8
|
.map(id => allRules.find(r => r._id === id))
|
|
9
9
|
.filter((r) => !!r);
|
|
@@ -86,12 +86,12 @@ function isTimeInRange(time, start, end) {
|
|
|
86
86
|
* Build schedule details for all affected days in a date range.
|
|
87
87
|
* Only includes days that have active timepoints or applied rules.
|
|
88
88
|
*/
|
|
89
|
-
export function buildAffectedDaysDetails(startDate, endDate, allRules, periods, options) {
|
|
89
|
+
export function buildAffectedDaysDetails(startDate, endDate, allRules, periods, holidays, options) {
|
|
90
90
|
const affectedDays = new Map();
|
|
91
91
|
let currentDate = startDate.startOf('day');
|
|
92
92
|
while (currentDate.unix_timestamp <= endDate.unix_timestamp) {
|
|
93
93
|
const key = calendarKey(currentDate);
|
|
94
|
-
const dayDetails = buildDayScheduleDetail(key, allRules, periods, options?.events);
|
|
94
|
+
const dayDetails = buildDayScheduleDetail(key, allRules, periods, holidays, options?.events);
|
|
95
95
|
// Only include days that are "affected" (have active timepoints or applied rules)
|
|
96
96
|
if (dayDetails.finalTimePoints.length > 0
|
|
97
97
|
|| dayDetails.includeRules.length > 0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Event, ScheduleRule, YearPeriod } from '@tmlmobilidade/types';
|
|
1
|
+
import type { Event, Holiday, ScheduleRule, YearPeriod } from '@tmlmobilidade/types';
|
|
2
2
|
/**
|
|
3
3
|
* Computes which timepoints are affected by a specific rule.
|
|
4
4
|
* Returns the set of timepoints that should be displayed for this rule in the schedule grid.
|
|
@@ -8,6 +8,6 @@ import type { Event, ScheduleRule, YearPeriod } from '@tmlmobilidade/types';
|
|
|
8
8
|
* - Event replacement rules: return timepoints that apply on the target weekdays
|
|
9
9
|
* - Event restriction rules: return timepoints that are removed on the affected weekdays
|
|
10
10
|
*/
|
|
11
|
-
export declare function computeRuleTimePoints(rule: ScheduleRule, allRules: ScheduleRule[], periods: YearPeriod[], options?: {
|
|
11
|
+
export declare function computeRuleTimePoints(rule: ScheduleRule, allRules: ScheduleRule[], periods: YearPeriod[], holidays: Holiday[], options?: {
|
|
12
12
|
events?: Event[];
|
|
13
13
|
}): Set<string>;
|
|
@@ -12,7 +12,7 @@ import { buildOperationalDateRange } from '../utils/date.js';
|
|
|
12
12
|
* - Event replacement rules: return timepoints that apply on the target weekdays
|
|
13
13
|
* - Event restriction rules: return timepoints that are removed on the affected weekdays
|
|
14
14
|
*/
|
|
15
|
-
export function computeRuleTimePoints(rule, allRules, periods, options) {
|
|
15
|
+
export function computeRuleTimePoints(rule, allRules, periods, holidays, options) {
|
|
16
16
|
// Manual rules: just return their timepoints
|
|
17
17
|
if (rule.kind === 'manual') {
|
|
18
18
|
if (!rule.event_id)
|
|
@@ -30,43 +30,36 @@ export function computeRuleTimePoints(rule, allRules, periods, options) {
|
|
|
30
30
|
const affectedWeekdays = new Set();
|
|
31
31
|
for (const opDate of relevantDates) {
|
|
32
32
|
const key = calendarKey(Dates.fromOperationalDate(opDate, 'Europe/Lisbon'));
|
|
33
|
-
affectedWeekdays.add(calendarWeekday(key));
|
|
33
|
+
affectedWeekdays.add(calendarWeekday(key, holidays));
|
|
34
34
|
}
|
|
35
35
|
// For replacement rules: return timepoints from the TARGET weekdays
|
|
36
36
|
if (rule.kind === 'event_replacement') {
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
continue;
|
|
47
|
-
// Check if this manual rule applies to the target weekdays/periods
|
|
48
|
-
const hasMatchingWeekday = r.weekdays?.some(w => targetWeekdays.has(w));
|
|
49
|
-
const hasMatchingPeriod = r.year_period_ids?.some(p => targetPeriods.has(p));
|
|
50
|
-
if (hasMatchingWeekday && hasMatchingPeriod) {
|
|
51
|
-
for (const tp of r.timepoints || []) {
|
|
52
|
-
timepoints.add(tp);
|
|
37
|
+
const addedTimepoints = new Set();
|
|
38
|
+
for (const date of rule.dates ?? []) {
|
|
39
|
+
const withRule = computeActiveRules(date, allRules, periods, holidays, options);
|
|
40
|
+
const withoutRule = computeActiveRules(date, allRules.filter(r => r._id !== rule._id), periods, holidays, options);
|
|
41
|
+
const withSet = new Set(withRule.timepoints);
|
|
42
|
+
const withoutSet = new Set(withoutRule.timepoints);
|
|
43
|
+
for (const tp of withSet) {
|
|
44
|
+
if (!withoutSet.has(tp)) {
|
|
45
|
+
addedTimepoints.add(tp);
|
|
53
46
|
}
|
|
54
47
|
}
|
|
55
48
|
}
|
|
56
|
-
return
|
|
49
|
+
return addedTimepoints;
|
|
57
50
|
}
|
|
58
51
|
// For restriction rules: compute what was removed on affected weekdays
|
|
59
52
|
// Generate a sample date range to check (1 year from now)
|
|
60
53
|
const start = Dates.now('Europe/Lisbon').startOf('day').js_date;
|
|
61
54
|
const end = Dates.fromJSDate(start).plus({ years: 1 }).js_date;
|
|
62
55
|
const dateRange = buildOperationalDateRange(start, end);
|
|
63
|
-
const withAll = computeScheduleMap(allRules, dateRange, periods, options?.events);
|
|
64
|
-
const withoutRule = computeScheduleMap(allRules.filter(r => r._id !== rule._id), dateRange, periods, options?.events);
|
|
56
|
+
const withAll = computeScheduleMap(allRules, dateRange, periods, holidays, options?.events);
|
|
57
|
+
const withoutRule = computeScheduleMap(allRules.filter(r => r._id !== rule._id), dateRange, periods, holidays, options?.events);
|
|
65
58
|
const removedTimePoints = new Set();
|
|
66
59
|
// Check only dates that match the affected weekdays
|
|
67
60
|
for (const opDate of dateRange) {
|
|
68
61
|
const key = calendarKey(Dates.fromOperationalDate(opDate, 'Europe/Lisbon'));
|
|
69
|
-
const weekday = calendarWeekday(key);
|
|
62
|
+
const weekday = calendarWeekday(key, holidays);
|
|
70
63
|
// Only look at days that match the affected weekdays
|
|
71
64
|
if (!affectedWeekdays.has(weekday))
|
|
72
65
|
continue;
|
|
@@ -84,11 +77,11 @@ export function computeRuleTimePoints(rule, allRules, periods, options) {
|
|
|
84
77
|
/**
|
|
85
78
|
* Helper: computes schedule for each date in range
|
|
86
79
|
*/
|
|
87
|
-
function computeScheduleMap(rules, dateRange, periods, events) {
|
|
80
|
+
function computeScheduleMap(rules, dateRange, periods, holidays, events) {
|
|
88
81
|
const result = new Map();
|
|
89
82
|
for (const date of dateRange) {
|
|
90
83
|
const key = calendarKey(Dates.fromOperationalDate(date, 'Europe/Lisbon'));
|
|
91
|
-
const application = computeActiveRules(date, rules, periods, { events });
|
|
84
|
+
const application = computeActiveRules(date, rules, periods, holidays, { events });
|
|
92
85
|
result.set(key, { timepoints: application.timepoints });
|
|
93
86
|
}
|
|
94
87
|
return result;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Dates } from '../../dates.js';
|
|
2
2
|
import { TimezoneIdentified } from '../../types.js';
|
|
3
|
-
import { IsoWeekday } from '@tmlmobilidade/types';
|
|
3
|
+
import { Holiday, IsoWeekday } from '@tmlmobilidade/types';
|
|
4
4
|
export declare const DEFAULT_CAL_TZ = "Europe/Lisbon";
|
|
5
5
|
export type CalendarKey = `${number}-${number}-${number}`;
|
|
6
6
|
export interface CalendarDay {
|
|
@@ -53,10 +53,11 @@ export declare function keyToYYYYMMDD(key: CalendarKey): string;
|
|
|
53
53
|
* Convert "YYYYMMDD" to CalendarKey "YYYY-MM-DD".
|
|
54
54
|
*/
|
|
55
55
|
export declare function yyyymmddToKey(yyyymmdd: string): CalendarKey;
|
|
56
|
+
export declare function isHoliday(input: CalendarKey | Dates, holidays: Holiday[], tz?: TimezoneIdentified): boolean;
|
|
56
57
|
/**
|
|
57
58
|
* ISO weekday (1..7 Mon..Sun) for a calendar key or Dates (civil day).
|
|
58
59
|
*/
|
|
59
|
-
export declare function calendarWeekday(input: CalendarKey | Dates, tz?: TimezoneIdentified): IsoWeekday;
|
|
60
|
+
export declare function calendarWeekday(input: CalendarKey | Dates, holidays: Holiday[] | null, tz?: TimezoneIdentified): IsoWeekday;
|
|
60
61
|
export declare function isWeekend(input: CalendarKey | Dates, tz?: TimezoneIdentified): boolean;
|
|
61
62
|
/**
|
|
62
63
|
* Iterate calendar day keys from start to end inclusive (civil days).
|
|
@@ -56,16 +56,24 @@ export function keyToYYYYMMDD(key) {
|
|
|
56
56
|
export function yyyymmddToKey(yyyymmdd) {
|
|
57
57
|
return `${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`;
|
|
58
58
|
}
|
|
59
|
+
export function isHoliday(input, holidays, tz = DEFAULT_CAL_TZ) {
|
|
60
|
+
const key = input instanceof Dates ? calendarKey(input, tz) : input;
|
|
61
|
+
const yyyymmdd = keyToYYYYMMDD(key);
|
|
62
|
+
return holidays.some(holiday => holiday.dates.includes(yyyymmdd));
|
|
63
|
+
}
|
|
59
64
|
/**
|
|
60
65
|
* ISO weekday (1..7 Mon..Sun) for a calendar key or Dates (civil day).
|
|
61
66
|
*/
|
|
62
|
-
export function calendarWeekday(input, tz = DEFAULT_CAL_TZ) {
|
|
67
|
+
export function calendarWeekday(input, holidays, tz = DEFAULT_CAL_TZ) {
|
|
63
68
|
const key = input instanceof Dates ? calendarKey(input, tz) : input;
|
|
69
|
+
if (holidays && isHoliday(key, holidays, tz)) {
|
|
70
|
+
return 7;
|
|
71
|
+
}
|
|
64
72
|
const d = Dates.fromFormat(`${key} 12:00`, 'yyyy-MM-dd HH:mm', tz);
|
|
65
73
|
return Number(d.toFormat('E'));
|
|
66
74
|
}
|
|
67
75
|
export function isWeekend(input, tz = DEFAULT_CAL_TZ) {
|
|
68
|
-
const wd = calendarWeekday(input, tz);
|
|
76
|
+
const wd = calendarWeekday(input, null, tz);
|
|
69
77
|
return wd === 6 || wd === 7;
|
|
70
78
|
}
|
|
71
79
|
/**
|