@tmlmobilidade/dates 20260408.1128.43 → 20260409.1552.16

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.
@@ -53,6 +53,9 @@ export function computeActiveRules(date, rules, periods, holidays, options) {
53
53
  return event.dates.includes(date);
54
54
  });
55
55
  const key = calendarKey(Dates.fromOperationalDate(date, 'Europe/Lisbon'));
56
+ // Filter out manual rules whose months list doesn't include the current month
57
+ const month = Number(key.slice(5, 7));
58
+ const monthFilteredManualRules = filteredManualRules.filter(rule => !rule.months?.length || rule.months.includes(month));
56
59
  const replacement = findReplacementForDate(date, replacementRules);
57
60
  // 1) Base timepoints
58
61
  let timepoints;
@@ -64,11 +67,11 @@ export function computeActiveRules(date, rules, periods, holidays, options) {
64
67
  ? { ...replacement, weekdays: [calendarWeekday(key, holidays)] }
65
68
  : replacement;
66
69
  // Replacement mode: match manuals by intersection (Option A)
67
- const base = collectReplacementManualIncludes(effectiveReplacement, filteredManualRules);
70
+ const base = collectReplacementManualIncludes(effectiveReplacement, monthFilteredManualRules);
68
71
  timepoints = base.timepoints;
69
72
  appliedRuleIds = base.appliedRuleIds;
70
73
  // Apply manual excludes *also* by intersection with replacement targets
71
- applyReplacementManualExcludes(timepoints, appliedRuleIds, effectiveReplacement, filteredManualRules);
74
+ applyReplacementManualExcludes(timepoints, appliedRuleIds, effectiveReplacement, monthFilteredManualRules);
72
75
  }
73
76
  else {
74
77
  // Normal mode: day resolves to a single weekday + single yearPeriodId
@@ -76,10 +79,10 @@ export function computeActiveRules(date, rules, periods, holidays, options) {
76
79
  weekday: calendarWeekday(key, holidays),
77
80
  yearPeriodId: getActivePeriodId(date, periods),
78
81
  };
79
- const base = collectManualIncludes(filteredManualRules, ctx);
82
+ const base = collectManualIncludes(monthFilteredManualRules, ctx);
80
83
  timepoints = base.timepoints;
81
84
  appliedRuleIds = base.appliedRuleIds;
82
- applyManualExcludes(timepoints, appliedRuleIds, filteredManualRules, ctx);
85
+ applyManualExcludes(timepoints, appliedRuleIds, monthFilteredManualRules, ctx);
83
86
  }
84
87
  // 2) Event restrictions always apply by date
85
88
  applyEventRestrictions(date, timepoints, appliedRuleIds, restrictionRules);
