diff --git a/src/schedule.js b/src/schedule.js index 862dc55..1e7c1a4 100644 --- a/src/schedule.js +++ b/src/schedule.js @@ -22,6 +22,69 @@ * positive integer, if `every` isn't 'week' or 'month', or if start or until isn't a real * calendar date in 'YYYY-MM-DD' form. */ +function parseDate(value) { + if (typeof value !== 'string') throw new RangeError('Invalid calendar date'); + + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); + if (!match) throw new RangeError('Invalid calendar date'); + + const date = new Date(`${value}T00:00:00.000Z`); + if ( + date.getUTCFullYear() !== Number(match[1]) || + date.getUTCMonth() !== Number(match[2]) - 1 || + date.getUTCDate() !== Number(match[3]) + ) { + throw new RangeError('Invalid calendar date'); + } + + return date; +} + +function addMonths(start, months) { + const monthIndex = start.getUTCMonth() + months; + const year = start.getUTCFullYear() + Math.floor(monthIndex / 12); + const month = monthIndex % 12; + const date = new Date(start); + + date.setUTCFullYear(year, month + 1, 0); + const lastDay = date.getUTCDate(); + date.setUTCFullYear(year, month, Math.min(start.getUTCDate(), lastDay)); + return date; +} + export function billingDates(start, rule) { - throw new Error('not implemented'); + const { every, count, until, skip = [] } = rule; + const interval = rule.interval === undefined ? 1 : rule.interval; + const hasCount = count !== undefined; + const hasUntil = until !== undefined; + + if (!hasCount && !hasUntil) throw new RangeError('count or until is required'); + if (every !== 'week' && every !== 'month') throw new RangeError('Invalid billing frequency'); + if (!Number.isInteger(interval) || interval <= 0) throw new RangeError('Invalid interval'); + if (hasCount && (!Number.isInteger(count) || count <= 0)) throw new RangeError('Invalid count'); + + const startDate = parseDate(start); + const untilDate = hasUntil ? parseDate(until) : null; + const skipped = new Set(skip); + const dates = []; + let current = startDate; + let months = 0; + + while ( + (!hasCount || dates.length < count) && + (!hasUntil || current <= untilDate) + ) { + const formatted = current.toISOString().slice(0, 10); + if (!skipped.has(formatted)) dates.push(formatted); + + if (every === 'week') { + current = new Date(current); + current.setUTCDate(current.getUTCDate() + interval * 7); + } else { + months += interval; + current = addMonths(startDate, months); + } + } + + return dates; }