← 中文资源

简体中文版本

精确计算年龄的年月日

按 UTC 纯日期计算出生日期与参考日期之间的整年、整月与剩余天数,并校验未来日期。

阅读英文原文

功能说明

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;
}