diff --git a/src/schedule.js b/src/schedule.js index 862dc55..022d1e8 100644 --- a/src/schedule.js +++ b/src/schedule.js @@ -23,5 +23,74 @@ * calendar date in 'YYYY-MM-DD' form. */ export function billingDates(start, rule) { - throw new Error('not implemented'); + if (rule.every !== 'week' && rule.every !== 'month') { + throw new RangeError("every must be 'week' or 'month'"); + } + + const interval = rule.interval === undefined ? 1 : rule.interval; + if (!Number.isInteger(interval) || interval <= 0) { + throw new RangeError('interval must be a positive integer'); + } + + const hasCount = rule.count !== undefined; + if (!hasCount && rule.until === undefined) { + throw new RangeError('count or until is required'); + } + if (hasCount && (!Number.isInteger(rule.count) || rule.count <= 0)) { + throw new RangeError('count must be a positive integer'); + } + + const first = parseDate(start); + const last = rule.until === undefined ? undefined : parseDate(rule.until); + const skip = new Set(rule.skip ?? []); + const dates = []; + let occurrence = 0; + + while (!hasCount || dates.length < rule.count) { + const date = rule.every === 'week' + ? addDays(first, occurrence * interval * 7) + : addMonths(first, occurrence * interval); + + if (last !== undefined && date > last) break; + + const formatted = formatDate(date); + if (!skip.has(formatted)) dates.push(formatted); + occurrence += 1; + } + + return dates; +} + +function parseDate(value) { + if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) { + throw new RangeError('date must be a real calendar date in YYYY-MM-DD form'); + } + + const date = new Date(`${value}T00:00:00.000Z`); + if (Number.isNaN(date.getTime()) || formatDate(date) !== value) { + throw new RangeError('date must be a real calendar date in YYYY-MM-DD form'); + } + return date; +} + +function formatDate(date) { + return date.toISOString().slice(0, 10); +} + +function addDays(date, days) { + const result = new Date(date); + result.setUTCDate(result.getUTCDate() + days); + return result; +} + +function addMonths(date, months) { + const day = date.getUTCDate(); + const result = new Date(date); + result.setUTCDate(1); + result.setUTCMonth(result.getUTCMonth() + months); + + const lastDay = new Date(result); + lastDay.setUTCMonth(lastDay.getUTCMonth() + 1, 0); + result.setUTCDate(Math.min(day, lastDay.getUTCDate())); + return result; }