@tmlmobilidade/dates 20260329.1522.22 → 20260330.1453.3

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,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);
@@ -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,5 @@
1
+ import { Event, Pattern, ScheduleRule } from '@tmlmobilidade/types';
2
+ /**
3
+ * Merges persisted pattern rules with event-derived rules relevant to this pattern.
4
+ */
5
+ export declare function resolvePatternRules(pattern: Pattern, events: Event[]): ScheduleRule[];
@@ -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,7 +30,7 @@ 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') {
@@ -60,13 +60,13 @@ export function computeRuleTimePoints(rule, allRules, periods, options) {
60
60
  const start = Dates.now('Europe/Lisbon').startOf('day').js_date;
61
61
  const end = Dates.fromJSDate(start).plus({ years: 1 }).js_date;
62
62
  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);
63
+ const withAll = computeScheduleMap(allRules, dateRange, periods, holidays, options?.events);
64
+ const withoutRule = computeScheduleMap(allRules.filter(r => r._id !== rule._id), dateRange, periods, holidays, options?.events);
65
65
  const removedTimePoints = new Set();
66
66
  // Check only dates that match the affected weekdays
67
67
  for (const opDate of dateRange) {
68
68
  const key = calendarKey(Dates.fromOperationalDate(opDate, 'Europe/Lisbon'));
69
- const weekday = calendarWeekday(key);
69
+ const weekday = calendarWeekday(key, holidays);
70
70
  // Only look at days that match the affected weekdays
71
71
  if (!affectedWeekdays.has(weekday))
72
72
  continue;
@@ -84,11 +84,11 @@ export function computeRuleTimePoints(rule, allRules, periods, options) {
84
84
  /**
85
85
  * Helper: computes schedule for each date in range
86
86
  */
87
- function computeScheduleMap(rules, dateRange, periods, events) {
87
+ function computeScheduleMap(rules, dateRange, periods, holidays, events) {
88
88
  const result = new Map();
89
89
  for (const date of dateRange) {
90
90
  const key = calendarKey(Dates.fromOperationalDate(date, 'Europe/Lisbon'));
91
- const application = computeActiveRules(date, rules, periods, { events });
91
+ const application = computeActiveRules(date, rules, periods, holidays, { events });
92
92
  result.set(key, { timepoints: application.timepoints });
93
93
  }
94
94
  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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/dates",
3
- "version": "20260329.1522.22",
3
+ "version": "20260330.1453.3",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"