diff --git a/src/schedule.js b/src/schedule.js index 862dc55..1f56b76 100644 --- a/src/schedule.js +++ b/src/schedule.js @@ -23,5 +23,62 @@ * calendar date in 'YYYY-MM-DD' form. */ export function billingDates(start, rule) { - throw new Error('not implemented'); + const { every, interval = 1, count, until, skip = [] } = rule; + + if (count === undefined && until === undefined) { + throw new RangeError('count or until is required'); + } + if (every !== 'week' && every !== 'month') { + throw new RangeError("every must be 'week' or 'month'"); + } + if (!Number.isInteger(interval) || interval <= 0) { + throw new RangeError('interval must be a positive integer'); + } + if (count !== undefined && (!Number.isInteger(count) || count <= 0)) { + throw new RangeError('count must be a positive integer'); + } + + const startDate = parseDate(start); + const untilDate = until === undefined ? undefined : parseDate(until); + const skipped = new Set(skip); + const dates = []; + + for (let occurrence = 0; count === undefined || dates.length < count; occurrence++) { + const date = new Date(startDate); + + if (every === 'week') { + date.setUTCDate(date.getUTCDate() + occurrence * interval * 7); + } else { + // Always advance from the original month so shortened months don't move the anchor. + date.setUTCDate(1); + date.setUTCMonth(date.getUTCMonth() + occurrence * interval); + const lastDay = new Date(date); + lastDay.setUTCMonth(lastDay.getUTCMonth() + 1, 0); + date.setUTCDate(Math.min(startDate.getUTCDate(), lastDay.getUTCDate())); + } + + if (untilDate !== undefined && date > untilDate) break; + + const formatted = date.toISOString().slice(0, 10); + if (!skipped.has(formatted)) dates.push(formatted); + } + + return dates; +} + +function parseDate(value) { + const match = typeof value === 'string' && /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); + if (!match) throw new RangeError('date must be a real calendar date in YYYY-MM-DD form'); + + const year = Number(match[1]); + const month = Number(match[2]); + const day = Number(match[3]); + const date = new Date(0); + date.setUTCFullYear(year, month - 1, day); + + if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) { + throw new RangeError('date must be a real calendar date in YYYY-MM-DD form'); + } + + return date; }