功能说明
calculateAge 返回从出生日期到参考日期的完整年龄,包括整年、剩余月数、剩余天数以及总月数和总天数。输入支持 Date 对象或 YYYY-MM-DD 字符串,内部统一转换为 UTC 零点,避免本地时区与夏令时导致的偏移。适合注册表单的年龄校验、保险与医疗业务计算、会员周年判断等需要精确年月日的场景。
关键代码
先按年月差算出总月数,如果参考日的日号小于出生日的日号,说明最后一个月尚未满,总月数减一。随后把出生日期加上总月数并做月末夹取,例如 1 月 31 日加一个月得到 2 月 28 日或 29 日,再用毫秒差除以一天得到剩余天数。所有中间日期都使用 UTC 构造器,日期字符串还会回读校验,拒绝 2 月 30 日这类被自动进位的非法日期。
使用注意
年龄按完整月份累计,2 月 29 日出生的人在非闰年会在 3 月 1 日才增加一岁,这是夹取策略的结果,业务若采用其他规则需自行调整。输入 Date 时只取 UTC 年月日,若调用方传入本地时间可能在东八区出现一天偏差,建议表单直接传日期字符串。函数不处理时区转换,也不适合计算精确到小时的时间差。
验证方式
覆盖 birthday 等于参考日、生日当天、生日前一天以及闰日出生等边界。测试 1 月 31 日到 2 月 28 日、3 月 1 日的月份夹取结果。用 2000-02-29 与 2023-02-28 验证整年计算,再测试未来日期、非法字符串、2 月 30 日、空输入与 Date 对象路径,确保抛出对应错误且错误类型明确。
代码示例
export function calculateAge(birthDate, asOf = new Date()) {
const birth = toDateOnly(birthDate);
const reference = toDateOnly(asOf);
if (birth > reference) throw new RangeError("birthDate must not be in the future");
let totalMonths = (reference.getUTCFullYear() - birth.getUTCFullYear()) * 12
+ (reference.getUTCMonth() - birth.getUTCMonth());
if (reference.getUTCDate() < birth.getUTCDate()) totalMonths -= 1;
const anchor = addMonthsClamped(birth, totalMonths);
const days = Math.floor((reference.getTime() - anchor.getTime()) / 86400000);
return {
years: Math.floor(totalMonths / 12),
months: totalMonths % 12,
days,
totalMonths,
totalDays: Math.floor((reference.getTime() - birth.getTime()) / 86400000),
};
}
function addMonthsClamped(date, months) {
const target = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1));
const lastDay = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
target.setUTCDate(Math.min(date.getUTCDate(), lastDay));
return target;
}
function toDateOnly(value) {
if (value instanceof Date) {
if (Number.isNaN(value.getTime())) throw new RangeError("invalid date");
return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()));
}
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value));
if (!match) throw new TypeError("expected YYYY-MM-DD or Date");
const year = Number(match[1]);
const month = Number(match[2]) - 1;
const day = Number(match[3]);
const date = new Date(Date.UTC(year, month, day));
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day) {
throw new RangeError("invalid calendar date");
}
return date;
}