diff --git a/src/schedule.js b/src/schedule.js index 862dc55..e4476be 100644 --- a/src/schedule.js +++ b/src/schedule.js @@ -23,5 +23,80 @@ * 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 (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 && until === undefined) { + throw new RangeError('count or until is required'); + } + 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 = []; + let date = startDate; + let monthsElapsed = 0; + + while ( + (count === undefined || dates.length < count) && + (untilDate === undefined || date <= untilDate) + ) { + const formatted = formatDate(date); + if (!skipped.has(formatted)) dates.push(formatted); + + if (count !== undefined && dates.length === count) break; + + if (every === 'week') { + date = calendarDate( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate() + interval * 7, + ); + } else { + monthsElapsed += interval; + const monthIndex = startDate.getUTCFullYear() * 12 + startDate.getUTCMonth() + monthsElapsed; + const year = Math.floor(monthIndex / 12); + const month = monthIndex % 12; + const lastDay = calendarDate(year, month + 1, 0).getUTCDate(); + date = calendarDate(year, month, Math.min(startDate.getUTCDate(), lastDay)); + } + } + + 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]) - 1; + const day = Number(match[3]); + const date = calendarDate(year, month, day); + + if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day) { + throw new RangeError('date must be a real calendar date in YYYY-MM-DD form'); + } + return date; +} + +function calendarDate(year, month, day) { + const date = new Date(0); + date.setUTCFullYear(year, month, day); + return date; +} + +function formatDate(date) { + const year = String(date.getUTCFullYear()).padStart(4, '0'); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); + const day = String(date.getUTCDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; }