日期推算:EDATE、EOMONTH、DATEDIF、NETWORKDAYS

四个推算函数各解决什么问题

日期推算四函数的分工

知道今天是几号之后,下一步几乎总是要算:三个月后是哪天、这个月最后一天是几号、两个日期之间差了几年几个月、这中间有多少个工作日。这些需求靠 DATE 硬拼也能做,但公式会又长又脆,真正该用的四个函数是 EDATE、EOMONTH、DATEDIF、NETWORKDAYS。

它们的分工非常清晰。EDATE 负责按整月平移,回答若干月之后的同一天是几号;EOMONTH 负责定位月末,回答某个月的最后一天是几号;DATEDIF 负责算间隔,回答两个日期之间差了多少年、多少月、多少天;NETWORKDAYS 负责算工作日,回答一段时间内扣掉周末和法定假日后还剩几天。

这四个函数里,EDATE 和 EOMONTH 在老版本 Excel 里属于分析工具库,需要加载宏才能用,从 Excel 2007 起已经内置,直接使用即可。NETWORKDAYS 同样从 2007 起内置。而 DATEDIF 是个特殊的存在,它从很早的版本就有,却一直是个隐藏函数,在函数列表里找不到它,输入时也不会弹出参数提示,但输入完整之后能正常计算。

函数 语法 返回值 典型用途
EDATE =EDATE(A2,3) 三个月后的同日 合同到期日、保修期
EOMONTH =EOMONTH(A2,0) 当月最后一天 月末结算日、账期
DATEDIF =DATEDIF(A2,B2,"M") 相距整月数 工龄、账龄、年龄
NETWORKDAYS =NETWORKDAYS(A2,B2) 工作日天数 工期、出勤统计
WORKDAY =WORKDAY(A2,10) 十个工作日后的日期 承诺交付日

表格里多列了一个 WORKDAY,它是 NETWORKDAYS 的逆向函数:后者问“这段时间有几个工作日”,前者问“过这么多工作日是几号”。两者通常成对出现,做项目排期时缺一不可。

在动手写公式之前,建议先花一分钟想清楚业务规则到底怎么定。同样是“三个月后”,有的公司是自然月三个月,有的公司是九十天,有的公司还要求落在月末或者遇节假日顺延。规则不同,公式完全不同。把规则用一句话写在表格的备注区,既能提醒自己,也方便日后别人接手时核对,避免因为理解偏差导致整批数据的到期日都算错。

使用这一组函数有个共同前提:参与计算的必须是真正的日期,不能是文本。如果 A2 是文本日期,EDATE 会返回 #VALUE!。遇到这种情况,先用上一章讲过的分列或者 DATEVALUE 转成真日期,再套推算函数。转换的具体手法参见 日期基础:TODAY、NOW、DATE 和年月日提取。

EDATE 与 EOMONTH:按月平移与月末定位

EDATE 的语法是 =EDATE(开始日期, 月数),返回开始日期之前或之后指定月数的同一天。月数可以是正数、负数或零,正数往后推,负数往前推,零返回原日期。

算一年后的同日:

=EDATE(A2,12)

算三个月前的同日:

=EDATE(A2,-3)

EDATE 最贴心的设计是自动处理月末。如果开始日期是某月的最后一天,结果也会落在目标月的最后一天:=EDATE("2023-1-31",1) 得到 2023-2-28,而不是顺延到三月。反过来,如果开始日期是一月三十日,加一个月得到二月二十八日,再回到一月三十日则不会发生,因为平移是单向的。

这个特性让它非常适合算合同到期日。一份合同签三年,签字日是 2023-2-28,到期日就是 =EDATE(A2,36),结果是 2026-2-28,完全符合业务预期,不需要人工去核对每年的天数。

EOMONTH 的语法是 =EOMONTH(开始日期, 月数),返回开始日期所在月(再偏移指定月数)的最后一天。第二个参数写零表示当月月末,写负一表示上个月月末,写一表示下个月月末。

当月最后一天:

=EOMONTH(A2,0)

上个月最后一天:

=EOMONTH(A2,-1)

EOMONTH 最常见的用途有三个。一是算当月一共有几天,配合 DAY 即可:=DAY(EOMONTH(A2,0)),一月返回三十一,二月返回二十八或二十九,闰年自动处理,比自己写判断闰年的公式靠谱得多。