@@ -35,3 +35,15 @@ export declare function buildWeekdaysPart(rule: EventReplacementRule | ManualRul
35
35
  export declare function buildDayPeriodsPart(parameter: StopsParameterOverride, cfg: {
36
36
  mode: 'long' | 'short';
37
37
  }): string;
38
+ /**
39
+ * Builds the months portion of a rule summary.
40
+ *
41
+ * Handles smart formatting:
42
+ * - No months or all 12 selected: "Todos os meses" / "em todos os meses"
43
+ * - Single month: Shows month label
44
+ * - Multiple months: "N meses" (short) or lists them (long)
45
+ */
46
+ export declare function buildMonthsPart(rule: Pick<ManualRule, 'months'>, cfg?: {
47
+ mode: 'long' | 'short';
48
+ omitIfAll?: boolean;
49
+ }): string;
@@ -1,4 +1,4 @@
1
- import { DAY_PERIOD_LABELS, WEEKDAY_OPTIONS } from '@tmlmobilidade/types';
1
+ import { DAY_PERIOD_LABELS, MONTH_OPTIONS, WEEKDAY_OPTIONS } from '@tmlmobilidade/types';
2
2
  /**
3
3
  * Builds the period portion of a rule summary.
4
4
  *
@@ -93,3 +93,29 @@ export function buildDayPeriodsPart(parameter, cfg) {
93
93
  return `Durante ${labels[0]}`;
94
94
  return `Durante ${labels.join(' e ')}`;
95
95
  }
96
+ /**
97
+ * Builds the months portion of a rule summary.
98
+ *
99
+ * Handles smart formatting:
100
+ * - No months or all 12 selected: "Todos os meses" / "em todos os meses"
101
+ * - Single month: Shows month label
102
+ * - Multiple months: "N meses" (short) or lists them (long)
103
+ */
104
+ export function buildMonthsPart(rule, cfg = { mode: 'long' }) {
105
+ if (!rule.months?.length || rule.months.length === 12) {
106
+ if (cfg.omitIfAll)
107
+ return '';
108
+ return cfg.mode === 'short' ? 'Todos os meses' : 'em todos os meses';
109
+ }
110
+ const labels = MONTH_OPTIONS
111
+ .filter(o => rule.months.includes(o.value))
112
+ .map(o => o.label);
113
+ if (cfg.mode === 'short') {
114
+ return labels.length === 1 ? labels[0] : `${labels.length} meses`;
115
+ }
116
+ if (labels.length === 1)
117
+ return labels[0];
118
+ if (labels.length === 2)
119
+ return `${labels[0]} e ${labels[1]}`;
120
+ return labels.join(', ');
121
+ }
@@ -1,7 +1,7 @@
1
1
  import { Dates } from '../../../dates.js';
2
2
  import { FORMATS } from '../../../format.js';
3
- import { WEEKDAY_OPTIONS, } from '@tmlmobilidade/types';
4
- import { buildWeekdaysPart, buildYearPeriodsPart } from './common.js';
3
+ import { MONTH_OPTIONS, WEEKDAY_OPTIONS, } from '@tmlmobilidade/types';
4
+ import { buildMonthsPart, buildWeekdaysPart, buildYearPeriodsPart } from './common.js';
5
5
  /**
6
6
  * Builds human-readable summaries of a scheduling rule.
7
7
  *
@@ -69,6 +69,9 @@ function buildRuleSummaryShort(rule, options) {
69
69
  const periodPart = buildYearPeriodsPart(rule, options, { mode: 'short', omitIfAll: true });
70
70
  if (periodPart)
71
71
  parts.push(periodPart);
72
+ const monthsPart = buildMonthsPart(rule, { mode: 'short', omitIfAll: true });
73
+ if (monthsPart)
74
+ parts.push(monthsPart);
72
75
  const weekdayPart = buildWeekdaysPart(rule, { mode: 'short', omitIfAll: true });
73
76
  if (weekdayPart)
74
77
  parts.push(weekdayPart);
@@ -79,6 +82,9 @@ function buildRuleSummaryShort(rule, options) {
79
82
  const periodPart = buildYearPeriodsPart(rule, options, { mode: 'short' });
80
83
  if (periodPart)
81
84
  parts.push(periodPart);
85
+ const monthsPart = buildMonthsPart(rule, { mode: 'short', omitIfAll: true });
86
+ if (monthsPart)
87
+ parts.push(monthsPart);
82
88
  const weekdayPart = buildWeekdaysPart(rule, { mode: 'short' });
83
89
  if (weekdayPart)
84
90
  parts.push(weekdayPart);
@@ -104,6 +110,9 @@ function buildRuleSummaryLong(rule, options) {
104
110
  const periodPart = buildYearPeriodsPart(rule, options, { mode: 'long', omitIfAll: true });
105
111
  if (periodPart)
106
112
  parts.push(periodPart);
113
+ const monthsPart = buildMonthsPart(rule, { mode: 'long', omitIfAll: true });
114
+ if (monthsPart)
115
+ parts.push(monthsPart);
107
116
  const weekdayPart = buildWeekdaysPart(rule, { mode: 'long', omitIfAll: true });
108
117
  if (weekdayPart)
109
118
  parts.push(weekdayPart);
@@ -114,6 +123,9 @@ function buildRuleSummaryLong(rule, options) {
114
123
  const periodPart = buildYearPeriodsPart(rule, options, { mode: 'long' });
115
124
  if (periodPart)
116
125
  parts.push(periodPart);
126
+ const monthsPart = buildMonthsPart(rule, { mode: 'long', omitIfAll: true });
127
+ if (monthsPart)
128
+ parts.push(monthsPart);
117
129
  const weekdayPart = buildWeekdaysPart(rule, { mode: 'long' });
118
130
  if (weekdayPart)
119
131
  parts.push(weekdayPart);
@@ -235,6 +247,14 @@ function mapWeekdaysToGtfsAbbreviation(weekdays) {
235
247
  };
236
248
  return sorted.map(day => map[day] || String(day)).join('-');
237
249
  }
250
+ function mapMonthsToGtfsAbbreviation(months) {
251
+ if (!months?.length)
252
+ return null;
253
+ const sorted = [...months].sort((a, b) => a - b);
254
+ return sorted
255
+ .map(m => MONTH_OPTIONS.find(o => o.value === m)?.label.toUpperCase() ?? String(m))
256
+ .join('-');
257
+ }
238
258
  /**
239
259
  * GTFS-oriented rule token:
240
260
  * - FER_DU
@@ -253,27 +273,26 @@ export function buildRuleSummaryGtfs(rule, options) {
253
273
  const weekdays = rule.weekdays ?? [];
254
274
  const periodPart = mapPeriodsToGtfsAbbreviation(periodIds);
255
275
  const weekdayPart = mapWeekdaysToGtfsAbbreviation(weekdays);
276
+ const monthsPart = mapMonthsToGtfsAbbreviation(rule.months);
256
277
  if (rule.kind === 'manual' && rule.event_id) {
257
278
  const title = getEventForManualRule(rule, options?.events)?.title ?? rule.name ?? rule._id;
258
- if (periodPart === 'ALL' && weekdayPart === 'ALL') {
279
+ const tokenParts = [];
280
+ if (periodPart !== 'ALL')
281
+ tokenParts.push(periodPart);
282
+ if (monthsPart)
283
+ tokenParts.push(monthsPart);
284
+ if (weekdayPart !== 'ALL')
285
+ tokenParts.push(weekdayPart);
286
+ if (!tokenParts.length)
259
287
  return title;
260
- }
261
- if (periodPart === 'ALL') {
262
- return `${title}_${weekdayPart}`;
263
- }
264
- if (weekdayPart === 'ALL') {
265
- return `${title}_${periodPart}`;
266
- }
267
- return `${title}_${periodPart}_${weekdayPart}`;
288
+ return `${title}_${tokenParts.join('_')}`;
268
289
  }
269
- if (periodPart === 'ALL' && weekdayPart === 'ALL') {
290
+ const tokenParts = [periodPart];
291
+ if (monthsPart)
292
+ tokenParts.push(monthsPart);
293
+ if (weekdayPart !== 'ALL')
294
+ tokenParts.push(weekdayPart);
295
+ if (tokenParts.length === 1 && tokenParts[0] === 'ALL')
270
296
  return 'ALL';
271
- }
272
- if (periodPart === 'ALL') {
273
- return `ALL_${weekdayPart}`;
274
- }
275
- if (weekdayPart === 'ALL') {
276
- return periodPart;
277
- }
278
- return `${periodPart}_${weekdayPart}`;
297
+ return tokenParts.join('_');
279
298
  }
@@ -25,6 +25,12 @@ function ruleAppliesToCivilKey(rule, key, ctx) {
25
25
  return false;
26
26
  if (!rule.weekdays.includes(weekday))
27
27
  return false;
28
+ // 3) Months filter (optional)
29
+ if (rule.months?.length) {
30
+ const month = Number(key.slice(5, 7));
31
+ if (!rule.months.includes(month))
32
+ return false;
33
+ }
28
34
  return true;
29
35
  }
30
36
  // USED IN RuleCreateEditView
@@ -64,6 +70,12 @@ export function getManualRuleAffectedDates(rule, ctx) {
64
70
  if (!isInPeriod(rule, key, ctx))
65
71
  continue;
66
72
  }
73
+ // If months are specified, narrow event dates to matching months only
74
+ if (rule.months?.length) {
75
+ const month = Number(key.slice(5, 7));
76
+ if (!rule.months.includes(month))
77
+ continue;
78
+ }
67
79
  affected.push(key);
68
80
  }
69
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/dates",
3
- "version": "20260408.1128.43",
3
+ "version": "20260409.1552.16",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"