@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
- events?: Event[];
30
- }): RuleApplication;
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 manualRules = rules.filter((r) => r.kind === 'manual');
35
- const restrictionRules = rules.filter(r => r.kind === 'event_restriction');
36
- const replacementRules = rules.filter((r) => r.kind === 'event_replacement');
37
- const filteredManualRules = manualRules.filter((rule) => {
38
- if (!rule.event_id)
39
- return true;
40
- const event = options?.events?.find(e => e._id === rule.event_id);
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.filter(rule => !rule.months?.length || rule.months.includes(month));
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 { computeActiveRules } from './calendar/rules/calculation/index.js';
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
- return { endDate, metadataByDate, operationalDates, relevantPeriods };
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 activeRules = computeActiveRules(date, mergedRules, periods, holidays, { events });
155
- const activeTripCount = activeRules.timepoints.length;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/dates",
3
- "version": "20260612.1126.8",
3
+ "version": "20260612.1446.12",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"