diff --git a/src/schedule.js b/src/schedule.js index 862dc55..fc3f590 100644 --- a/src/schedule.js +++ b/src/schedule.js @@ -23,5 +23,56 @@ * 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, 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 < 1) { + throw new RangeError('interval must be a positive integer'); + } + const hasCount = count !== undefined && count !== null; + const hasUntil = until !== undefined && until !== null; + if (!hasCount && !hasUntil) throw new RangeError('count or until is required'); + if (hasCount && (!Number.isInteger(count) || count < 1)) { + throw new RangeError('count must be a positive integer'); + } + + const [year, month, day] = parseDate(start, 'start'); + if (hasUntil) parseDate(until, 'until'); + const skipped = new Set(skip); + + const result = []; + for (let k = 0; ; k++) { + let date; + if (every === 'week') { + date = formatDate(new Date(Date.UTC(year, month - 1, day + 7 * interval * k))); + } else { + const m = month - 1 + interval * k; + const lastDay = new Date(Date.UTC(year, m + 1, 0)).getUTCDate(); + date = formatDate(new Date(Date.UTC(year, m, Math.min(day, lastDay)))); + } + if (hasUntil && date > until) break; + if (skipped.has(date)) continue; + result.push(date); + if (hasCount && result.length >= count) break; + } + return result; +} + +function parseDate(str, name) { + const match = typeof str === 'string' && /^(\d{4})-(\d{2})-(\d{2})$/.exec(str); + if (!match) throw new RangeError(`${name} must be a date in YYYY-MM-DD form`); + const [y, m, d] = [Number(match[1]), Number(match[2]), Number(match[3])]; + const check = new Date(Date.UTC(y, m - 1, d)); + check.setUTCFullYear(y); // years 0-99 are otherwise mapped to 1900-1999 + if (check.getUTCFullYear() !== y || check.getUTCMonth() !== m - 1 || check.getUTCDate() !== d) { + throw new RangeError(`${name} is not a real calendar date`); + } + return [y, m, d]; +} + +function formatDate(date) { + return date.toISOString().slice(0, 10); }