@tmlmobilidade/dates 20260331.1620.53 → 20260406.1418.25

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.
@@ -18,12 +18,12 @@
18
18
  export function manualRuleMatchesContext(rule, ctx) {
19
19
  if (rule.event_id) {
20
20
  // Event-exception rules: date matching already done by filteredManualRules in computeActiveRules.
21
- // year_period_ids is cleared for these rules, so skip that check.
22
21
  // If weekdays is non-empty, the user narrowed the event to specific days — check it.
23
- // If weekdays is empty, all days of the event are included.
24
- if (rule.weekdays.length > 0) {
25
- return rule.weekdays.includes(ctx.weekday);
26
- }
22
+ if (rule.weekdays.length > 0 && !rule.weekdays.includes(ctx.weekday))
23
+ return false;
24
+ // If year_period_ids is non-empty, the user narrowed the event to specific periods — check it.
25
+ if (rule.year_period_ids.length > 0 && !rule.year_period_ids.includes(ctx.yearPeriodId))
26
+ return false;
27
27
  return true;
28
28
  }
29
29
  // Strict by design: rule must include the weekday and the yearPeriodId.
@@ -9,8 +9,9 @@ import { EventReplacementRule, ManualRule, StopsParameterOverride, YearPeriod }
9
9
  */
10
10
  export declare function buildYearPeriodsPart(rule: EventReplacementRule | ManualRule | StopsParameterOverride, options: {
11
11
  periods?: YearPeriod[];
12
- }, cfg: {
12
+ }, cfg?: {
13
13
  mode: 'long' | 'short';
14
+ omitIfAll?: boolean;
14
15
  }): string;
15
16
  /**
16
17
  * Builds the weekday portion of a rule summary.
@@ -27,8 +28,9 @@ export declare function buildYearPeriodsPart(rule: EventReplacementRule | Manual
27
28
  * @param cfg - Format mode (short or long)
28
29
  * @returns Formatted weekday string in Portuguese
29
30
  */
30
- export declare function buildWeekdaysPart(rule: EventReplacementRule | ManualRule | StopsParameterOverride, cfg: {
31
+ export declare function buildWeekdaysPart(rule: EventReplacementRule | ManualRule | StopsParameterOverride, cfg?: {
31
32
  mode: 'long' | 'short';
33
+ omitIfAll?: boolean;
32
34
  }): string;
33
35
  export declare function buildDayPeriodsPart(parameter: StopsParameterOverride, cfg: {
34
36
  mode: 'long' | 'short';
@@ -7,11 +7,13 @@ import { DAY_PERIOD_LABELS, WEEKDAY_OPTIONS } from '@tmlmobilidade/types';
7
7
  * - Single period: Shows period name
8
8
  * - Multiple periods: "N períodos" (short) or "Durante os períodos de X e Y" (long)
9
9
  */
10
- export function buildYearPeriodsPart(rule, options, cfg) {
10
+ export function buildYearPeriodsPart(rule, options, cfg = { mode: 'long', omitIfAll: false }) {
11
11
  const allPeriodIds = options?.periods?.map(p => p._id) ?? [];
12
12
  const selectedPeriodIds = rule.year_period_ids || [];
13
13
  const isAll = allPeriodIds.length > 0 && selectedPeriodIds.length === allPeriodIds.length && allPeriodIds.every(id => selectedPeriodIds.includes(id));
14
14
  if (!selectedPeriodIds.length || isAll) {
15
+ if (cfg.omitIfAll)
16
+ return '';
15
17
  return cfg.mode === 'short' ? 'Todos os períodos' : 'Em todos os períodos';
16
18
  }
17
19
  const labels = selectedPeriodIds.map(id => options?.periods?.find(p => p._id === id)?.name ?? 'período desconhecido');
@@ -39,13 +41,12 @@ export function buildYearPeriodsPart(rule, options, cfg) {
39
41
  * @param cfg - Format mode (short or long)
40
42
  * @returns Formatted weekday string in Portuguese
41
43
  */
42
- export function buildWeekdaysPart(rule, cfg) {
43
- if (!rule.weekdays || rule.weekdays.length === 0) {
44
+ export function buildWeekdaysPart(rule, cfg = { mode: 'long' }) {
45
+ if (!rule.weekdays || rule.weekdays.length === 0 || rule.weekdays.length === 7) {
46
+ if (cfg.omitIfAll)
47
+ return '';
44
48
  return cfg.mode === 'short' ? 'Todos os dias' : 'em todos os dias';
45
49
  }
46
- if (rule.weekdays.length === 7) {
47
- return 'Todos os dias';
48
- }
49
50
  const weekdaySet = new Set(rule.weekdays);
50
51
  const hasMonFri = [1, 2, 3, 4, 5].every(d => weekdaySet.has(d));
51
52
  const hasWeekend = weekdaySet.has(6) && weekdaySet.has(7);
@@ -18,21 +18,6 @@ export interface RuleSummary {
18
18
  * - **short**: Compact label for badges/pills (e.g., "Dias úteis · Período Escolar")
19
19
  * - **long**: Full description for tooltips (e.g., "Durante o Período Escolar, nos dias úteis")
20
20
  * - **tooltip**: Detailed event information with dates and times
21
- *
22
- * @param rule - The scheduling rule to summarize
23
- * @param options - Configuration options (periods array for name resolution)
24
- * @returns RuleSummary with short, long, and tooltip text
25
- *
26
- * @example
27
- * ```ts
28
- * const rule = { kind: 'manual', weekdays: [1,2,3,4,5], year_period_ids: ['school'], ... };
29
- * const summary = buildRuleSummary(rule, { periods });
30
- * // summary = {
31
- * // short: "Dias úteis · Período Escolar",
32
- * // long: "Durante o Período Escolar, nos dias úteis",
33
- * // tooltip: ""
34
- * // }
35
- * ```
36
21
  */
37
22
  export declare function buildRuleSummary(rule: ScheduleRule, options: {
38
23
  events?: Event[];
@@ -46,7 +31,7 @@ export declare function buildRuleSummary(rule: ScheduleRule, options: {
46
31
  * - ALL
47
32
  * - ALL_DU
48
33
  * - VER-FER_SAB-DOM
49
- * - event title for event rules (temporary)
34
+ * - Rock in Rio_VER_DU
50
35
  */
51
36
  export declare function buildRuleSummaryGtfs(rule: ScheduleRule, options: {
52
37
  events?: Event[];
@@ -1,6 +1,6 @@
1
1
  import { Dates } from '../../../dates.js';
2
2
  import { FORMATS } from '../../../format.js';
3
- import { WEEKDAY_OPTIONS } from '@tmlmobilidade/types';
3
+ import { WEEKDAY_OPTIONS, } from '@tmlmobilidade/types';
4
4
  import { buildWeekdaysPart, buildYearPeriodsPart } from './common.js';
5
5
  /**
6
6
  * Builds human-readable summaries of a scheduling rule.
@@ -9,21 +9,6 @@ import { buildWeekdaysPart, buildYearPeriodsPart } from './common.js';
9
9
  * - **short**: Compact label for badges/pills (e.g., "Dias úteis · Período Escolar")
10
10
  * - **long**: Full description for tooltips (e.g., "Durante o Período Escolar, nos dias úteis")
11
11
  * - **tooltip**: Detailed event information with dates and times
12
- *
13
- * @param rule - The scheduling rule to summarize
14
- * @param options - Configuration options (periods array for name resolution)
15
- * @returns RuleSummary with short, long, and tooltip text
16
- *
17
- * @example
18
- * ```ts
19
- * const rule = { kind: 'manual', weekdays: [1,2,3,4,5], year_period_ids: ['school'], ... };
20
- * const summary = buildRuleSummary(rule, { periods });
21
- * // summary = {
22
- * // short: "Dias úteis · Período Escolar",
23
- * // long: "Durante o Período Escolar, nos dias úteis",
24
- * // tooltip: ""
25
- * // }
26
- * ```
27
12
  */
28
13
  export function buildRuleSummary(rule, options) {
29
14
  return {
@@ -46,27 +31,48 @@ const getEventForManualRule = (rule, events) => {
46
31
  return events?.find(event => event._id === rule.event_id);
47
32
  };
48
33
  /* ---------------- helpers ---------------- */
34
+ function dateMatchesWeekdays(date, weekdays) {
35
+ if (!weekdays?.length)
36
+ return true;
37
+ const jsDay = Dates.fromOperationalDate(date, 'Europe/Lisbon').js_date.getDay();
38
+ const isoWeekday = (jsDay === 0 ? 7 : jsDay);
39
+ return weekdays.includes(isoWeekday);
40
+ }
41
+ function dateMatchesPeriods(date, yearPeriodIds, periods) {
42
+ if (!yearPeriodIds?.length)
43
+ return true;
44
+ const allowedDates = new Set(periods
45
+ ?.filter(p => yearPeriodIds.includes(p._id))
46
+ .flatMap(p => p.dates ?? []) ?? []);
47
+ return allowedDates.has(date);
48
+ }
49
49
  /**
50
50
  * Builds the short summary format for a rule.
51
51
  *
52
- * Event rules: Returns event title
53
- * Manual rules: Returns "YearPeriod · Weekdays" format
52
+ * Event restriction / replacement rules: event title
53
+ * Manual rules:
54
+ * - event-based: "Event · Period · Weekdays"
55
+ * - normal: "Period · Weekdays"
54
56
  */
55
57
  function buildRuleSummaryShort(rule, options) {
56
58
  if (isEventRestriction(rule)) {
57
- // Restriction: show event name
58
59
  return rule.event?.title ?? '';
59
60
  }
60
61
  if (isEventReplacement(rule)) {
61
- // Replacement: show event name
62
62
  return rule.event?.title ?? '';
63
63
  }
64
64
  if (rule.kind === 'manual' && rule.event_id) {
65
65
  const title = getEventForManualRule(rule, options?.events)?.title ?? '';
66
- if (!rule.weekdays?.length)
67
- return title;
68
- const weekdayPart = buildWeekdaysPart(rule, { mode: 'short' });
69
- return [title, weekdayPart].filter(Boolean).join(' · ');
66
+ const parts = [];
67
+ if (title)
68
+ parts.push(title);
69
+ const periodPart = buildYearPeriodsPart(rule, options, { mode: 'short', omitIfAll: true });
70
+ if (periodPart)
71
+ parts.push(periodPart);
72
+ const weekdayPart = buildWeekdaysPart(rule, { mode: 'short', omitIfAll: true });
73
+ if (weekdayPart)
74
+ parts.push(weekdayPart);
75
+ return parts.join(' · ');
70
76
  }
71
77
  // manual
72
78
  const parts = [];
@@ -81,8 +87,10 @@ function buildRuleSummaryShort(rule, options) {
81
87
  /**
82
88
  * Builds the long summary format for a rule.
83
89
  *
84
- * Event rules: Returns event title
85
- * Manual rules: Returns "During [period], on [weekdays]" format in Portuguese
90
+ * Event restriction / replacement rules: event title
91
+ * Manual rules:
92
+ * - event-based: "Event, Period, Weekdays"
93
+ * - normal: "Period, Weekdays"
86
94
  */
87
95
  function buildRuleSummaryLong(rule, options) {
88
96
  if (isEventReplacement(rule) || isEventRestriction(rule)) {
@@ -90,10 +98,16 @@ function buildRuleSummaryLong(rule, options) {
90
98
  }
91
99
  if (rule.kind === 'manual' && rule.event_id) {
92
100
  const title = getEventForManualRule(rule, options?.events)?.title ?? '';
93
- if (!rule.weekdays?.length)
94
- return title;
95
- const weekdayPart = buildWeekdaysPart(rule, { mode: 'long' });
96
- return [title, weekdayPart].filter(Boolean).join(', ');
101
+ const parts = [];
102
+ if (title)
103
+ parts.push(title);
104
+ const periodPart = buildYearPeriodsPart(rule, options, { mode: 'long', omitIfAll: true });
105
+ if (periodPart)
106
+ parts.push(periodPart);
107
+ const weekdayPart = buildWeekdaysPart(rule, { mode: 'long', omitIfAll: true });
108
+ if (weekdayPart)
109
+ parts.push(weekdayPart);
110
+ return parts.join(', ');
97
111
  }
98
112
  // manual
99
113
  const parts = [];
@@ -128,7 +142,7 @@ function truncateDates(dates, max = 5) {
128
142
  *
129
143
  * Restriction rules: "Oferta excluída [on dates] [time window]"
130
144
  * Replacement rules: "Funcionará como [weekdays] · [periods] · [dates]"
131
- * Manual rules: Returns empty string (no tooltip needed)
145
+ * Manual event rules: filtered event dates based on weekdays and/or periods
132
146
  */
133
147
  function buildRuleSummaryTooltip(rule, options) {
134
148
  if (isEventRestriction(rule)) {
@@ -161,11 +175,9 @@ function buildRuleSummaryTooltip(rule, options) {
161
175
  if (rule.kind === 'manual' && rule.event_id) {
162
176
  const event = getEventForManualRule(rule, options?.events);
163
177
  const filteredDates = (event?.dates ?? []).filter((date) => {
164
- if (!rule.weekdays?.length)
165
- return true;
166
- const jsDay = Dates.fromOperationalDate(date, 'Europe/Lisbon').js_date.getDay();
167
- const isoWeekday = (jsDay === 0 ? 7 : jsDay);
168
- return rule.weekdays.includes(isoWeekday);
178
+ const matchesWeekdays = dateMatchesWeekdays(date, rule.weekdays);
179
+ const matchesPeriods = dateMatchesPeriods(date, rule.year_period_ids, options?.periods);
180
+ return matchesWeekdays && matchesPeriods;
169
181
  });
170
182
  const dates = truncateDates(filteredDates.map(formatDateWithWeekday));
171
183
  if (!dates)
@@ -231,19 +243,29 @@ function mapWeekdaysToGtfsAbbreviation(weekdays) {
231
243
  * - ALL
232
244
  * - ALL_DU
233
245
  * - VER-FER_SAB-DOM
234
- * - event title for event rules (temporary)
246
+ * - Rock in Rio_VER_DU
235
247
  */
236
248
  export function buildRuleSummaryGtfs(rule, options) {
237
249
  if (isEventRestriction(rule) || isEventReplacement(rule)) {
238
250
  return rule.event?.title ?? rule.name ?? rule._id;
239
251
  }
240
- if (rule.kind === 'manual' && rule.event_id) {
241
- return getEventForManualRule(rule, options?.events)?.title ?? rule.name ?? rule._id;
242
- }
243
252
  const periodIds = rule.year_period_ids ?? [];
244
253
  const weekdays = rule.weekdays ?? [];
245
254
  const periodPart = mapPeriodsToGtfsAbbreviation(periodIds);
246
255
  const weekdayPart = mapWeekdaysToGtfsAbbreviation(weekdays);
256
+ if (rule.kind === 'manual' && rule.event_id) {
257
+ const title = getEventForManualRule(rule, options?.events)?.title ?? rule.name ?? rule._id;
258
+ if (periodPart === 'ALL' && weekdayPart === 'ALL') {
259
+ 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}`;
268
+ }
247
269
  if (periodPart === 'ALL' && weekdayPart === 'ALL') {
248
270
  return 'ALL';
249
271
  }
@@ -59,6 +59,11 @@ export function getManualRuleAffectedDates(rule, ctx) {
59
59
  if (!rule.weekdays.includes(weekday))
60
60
  continue;
61
61
  }
62
+ // If year periods are specified, narrow event dates to matching periods only
63
+ if (rule.year_period_ids?.length) {
64
+ if (!isInPeriod(rule, key, ctx))
65
+ continue;
66
+ }
62
67
  affected.push(key);
63
68
  }
64
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/dates",
3
- "version": "20260331.1620.53",
3
+ "version": "20260406.1418.25",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"