• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

statuscompliance / status-backend / 15140027356

20 May 2025 02:18PM UTC coverage: 57.326% (+3.0%) from 54.282%
15140027356

Pull #90

github

web-flow
Merge 233d62bf9 into b7c3290a2
Pull Request #90: test(utils): added dates

489 of 957 branches covered (51.1%)

Branch coverage included in aggregate %.

111 of 115 new or added lines in 1 file covered. (96.52%)

1166 of 1930 relevant lines covered (60.41%)

10.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.95
/src/utils/dates.js
1
import { addYears, addMonths, addWeeks, addDays, parseISO, isWithinInterval, add, setSeconds, isSameDay, getDay, getDaysInMonth, getYear, getMonth, isValid } from 'date-fns';
2
import logger from '../config/logger.js';
3

4
// Period types with their corresponding date-fns functions
5
export const periodTypes = {
28✔
6
  yearly: addYears,
7
  monthly: addMonths,
8
  weekly: addWeeks,
9
  daily: addDays,
10
  hourly: (date) => new Date(date.setHours(date.getHours() + 1)),
3✔
11
};
12

13
export function getDates(from, to, period, customConfig) {
14
  if (!isValid(from)) {
18✔
15
    logger.error("Invalid 'from' date provided.");
1✔
16
    return [];
1✔
17
  }
18

19
  if (!isValid(to)) {
17✔
20
    logger.error("Invalid 'to' date provided.");
1✔
21
    return [];
1✔
22
  }
23
  
24
  if (period in periodTypes) {
16✔
25
    const periodFunction = periodTypes[period];
12✔
26
    let current = new Date(from);
12✔
27
    const dates = [];
12✔
28
    try {
12✔
29
      while (current <= to) {
12✔
30
        dates.push(new Date(current));
21✔
31
        current = periodFunction(current, 1);
21✔
32
      }
33
      return dates;
11✔
34
    } catch (error) {
35
      logger.error(`Error generating dates for period ${period}:`, { error });
1✔
36
      return [];
1✔
37
    }
38
  }
39

40
  if (period === 'customRules') {
4✔
41
    if (!customConfig?.rules || !customConfig?.Wto) {
3✔
42
      logger.error("Incomplete custom rules configuration: 'rules' and 'Wto' are required.");
2✔
43
      return [];
2✔
44
    }
45
    try {
1✔
46
      let rulesArr = customConfig.rules.split('---');
1✔
47
      let untilDate = `;UNTIL=${customConfig.Wto.toISOString().replace(/[.\-:]/g, '').substring(0, 15)}Z`;
1✔
NEW
48
      rulesArr = rulesArr.map(rule => rule + untilDate);
×
NEW
49
      return generateDatesFromRules(rulesArr, from, to);
×
50
    } catch (error) {
51
      logger.error('Error generating dates with custom rules:', { error });
1✔
52
      return [];
1✔
53
    }
54
  }
55

56
  logger.error(`Invalid period type: ${period}`);
1✔
57
  return [];
1✔
58
}
59

60
export function generateDatesFromRules(rulesArr, from, to) {
61
  const generatedDates = [];
3✔
62

63
  rulesArr.forEach(rule => {
3✔
64
    const ruleData = parseRule(rule);
3✔
65
    if (ruleData) {
3✔
66
      generatedDates.push(...generateDatesForFrequency(ruleData, from, to));
2✔
67
    }
68
  });
69

70
  return generatedDates;
3✔
71
}
72

