"use strict"; // Day-number kernel: Howard Hinnant, "chrono-Compatible Low-Level Date Algorithms", // https://howardhinnant.github.io/date_algorithms.html. Used here only to validate calendar // dates (via daysInMonth's leap-year rule); the age itself is computed by calendar-field // subtraction with borrowing, as described in the steps below. No host clock, no Date.now(). function isLeapGregorian(y) { return (y % 4 === 0 && y % 100 !== 0) || (y % 400 === 0); } function daysInMonth(y, m) { const dim = [31, isLeapGregorian(y) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; return dim[m - 1]; } function parseISODate(s, label) { const m = /^(-?\d{4})-(\d{2})-(\d{2})$/.exec(String(s).trim()); if (!m) throw new Error((label || "date") + " must be an ISO 8601 date YYYY-MM-DD, got: " + s); const year = parseInt(m[1], 10), month = parseInt(m[2], 10), day = parseInt(m[3], 10); if (month < 1 || month > 12) throw new Error((label || "date") + " month out of range 1-12"); if (day < 1 || day > daysInMonth(year, month)) throw new Error((label || "date") + " day out of range for that month"); return { year, month, day }; } function daysFromCivil(y, m, d) { y = m <= 2 ? y - 1 : y; const era = Math.floor((y >= 0 ? y : y - 399) / 400); const yoe = y - era * 400; const doy = Math.floor((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5) + d - 1; const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy; return era * 146097 + doe - 719468; } module.exports = { slug: "age-in-years-months-days", title: "Age in years, months, and days", deck_category: "dates", description: "Computes exact calendar age (years, months, days) between a birth date and an as-of date.", inputs: [ { name: "birth_date", type: "string", format: "date", required: true, description: "ISO 8601 birth date, YYYY-MM-DD" }, { name: "as_of_date", type: "string", format: "date", required: true, description: "ISO 8601 date to compute the age as of, must be on or after birth_date" } ], outputs: [ { name: "years", unit: "years", description: "Completed years" }, { name: "months", unit: "months", description: "Completed months beyond the years" }, { name: "days", unit: "days", description: "Remaining days beyond the years and months" } ], steps: [ "Parse birth_date and as_of_date as proleptic Gregorian civil dates; require as_of_date on or after birth_date (checked via day-number, Hinnant days_from_civil, cited above).", "years = as_of.year - birth.year; months = as_of.month - birth.month; days = as_of.day - birth.day.", "If days is negative: subtract 1 from months, and walk back one calendar month at a time from as_of_date's month, adding that month's day count to days, repeating until days is no longer negative (this handles the rare case where the birth day-of-month exceeds the days in the immediately preceding month, e.g. a day-31 birth date compared against an early day in a short month).", "If months is still negative after that, subtract 1 from years and add 12 to months.", "The result is the completed years, months, and days of age as of as_of_date." ], constants: [], sources: [ { kind: "url", value: "https://howardhinnant.github.io/date_algorithms.html", title: "chrono-Compatible Low-Level Date Algorithms", publisher: "Howard Hinnant", retrieved_utc: "2026-09-30T19:41:35Z", note: "day-number kernel used for date validation and ordering" } ], disclaimer: null, questionText(inputs) { return "What is the exact age, in years, months, and days, of someone born on " + inputs.birth_date + " as of " + inputs.as_of_date + "?"; }, run(inputs) { const b = parseISODate(inputs.birth_date, "birth_date"); const a = parseISODate(inputs.as_of_date, "as_of_date"); if (daysFromCivil(a.year, a.month, a.day) < daysFromCivil(b.year, b.month, b.day)) { throw new Error("as_of_date must not be before birth_date"); } let years = a.year - b.year; let months = a.month - b.month; let days = a.day - b.day; let backMonth = a.month, backYear = a.year; while (days < 0) { backMonth -= 1; if (backMonth < 1) { backMonth = 12; backYear -= 1; } days += daysInMonth(backYear, backMonth); months -= 1; } if (months < 0) { years -= 1; months += 12; } const answer = years + " years, " + months + " months, " + days + " days"; return { answer, values: { years, months, days } }; }, examples: [ { name: "worked example", inputs: { birth_date: "1990-06-15", as_of_date: "2026-09-30" }, expected: { answer: "36 years, 3 months, 15 days" }, check: "1990-06-15 to 2026-06-15 is 36 years; 2026-06-15 to 2026-09-15 is 3 months; 2026-09-15 to 2026-09-30 is 15 days" }, { name: "test vector: single-month borrow", inputs: { birth_date: "1995-11-20", as_of_date: "2026-03-05" }, expected: { answer: "30 years, 3 months, 13 days" }, check: "1995-11-20 to 2025-11-20 is 30 years; to 2026-02-20 is 3 more months; Feb 20 to Mar 5, 2026 (28-day Feb) is 13 days" }, { name: "test vector: same-day age is exact years", inputs: { birth_date: "2000-02-29", as_of_date: "2024-02-29" }, expected: { answer: "24 years, 0 months, 0 days" }, check: "2024 is a leap year so Feb 29 exists; exactly 24 full years elapsed with no leftover months or days" } ] };