二是算月末结算日。很多公司的账期规则是“次月月末付款”,写法就是 =EOMONTH(开票日期,1)。如果规则是“开票后六十天,遇到月末顺延到当月最后一天”,则写成 =EOMONTH(开票日期+60,0)。

三是做按月的区间统计。求某个月的第一天和最后一天,可以分别写 =EOMONTH(A2,-1)+1 和 =EOMONTH(A2,0),把这两个值作为统计区间的上下界,配合条件求和就能得到整月数据。多条件统计的写法在 SUMIFS 多条件统计:日期区间加多字段 里有详细展开。

EOMONTH 还能轻松定位季度末和半年末。某个日期所在季度的最后一天:

=EOMONTH(A2,MOD(3-MONTH(A2),3))

原理是先算出当前月距离本季度末还差几个月,再交给 EOMONTH 平移。比如四月距离六月末差两个月,公式算出的偏移量就是二,结果正好是六月三十日。半年末的写法同理,把三换成六即可:=EOMONTH(A2,MOD(6-MONTH(A2),6))。这两个公式在按季度、按半年度出报表时非常省事,不用每次手工翻日历。

还有一个实用技巧:判断某年是否为闰年,可以写 =IF(DAY(EOMONTH(DATE(A2,2,1),0))=29,"闰年","平年"),其中 A2 是年份。原理是算出二月最后一天是不是二十九号,逻辑直白,也不需要记那些复杂的闰年规则。

DATEDIF:被藏起来的间隔计算神器

DATEDIF三参数怎么写

DATEDIF 的语法是 =DATEDIF(开始日期, 结束日期, 单位代码),返回两个日期之间的间隔。它的强大之处在于可以按不同单位返回,而且能返回“扣除整年后剩下的月数”这种复合结果。

六个单位代码的含义如下:

单位代码 含义 示例结果
"Y" 相隔的完整年数 =DATEDIF(A2,B2,"Y")
"M" 相隔的完整月数 =DATEDIF(A2,B2,"M")
"D" 相隔的总天数 =DATEDIF(A2,B2,"D")
"YM" 扣除整年后剩余的月数 =DATEDIF(A2,B2,"YM")
"YD" 扣除整年后剩余的天数 =DATEDIF(A2,B2,"YD")
"MD" 扣除整年月后剩余的天数 =DATEDIF(A2,B2,"MD")

后三个代码是 DATEDIF 独有的能力,其他函数做不到。利用它们可以拼出“三年零五个月”这样的表达:

=DATEDIF(A2,B2,"Y")&"年"&DATEDIF(A2,B2,"YM")&"个月"

算周岁年龄是最经典的用法,把结束日期换成 TODAY() 即可:

=DATEDIF(A2,TODAY(),"Y")

算工龄并精确到月:

=DATEDIF(入职日期,TODAY(),"Y")&"年"&DATEDIF(入职日期,TODAY(),"YM")&"个月"

DATEDIF 有三个必须记住的坑。第一,开始日期必须小于等于结束日期,否则返回 #NUM!。如果数据的先后不确定,用 IF 兜一下:=IF(A2>B2,DATEDIF(B2,A2,"D"),DATEDIF(A2,B2,"D"))。

第二,单位代码必须加英文双引号,写 Y 不加引号会被当成名称而返回 #NAME?。这是新手最高频的报错原因。

第三,"MD" 这个代码在少数情况下会返回零甚至负数,属于该函数历史上遗留的计算缺陷。如果你要的就是剩余天数,更稳妥的做法是用 =B2-EDATE(A2,DATEDIF(A2,B2,"M")),先算出整月数对应的日期,再用结束日期减它,结果稳定且可验证。

NETWORKDAYS 与 WORKDAY:按工作日推算

NETWORKDAYS 的语法是 =NETWORKDAYS(开始日期, 结束日期, [节假日]),返回两个日期之间(含首尾两天)的工作日天数,默认周六周日为休息日。

不加节假日参数:

=NETWORKDAYS(A2,B2)

加上节假日参数:

=NETWORKDAYS(A2,B2,$F$2:$F$20)

第三个参数是一个节假日日期区域,建议单独在工作表的某个角落列出来,比如放在 F 列,并且用绝对引用 $F$2:$F$20 锁定,这样公式向下填充时区域不会偏移。节假日表每年更新一次即可,比把日期硬编码在公式里好维护得多。