73
export function parseRule(rule) {
74
  const dtstartMatch = rule.match(/DTSTART:(\d+T\d+)/);
10✔
75
  const rruleMatch = rule.match(/RRULE:(.+)/);
10✔
76

77
  if (!dtstartMatch || !rruleMatch) {
10✔
78
    return null;
3✔
79
  }
80

81
  const startDate = parseISO(dtstartMatch[1]);
7✔
82
  const rruleParts = rruleMatch[1].split(';');
7✔
83
  const frequency = rruleParts.find(part => part.startsWith('FREQ='))?.split('=')[1];
7✔
84
  const interval = parseInt(rruleParts.find(part => part.startsWith('INTERVAL='))?.split('=')[1] || '1');
14✔
85
  const byHour = rruleParts.find(part => part.startsWith('BYHOUR='))?.split('=')[1]?.split(',').map(Number) || [0];
15✔
86
  const until = rruleParts.find(part => part.startsWith('UNTIL='))?.split('=')[1];
16✔
87
  const byDay = rruleParts.find(part => part.startsWith('BYDAY='))?.split('=')[1]?.split(',');
16✔
88
  const byMonthDay = rruleParts.find(part => part.startsWith('BYMONTHDAY='))?.split('=')[1];
16✔
89
  const byMonth = rruleParts.find(part => part.startsWith('BYMONTH='))?.split('=')[1];
15✔
90

91
  return {
7✔
92
    startDate,
93
    frequency,
94
    interval,
95
    byHour,
96
    until,
97
    byDay,
98
    byMonthDay: byMonthDay ? parseInt(byMonthDay, 10) : undefined,
7✔
99
    byMonth: byMonth ? parseInt(byMonth, 10) - 1 : undefined, // Month in date-fns is 0-indexed
7✔
100
  };
101
}
102

103
export function generateDatesForFrequency(ruleData, from, to) {
104
  const generatedDates = [];
8✔
105
  let currentDate = new Date(ruleData.startDate);
8✔
106
  const untilDate = ruleData.until ? parseISO(ruleData.until) : to;
8✔
107
  const byHour = Array.isArray(ruleData.byHour) ? ruleData.byHour : [0];
8✔
108

109
  if (ruleData.frequency === 'WEEKLY' && ruleData.byDay) {
8✔
110
    currentDate = adjustToFirstWeeklyOccurrence(currentDate, ruleData.byDay);
1✔
111
  }
112

113
  while (isWithinInterval(currentDate, { start: from, end: untilDate }) || isSameDay(currentDate, untilDate)) {
8✔
114
    generateDatesForDay(currentDate, byHour, from, to, generatedDates);
17✔
115
    currentDate = advanceToNextDate(currentDate, ruleData);
17✔
116
    if (!currentDate) break;
17✔
117
  }
118

119
  return generatedDates.sort((a, b) => a.getTime() - b.getTime());
9✔
120
}
121

122
function adjustToFirstWeeklyOccurrence(currentDate, byDay) {
123
  const firstOccurrence = findNextWeeklyDate(new Date(add(currentDate, { days: -7 })), byDay, 1);
1✔
124
  if (firstOccurrence && firstOccurrence >= currentDate) {
1!
NEW
125
    return firstOccurrence;
×
126
  } else if (firstOccurrence && firstOccurrence < currentDate) {
1!
127
    return findNextWeeklyDate(new Date(currentDate), byDay, 1);
1✔
128
  }
NEW
129
  return currentDate;
×
130
}
131

132
function generateDatesForDay(currentDate, byHour, from, to, generatedDates) {
133
  byHour.forEach(hour => {
17✔
134
    const dateWithHour = adjustDateToHour(new Date(currentDate), hour);
17✔
135
    if (isDateWithinRange(dateWithHour, from, to)) {
17✔
136
      generatedDates.push(dateWithHour);
16✔
137
    }
138
  });
139
}
140

