diff --git a/src/schedule.js b/src/schedule.js index 862dc55..be72728 100644 --- a/src/schedule.js +++ b/src/schedule.js @@ -23,5 +23,61 @@ * calendar date in 'YYYY-MM-DD' form. */ export function billingDates(start, rule) { - throw new Error('not implemented'); + if (rule === null || typeof rule !== 'object') throw new RangeError('rule is required'); + const { every, until, skip = [] } = rule; + const interval = rule.interval === undefined ? 1 : rule.interval; + const count = rule.count; + + if (every !== 'week' && every !== 'month') throw new RangeError("every must be 'week' or 'month'"); + if (!isPositiveInt(interval)) throw new RangeError('interval must be a positive integer'); + if (count !== undefined && !isPositiveInt(count)) throw new RangeError('count must be a positive integer'); + if (count === undefined && until === undefined) throw new RangeError('count or until is required'); + + const [y, m, d] = parseDate(start, 'start'); + if (until !== undefined) parseDate(until, 'until'); + const skipped = new Set(skip); + + const result = []; + for (let n = 0; count === undefined || result.length < count; n++) { + let date; + if (every === 'week') { + date = formatDate(new Date(utc(y, m - 1, d + 7 * interval * n))); + } else { + const total = m - 1 + interval * n; + const year = y + Math.floor(total / 12); + const month = total % 12; + const last = new Date(utc(year, month + 1, 0)).getUTCDate(); + date = formatDate(new Date(utc(year, month, Math.min(d, last)))); + } + if (until !== undefined && date > until) break; + if (!skipped.has(date)) result.push(date); + } + return result; +} + +function isPositiveInt(v) { + return Number.isInteger(v) && v > 0; +} + +// Date.UTC treats years 0-99 as 1900-1999, so set the year explicitly. +function utc(year, month, day) { + const dt = new Date(0); + dt.setUTCFullYear(year, month, day); + return dt.getTime(); +} + +function parseDate(s, name) { + const match = typeof s === 'string' && /^(\d{4})-(\d{2})-(\d{2})$/.exec(s); + if (!match) throw new RangeError(`${name} must be a 'YYYY-MM-DD' date`); + const [y, m, d] = match.slice(1).map(Number); + const dt = new Date(utc(y, m - 1, d)); + if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== m - 1 || dt.getUTCDate() !== d) { + throw new RangeError(`${name} is not a real calendar date`); + } + return [y, m, d]; +} + +function formatDate(dt) { + const p = (n, w) => String(n).padStart(w, '0'); + return `${p(dt.getUTCFullYear(), 4)}-${p(dt.getUTCMonth() + 1, 2)}-${p(dt.getUTCDate(), 2)}`; }