NETWORKDAYS.INTL 是它的增强版,多了一个周末参数,可以自定义哪几天休息。周末参数用一串七位的零一字符串表示,从周一开始,0 表示上班,1 表示休息:

=NETWORKDAYS.INTL(A2,B2,"0000011",$F$2:$F$20)

上面的 "0000011" 表示周一到周五上班、周六周日休息,效果等同于普通的 NETWORKDAYS。如果公司只有周日单休,就写 "0000001";如果是轮休周三周四,就写 "0011000"。这个灵活度让它能适配各种排班制度。

WORKDAY 是反向计算,语法是 =WORKDAY(开始日期, 工作日天数, [节假日]),返回从开始日期起经过指定工作日之后的日期。它同样有增强版 WORKDAY.INTL。

算十个工作日之后的交付日:

=WORKDAY(A2,10,$F$2:$F$20)

算项目结束日,已知需要二十五个工作日:

=WORKDAY(开始日期,25,节假日区域)

这两个函数经常配合使用:先用 NETWORKDAYS 算出实际用了几个工作日,再用 WORKDAY 反推应该什么时候结束,两者对比就能看出是否延期。判断是否延期可以写成 =IF(实际完成日>WORKDAY(开始日,计划工作日,节假日),"延期","准时")。

需要提醒的是,NETWORKDAYS 和 WORKDAY 只认你给出的节假日清单,不会自动包含法定节假日。中国的春节、国庆每年日期不同,需要人工维护到节假日区域里,一般把未来两三年的假期一次性列齐即可。

算一整月的工作日天数也是高频需求,把月初月末和 NETWORKDAYS 拼起来即可:

=NETWORKDAYS(EOMONTH(A2,-1)+1,EOMONTH(A2,0),法定假日)

其中 EOMONTH(A2,-1)+1 是当月第一天,EOMONTH(A2,0) 是当月最后一天。有了应出勤天数,再拿实际出勤天数相除就得到了出勤率,考勤月报的核心指标就是这么算出来的。

如果你想算某段时间内每个工作日的日期清单(比如生成排班表的表头),可以用 WORKDAY 配合行号:=WORKDAY(起始日-1,ROW(A1)),往下填充就能依次得到第一个、第二个、第 n 个工作日。减一是因为 WORKDAY 从起始日的次日开始数。

真实场景与兼容提示

账龄分档

应收账款表里,A 列是开票日期,需要按账龄分成三十天内、三十一到六十天、六十一到九十天、九十天以上四档。先算天数差,再分档:

=DATEDIF(A2,TODAY(),"D")

分档公式:

=IF(B2<=30,"30天内",IF(B2<=60,"31-60天",IF(B2<=90,"61-90天","90天以上")))

这里用了一层套一层的写法,判断顺序必须从最小的区间开始,否则会被上一个条件截住。分档之后配合条件求和就能得到各档位的金额合计。

合同到期提醒与自动续期

合同表里,A 列是签订日期,B 列是期限月数。到期日:

=EDATE(A2,B2)

是否即将到期:

=IF(C2-TODAY()<=30,"即将到期","正常")

自动续期一年:

=IF(C2<TODAY(),EDATE(C2,12),C2)

意思是已经过期的自动往后延十二个月,未过期的保持原到期日。这类自动续期公式在管理大量合同时非常省事,但要注意保留原始列,别让公式把历史信息覆盖掉。

员工年假天数计算

假设公司规定:满一年不满五年给五天,满五年不满十年给十天,满十年及以上给十五天。先用 DATEDIF 算出整年工龄,再分档:

=DATEDIF(入职日期,TODAY(),"Y")

然后分档:

=IF(B2<1,0,IF(B2<5,5,IF(B2<10,10,15)))

这个公式的判断顺序必须从低到高,因为 IF 一旦命中就不再往下走。如果先写大于等于十,那所有工龄都会被截在最低档。写好之后建议拿几个边界值验一遍:正好满一年、正好满五年、正好满十年的员工,看给的档位是否符合制度。

如果公司还规定入职当年按月折算,可以在第一档里再用 DATEDIF 取剩余月数:=IF(B2<1,ROUNDDOWN(DATEDIF(入职日期,TODAY(),"M")/12*5,0),...),按整月折算后向下取整。这类规则因公司而异,写公式前务必先把制度条款逐条列清楚,再翻译成判断条件。