141
export function advanceToNextDate(currentDate, ruleData) {
142
  if (ruleData.frequency === 'DAILY') {
25✔
143
    return add(currentDate, { days: ruleData.interval || 1 });
14!
144
  } else if (ruleData.frequency === 'WEEKLY' && ruleData.byDay) {
11✔
145
    return findNextWeeklyDate(new Date(currentDate), ruleData.byDay, ruleData.interval || 1);
4!
146
  } else if (ruleData.frequency === 'MONTHLY' && ruleData.byMonthDay !== undefined) {
7✔
147
    return advanceToMonthlyDate(new Date(currentDate), ruleData.byMonthDay, ruleData.interval || 1);
1!
148
  } else if (ruleData.frequency === 'YEARLY' && ruleData.byMonth !== undefined && ruleData.byMonthDay !== undefined) {
6✔
149
    return advanceToYearlyDate(new Date(currentDate), ruleData.byMonth, ruleData.byMonthDay, ruleData.interval || 1);
1!
150
  } else {
151
    console.warn(`Unsupported frequency or missing parameters: ${ruleData.frequency}`);
5✔
152
    return null;
5✔
153
  }
154
}
155

156
export function adjustDateToHour(date, hour) {
157
  const dateWithHour = setSeconds(new Date(date), 0);
20✔
158
  dateWithHour.setHours(hour);
20✔
159
  return dateWithHour;
20✔
160
}
161

162
export function isDateWithinRange(date, from, to) {
163
  return isWithinInterval(date, { start: from, end: to }) || isSameDay(date, to);
25✔
164
}
165

166
export function findNextWeeklyDate(currentDate, byDay, interval) {
167
  let foundDay = false;
12✔
168
  let nextDate = new Date(currentDate);
12✔
169
  for (let i = 1; i <= 7; i++) {
12✔
170
    const potentialDate = add(currentDate, { days: i });
40✔
171
    const dayOfWeek = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'][getDay(potentialDate)];
40✔
172
    if (byDay.includes(dayOfWeek)) {
40✔
173
      nextDate = potentialDate;
11✔
174
      foundDay = true;
11✔
175
      break;
11✔
176
    }
177
  }
178

179
  if (foundDay) {
12✔
180
    const currentDayIndex = getDay(currentDate);
11✔
181
    const nextDayIndex = getDay(nextDate);
11✔
182
    // Calculate the number of days to add for the interval
183
    let daysToAdd = (interval - 1) * 7;
11✔
184
    if (nextDayIndex <= currentDayIndex) {
11✔
185
      daysToAdd += (7 - currentDayIndex + nextDayIndex);
4✔
186
    } else {
187
      daysToAdd += (nextDayIndex - currentDayIndex);
7✔
188
    }
189
    const finalNextDate = add(currentDate, { days: daysToAdd });
11✔
190
    return finalNextDate;
11✔
191
  }
192
  return null;
1✔
193
}
194

195
export function advanceToMonthlyDate(currentDate, byMonthDay, interval) {
196
  const currentYear = getYear(currentDate);
6✔
197
  const currentMonth = getMonth(currentDate);
6✔
198
  const nextMonth = currentMonth + interval;
6✔
199
  const nextYearBasedOnMonth = currentYear + Math.floor(nextMonth / 12);
6✔
200
  const finalMonth = nextMonth % 12;
6✔
201
  const daysInNextMonth = getDaysInMonth(new Date(nextYearBasedOnMonth, finalMonth));
6✔
202
  const dayOfMonth = Math.min(byMonthDay, daysInNextMonth);
6✔
203
  return new Date(nextYearBasedOnMonth, finalMonth, dayOfMonth, currentDate.getHours(), currentDate.getMinutes(), currentDate.getSeconds());
6✔
204
}
205

206
export function advanceToYearlyDate(currentDate, byMonth, byMonthDay, interval) {
207
  const nextYear = getYear(currentDate) + interval;
5✔
208
  const monthIndex = byMonth - 1;
5✔
209
  const daysInMonth = getDaysInMonth(new Date(nextYear, monthIndex));
5✔
210
  const dayOfMonth = Math.min(byMonthDay, daysInMonth);
5✔
211
  return new Date(nextYear, monthIndex, dayOfMonth, currentDate.getHours(), currentDate.getMinutes(), currentDate.getSeconds());
5✔
212
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc