@tmlmobilidade/dates 20260612.1126.8 → 20260612.1446.12
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.
|
@@ -1,5 +1,21 @@
|
|
|
1
|
-
import type { RuleApplication } from './types.js';
|
|
2
|
-
import type { Event, Holiday, OperationalDate, ScheduleRule, YearPeriod } from '@tmlmobilidade/types';
|
|
1
|
+
import type { DayContext, RuleApplication } from './types.js';
|
|
2
|
+
import type { Event, EventReplacementRule, EventRestrictionRule, Holiday, ManualRule, OperationalDate, ScheduleRule, YearPeriod } from '@tmlmobilidade/types';
|
|
3
|
+
export interface ComputeActiveRulesOptions {
|
|
4
|
+
dateContext?: ActiveRulesDateContext;
|
|
5
|
+
eventById?: Map<string, Event>;
|
|
6
|
+
events?: Event[];
|
|
7
|
+
}
|
|
8
|
+
export interface ActiveRulesDateContext extends DayContext {
|
|
9
|
+
key: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SplitScheduleRules {
|
|
12
|
+
manualRules: ManualRule[];
|
|
13
|
+
replacementRules: EventReplacementRule[];
|
|
14
|
+
restrictionRules: EventRestrictionRule[];
|
|
15
|
+
}
|
|
16
|
+
export declare function buildActiveRulesEventIndex(events?: Event[]): Map<string, Event>;
|
|
17
|
+
export declare function buildActiveRulesDateContext(date: OperationalDate, periods: YearPeriod[], holidays: Holiday[]): ActiveRulesDateContext;
|
|
18
|
+
export declare function splitScheduleRules(rules: ScheduleRule[]): SplitScheduleRules;
|
|
3
19
|
/**
|
|
4
20
|
* Operating schedule for one operational date: merged timepoints and applied rule IDs.
|
|
5
21
|
*
|
|
@@ -25,6 +41,6 @@ import type { Event, Holiday, OperationalDate, ScheduleRule, YearPeriod } from '
|
|
|
25
41
|
* @param holidays - Holiday calendar for weekday resolution
|
|
26
42
|
* @returns Active timepoints and IDs of rules that contributed
|
|
27
43
|
*/
|
|
28
|
-
export declare function computeActiveRules(date: OperationalDate, rules: ScheduleRule[], periods: YearPeriod[], holidays: Holiday[], options?:
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
export declare function computeActiveRules(date: OperationalDate, rules: ScheduleRule[], periods: YearPeriod[], holidays: Holiday[], options?: ComputeActiveRulesOptions): RuleApplication;
|
|
45
|
+
export declare function computeActiveRulesFromSplit(date: OperationalDate, rules: SplitScheduleRules, periods: YearPeriod[], holidays: Holiday[], options?: ComputeActiveRulesOptions): RuleApplication;
|
|
46
|
+
export declare function computeActiveRuleCountFromSplit(date: OperationalDate, rules: SplitScheduleRules, periods: YearPeriod[], holidays: Holiday[], options?: ComputeActiveRulesOptions): number;
|
|
@@ -4,7 +4,95 @@ import { resolveEffectiveReplacement } from '../export-attribution/replacement.j
|
|
|
4
4
|
import { findReplacementForDate, getActivePeriodId } from '../utils/date.js';
|
|
5
5
|
import { collectManualIncludes, collectReplacementManualIncludes } from './collectors.js';
|
|
6
6
|
import { applyEventRestrictions, applyManualExcludes, applyReplacementManualExcludes } from './filters.js';
|
|
7
|
-
|
|
7
|
+
import { getTimepointsRemovedByEventRestriction } from './filters.js';
|
|
8
|
+
import { manualRuleMatchesContext, manualRuleMatchesReplacement } from './matchers.js';
|
|
9
|
+
export function buildActiveRulesEventIndex(events = []) {
|
|
10
|
+
return new Map(events.map(event => [event._id, event]));
|
|
11
|
+
}
|
|
12
|
+
export function buildActiveRulesDateContext(date, periods, holidays) {
|
|
13
|
+
const key = calendarKey(Dates.fromOperationalDate(date, 'Europe/Lisbon'));
|
|
14
|
+
return {
|
|
15
|
+
key,
|
|
16
|
+
weekday: calendarWeekday(key, holidays),
|
|
17
|
+
yearPeriodId: getActivePeriodId(date, periods),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function splitScheduleRules(rules) {
|
|
21
|
+
return {
|
|
22
|
+
manualRules: rules.filter((r) => r.kind === 'manual'),
|
|
23
|
+
replacementRules: rules.filter((r) => r.kind === 'event_replacement'),
|
|
24
|
+
restrictionRules: rules.filter((r) => r.kind === 'event_restriction'),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function resolveEventById(id, options) {
|
|
28
|
+
return options?.eventById?.get(id) ?? options?.events?.find(event => event._id === id);
|
|
29
|
+
}
|
|
30
|
+
function filterManualRulesByEventDate(manualRules, date, options) {
|
|
31
|
+
return manualRules.filter((rule) => {
|
|
32
|
+
if (!rule.event_id)
|
|
33
|
+
return true;
|
|
34
|
+
const event = resolveEventById(rule.event_id, options);
|
|
35
|
+
if (!event?.dates?.length)
|
|
36
|
+
return false;
|
|
37
|
+
return event.dates.includes(date);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function filterManualRulesByMonth(manualRules, month) {
|
|
41
|
+
return manualRules.filter(rule => !rule.months?.length || rule.months.includes(month));
|
|
42
|
+
}
|
|
43
|
+
function collectManualIncludeTimepoints(manualRules, ctx) {
|
|
44
|
+
const timepoints = new Set();
|
|
45
|
+
for (const rule of manualRules) {
|
|
46
|
+
if (rule.operating_mode !== 'include')
|
|
47
|
+
continue;
|
|
48
|
+
if (!manualRuleMatchesContext(rule, ctx))
|
|
49
|
+
continue;
|
|
50
|
+
for (const timepoint of rule.timepoints ?? [])
|
|
51
|
+
timepoints.add(timepoint);
|
|
52
|
+
}
|
|
53
|
+
return timepoints;
|
|
54
|
+
}
|
|
55
|
+
function collectReplacementManualIncludeTimepoints(replacement, manualRules) {
|
|
56
|
+
const timepoints = new Set();
|
|
57
|
+
for (const rule of manualRules) {
|
|
58
|
+
if (rule.operating_mode !== 'include')
|
|
59
|
+
continue;
|
|
60
|
+
if (!manualRuleMatchesReplacement(rule, replacement))
|
|
61
|
+
continue;
|
|
62
|
+
for (const timepoint of rule.timepoints)
|
|
63
|
+
timepoints.add(timepoint);
|
|
64
|
+
}
|
|
65
|
+
return timepoints;
|
|
66
|
+
}
|
|
67
|
+
function applyManualExcludeTimepoints(timepoints, manualRules, ctx) {
|
|
68
|
+
for (const rule of manualRules) {
|
|
69
|
+
if (rule.operating_mode !== 'exclude')
|
|
70
|
+
continue;
|
|
71
|
+
if (!manualRuleMatchesContext(rule, ctx))
|
|
72
|
+
continue;
|
|
73
|
+
for (const timepoint of rule.timepoints ?? [])
|
|
74
|
+
timepoints.delete(timepoint);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function applyReplacementManualExcludeTimepoints(timepoints, replacement, manualRules) {
|
|
78
|
+
for (const rule of manualRules) {
|
|
79
|
+
if (rule.operating_mode !== 'exclude')
|
|
80
|
+
continue;
|
|
81
|
+
if (!manualRuleMatchesReplacement(rule, replacement))
|
|
82
|
+
continue;
|
|
83
|
+
for (const timepoint of rule.timepoints)
|
|
84
|
+
timepoints.delete(timepoint);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function applyEventRestrictionTimepoints(date, timepoints, rules) {
|
|
88
|
+
for (const rule of rules) {
|
|
89
|
+
if (!rule.dates?.includes(date))
|
|
90
|
+
continue;
|
|
91
|
+
for (const timepoint of getTimepointsRemovedByEventRestriction(rule, timepoints)) {
|
|
92
|
+
timepoints.delete(timepoint);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
8
96
|
/**
|
|
9
97
|
* Operating schedule for one operational date: merged timepoints and applied rule IDs.
|
|
10
98
|
*
|
|
@@ -31,21 +119,16 @@ import { applyEventRestrictions, applyManualExcludes, applyReplacementManualExcl
|
|
|
31
119
|
* @returns Active timepoints and IDs of rules that contributed
|
|
32
120
|
*/
|
|
33
121
|
export function computeActiveRules(date, rules, periods, holidays, options) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!event?.dates?.length)
|
|
42
|
-
return false;
|
|
43
|
-
return event.dates.includes(date);
|
|
44
|
-
});
|
|
45
|
-
const key = calendarKey(Dates.fromOperationalDate(date, 'Europe/Lisbon'));
|
|
122
|
+
const splitRules = splitScheduleRules(rules);
|
|
123
|
+
return computeActiveRulesFromSplit(date, splitRules, periods, holidays, options);
|
|
124
|
+
}
|
|
125
|
+
export function computeActiveRulesFromSplit(date, rules, periods, holidays, options) {
|
|
126
|
+
const { manualRules, replacementRules, restrictionRules } = rules;
|
|
127
|
+
const filteredManualRules = filterManualRulesByEventDate(manualRules, date, options);
|
|
128
|
+
const key = options?.dateContext?.key ?? calendarKey(Dates.fromOperationalDate(date, 'Europe/Lisbon'));
|
|
46
129
|
// Filter out manual rules whose months list doesn't include the current month
|
|
47
130
|
const month = Number(key.slice(5, 7));
|
|
48
|
-
const monthFilteredManualRules = filteredManualRules
|
|
131
|
+
const monthFilteredManualRules = filterManualRulesByMonth(filteredManualRules, month);
|
|
49
132
|
const replacement = findReplacementForDate(date, replacementRules);
|
|
50
133
|
// 1) Base timepoints
|
|
51
134
|
let timepoints;
|
|
@@ -61,10 +144,7 @@ export function computeActiveRules(date, rules, periods, holidays, options) {
|
|
|
61
144
|
}
|
|
62
145
|
else {
|
|
63
146
|
// Normal mode: day resolves to a single weekday + single yearPeriodId
|
|
64
|
-
const ctx =
|
|
65
|
-
weekday: calendarWeekday(key, holidays),
|
|
66
|
-
yearPeriodId: getActivePeriodId(date, periods),
|
|
67
|
-
};
|
|
147
|
+
const ctx = options?.dateContext ?? buildActiveRulesDateContext(date, periods, holidays);
|
|
68
148
|
const base = collectManualIncludes(monthFilteredManualRules, ctx);
|
|
69
149
|
timepoints = base.timepoints;
|
|
70
150
|
appliedRuleIds = base.appliedRuleIds;
|
|
@@ -78,4 +158,24 @@ export function computeActiveRules(date, rules, periods, holidays, options) {
|
|
|
78
158
|
};
|
|
79
159
|
return result;
|
|
80
160
|
}
|
|
161
|
+
export function computeActiveRuleCountFromSplit(date, rules, periods, holidays, options) {
|
|
162
|
+
const filteredManualRules = filterManualRulesByEventDate(rules.manualRules, date, options);
|
|
163
|
+
const key = options?.dateContext?.key ?? calendarKey(Dates.fromOperationalDate(date, 'Europe/Lisbon'));
|
|
164
|
+
const month = Number(key.slice(5, 7));
|
|
165
|
+
const monthFilteredManualRules = filterManualRulesByMonth(filteredManualRules, month);
|
|
166
|
+
const replacement = findReplacementForDate(date, rules.replacementRules);
|
|
167
|
+
let timepoints;
|
|
168
|
+
if (replacement) {
|
|
169
|
+
const effectiveReplacement = resolveEffectiveReplacement(date, replacement, holidays);
|
|
170
|
+
timepoints = collectReplacementManualIncludeTimepoints(effectiveReplacement, monthFilteredManualRules);
|
|
171
|
+
applyReplacementManualExcludeTimepoints(timepoints, effectiveReplacement, monthFilteredManualRules);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const ctx = options?.dateContext ?? buildActiveRulesDateContext(date, periods, holidays);
|
|
175
|
+
timepoints = collectManualIncludeTimepoints(monthFilteredManualRules, ctx);
|
|
176
|
+
applyManualExcludeTimepoints(timepoints, monthFilteredManualRules, ctx);
|
|
177
|
+
}
|
|
178
|
+
applyEventRestrictionTimepoints(date, timepoints, rules.restrictionRules);
|
|
179
|
+
return timepoints.size;
|
|
180
|
+
}
|
|
81
181
|
/* * */
|
package/dist/vkm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildOperationalDateRange } from './calendar/rules/utils/date.js';
|
|
2
2
|
import { calendarWeekday } from './calendar/utils/index.js';
|
|
3
3
|
import { Dates } from './dates.js';
|
|
4
|
-
import {
|
|
4
|
+
import { buildActiveRulesDateContext, buildActiveRulesEventIndex, computeActiveRuleCountFromSplit, splitScheduleRules } from './calendar/rules/calculation/index.js';
|
|
5
5
|
import { resolvePatternRules } from './calendar/rules/merging/index.js';
|
|
6
6
|
const UNASSIGNED_PERIOD_KEY = '__unassigned__';
|
|
7
7
|
const UNASSIGNED_PERIOD_NAME = 'Sem período definido';
|
|
@@ -119,7 +119,11 @@ function buildCalculationContext(request, holidays, periods) {
|
|
|
119
119
|
const { endDate, startDate } = resolveCalculationEndDate(request);
|
|
120
120
|
const operationalDates = buildOperationalDateRange(startDate.js_date, endDate.js_date);
|
|
121
121
|
const { metadataByDate, relevantPeriods } = buildDayMetadata(operationalDates, holidays, periods);
|
|
122
|
-
|
|
122
|
+
const activeRulesContextByDate = new Map();
|
|
123
|
+
for (const date of operationalDates) {
|
|
124
|
+
activeRulesContextByDate.set(date, buildActiveRulesDateContext(date, periods, holidays));
|
|
125
|
+
}
|
|
126
|
+
return { activeRulesContextByDate, endDate, metadataByDate, operationalDates, relevantPeriods };
|
|
123
127
|
}
|
|
124
128
|
function buildCalculationInputs(agency, request, endDate) {
|
|
125
129
|
const totalVkmPerYear = agency.financials.vkm_per_month.reduce((sum, value) => sum + Number(value ?? 0), 0);
|
|
@@ -141,8 +145,9 @@ export function getPatternExtensionMeters(pattern, source) {
|
|
|
141
145
|
return Number(pattern.shape?.extension ?? 0);
|
|
142
146
|
}
|
|
143
147
|
export function calculateAgencyVkm({ agency, events, holidays, patterns, periods, request }) {
|
|
144
|
-
const { endDate, metadataByDate, operationalDates, relevantPeriods } = buildCalculationContext(request, holidays, periods);
|
|
148
|
+
const { activeRulesContextByDate, endDate, metadataByDate, operationalDates, relevantPeriods } = buildCalculationContext(request, holidays, periods);
|
|
145
149
|
const accumulator = createAccumulator(relevantPeriods);
|
|
150
|
+
const eventById = buildActiveRulesEventIndex(events);
|
|
146
151
|
for (const pattern of patterns) {
|
|
147
152
|
const extensionMeters = getPatternExtensionMeters(pattern, request.extension_source);
|
|
148
153
|
if (!extensionMeters)
|
|
@@ -150,9 +155,16 @@ export function calculateAgencyVkm({ agency, events, holidays, patterns, periods
|
|
|
150
155
|
const mergedRules = resolvePatternRules(pattern, events);
|
|
151
156
|
if (!mergedRules.length)
|
|
152
157
|
continue;
|
|
158
|
+
const splitRules = splitScheduleRules(mergedRules);
|
|
153
159
|
for (const date of operationalDates) {
|
|
154
|
-
const
|
|
155
|
-
|
|
160
|
+
const dateContext = activeRulesContextByDate.get(date);
|
|
161
|
+
if (!dateContext)
|
|
162
|
+
continue;
|
|
163
|
+
const activeTripCount = computeActiveRuleCountFromSplit(date, splitRules, periods, holidays, {
|
|
164
|
+
dateContext,
|
|
165
|
+
eventById,
|
|
166
|
+
events,
|
|
167
|
+
});
|
|
156
168
|
if (!activeTripCount)
|
|
157
169
|
continue;
|
|
158
170
|
const dayMetadata = metadataByDate.get(date);
|