版本兼容性

EDATE、EOMONTH、NETWORKDAYS、WORKDAY 从 Excel 2007 起内置,WPS 表格同样支持,可以放心使用。如果你的文件需要在 Excel 2003 里打开,需要启用分析工具库,否则会返回 #NAME?,这种情况建议直接改用 DATE 组合写法。

NETWORKDAYS.INTL 和 WORKDAY.INTL 是 Excel 2010 才加入的,WPS 较新版本支持,老版本不支持。如果打开文件后出现 #NAME?,退回不带 INTL 的普通版本即可,代价是只能用周六周日双休的默认规则。

DATEDIF 在所有版本里都能用,包括 WPS,但它没有参数提示,输入时要格外小心参数顺序:先开始日期,再结束日期,最后单位代码。顺序写反会直接报 #NUM!。另外要记住结束日期如果是 TODAY(),结果会每天自动变化,做历史快照时记得转成数值固定下来。

常见问题

DATEDIF 为什么在函数列表里找不到

它确实不在函数列表里,也不会弹出参数提示,这是微软历史遗留的处理方式:这个函数最早是为了兼容 Lotus 而保留的,后来一直没有被正式纳入函数向导。但它能正常计算,只要完整输入 =DATEDIF(A2,B2,"Y") 就能得到结果。

因为没有提示,输入时全靠记忆,最容易错的地方是单位代码忘了加英文双引号,以及开始日期和结束日期写反。写反的表现是返回 #NUM!,把两个参数调换过来即可。另外该函数在 WPS 里同样可用,行为与 Excel 完全一致。

怎么算两个日期之间相差几年几个月零几天

分三次调用 DATEDIF,分别取整年、扣除整年后的剩余月数、扣除整年月后的剩余天数,再用连接符拼成一句话:

=DATEDIF(A2,B2,"Y")&"年"&DATEDIF(A2,B2,"YM")&"个月"&DATEDIF(A2,B2,"MD")&"天"

如果觉得三个 DATEDIF 太长,可以在辅助列里分别算出这三个值,最后一列拼装。辅助列的写法虽然多占三列,但每一列都能单独检查,出问题时一眼就能看出是哪一段算错了,比一长串嵌套公式好维护。

EDATE 加一个月,为什么一月三十一号变成了二月二十八号

这是 EDATE 的正常行为,也是它的设计优点。当目标月份没有对应的日期时,函数会返回该月的最后一天,而不是顺延到下个月。所以一月三十一号加一个月得到二月二十八号(闰年二十九号),三月三十一号减一个月得到二月二十八号。

如果你的业务规则恰好相反,要求顺延而不是回退,那就不要用 EDATE,改用 =DATE(YEAR(A2),MONTH(A2)+1,DAY(A2)),这个写法会因为自动进位而顺延到三月三日。两种规则没有对错,关键在于和业务方确认清楚。

节假日表要怎么维护才不容易出错

三条建议。第一,单独放在一张工作表里,或者至少放在当前表的固定列,比如 F 列从第二行开始,写清年份和放假日期,方便每年更新。第二,公式里一律用绝对引用锁定这个区域,写成 $F$2:$F$50,并且多留一些空行给未来新增,这样向下填充公式时区域不会跑偏。

第三,把节假日区域定义为名称,比如取名叫“法定假日”,之后公式就可以写成 =NETWORKDAYS(A2,B2,法定假日),可读性好得多,区域调整时也只需要改一次定义。定义名称的方法是在公式选项卡里点定义名称,或者在名称框里直接输入。

为什么 NETWORKDAYS 算出来的天数比日历天数还多

这种情况几乎只可能是日期顺序写反了。当开始日期晚于结束日期时,NETWORKDAYS 会返回一个负数,而不是报错。如果你在别处取了绝对值或者做了其他处理,就容易看到不合理的数字。

排查办法是加一列 =B2-A2 看日历天数差,正常情况下工作日数应该小于等于日历天数。如果工作日数比它还大,就说明数据顺序或者区域引用出了问题。另外要注意,NETWORKDAYS 的计数包含首尾两天,所以同一天开始同一天结束会返回一,而不是零。