ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

日期处理避坑指南:从闰年判断到时区夏令时

日期处理避坑指南:从闰年判断到时区夏令时 日期大概是每个写代码的人都绕不开、却很少愿意主动深挖的话题。我这些年做网站、写接口、处理Excel报表几乎每个项目都会撞上日期问题明明就是“今天是几号、转成字符串、再解析回来”但真到了线上闰年、时区、夏令时、区域设置、空值、微秒精度……每一个都能让人排查半天。网上搜出来的答案又常常只针对某个具体报错换个环境就不灵了。所以我把这些散落的“日期题”收集起来整理成一套题目集。这里面有最经典的算法题“输入年、月、日判断是这一年的第几天”也有大量真实项目里踩过的坑比如Java生成英文日期格式、JS时间戳转日期、Excel日期读出来是空白、Hibernate夏令时报错、GIS字段怎么赋值yyyymmdd、小程序日期选择组件怎么选等等。每一题我都会给出可复现的实现思路、参考代码和需要注意的边界条件。不管你是准备笔试的学生还是正在被线上bug折磨的工程师都可以把这篇当作一本随手查的日期问题笔记。1. 日期题目的核心本质从“第几天”开始1.1 经典题目拆解闰年判断与天数累加先看最经典的那道C语言题输入一个日期的年、月、日计算并输出这天是该年的第几天。题目本身不复杂核心就两件事第一把当月之前所有月份的天数累加起来第二加上当月已经过去的日号。比如2024年3月1日1月有31天2月有29天因为2024年是闰年累加结果是60再加1号得到61所以3月1日是2024年的第61天。真正需要小心的只有闰年判断。闰年的规则是“四年一闰百年不闰四百年再闰”翻译成代码就是能被400整除一定是闰年能被4整除但不能被100整除是闰年其他情况都不是闰年。这个规则看着简单但每年都能在笔试里看到有人写错。有些人只写了y % 4 0直接把1900年这种整百年份也算成闰年一算2月天数就错了。所以建议把闰年判断单独抽成一个函数后面所有日期计算都能复用。1.2 C语言实现数组查表法与循环累加法的对比第一种写法是数组查表法把每个月天数放到数组里平年二月是28天闰年时临时改成29天然后循环累加。#include stdio.h int isLeap(int y) { return (y % 4 0 y % 100 ! 0) || (y % 400 0); } int dayOfYear1(int y, int m, int d) { int days[12] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (isLeap(y)) { days[1] 29; } int sum 0; for (int i 0; i m - 1; i) { sum days[i]; } return sum d; } int main() { int y, m, d; scanf(%d %d %d, y, m, d); printf(%d\n, dayOfYear1(y, m, d)); return 0; }第二种写法是前缀和数组本质上是一种空间换时间的优化。因为每个月之前的天数是固定的可以提前算好int dayOfYear2(int y, int m, int d) { const int prefix[12] {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; int sum prefix[m - 1] d; if (m 2 isLeap(y)) { sum; } return sum; }这里为什么要判断m 2因为只有2月之后的天数才会受闰年影响1月份不用加。很多人第一次写前缀和版本时容易忘了这个条件结果输入1月也会多算一天。两种方法都有代表性。数组查表法适合讲解循环和数组逻辑更直观前缀和法省去循环在频繁调用时有性能优势。我个人的建议是笔试时写第一种因为可读性最好工程上如果做批量日期计算可以用第二种。1.3 从这个题延伸出的通用日期思维别看这道题简单它把日期处理里最重要的几个点全覆盖了边界条件、非法输入、闰年规则。实际开发里遇到的日期bug十有八九也出在这三件事上。先说边界。1月、12月、2月29日、12月31日这些都是必须单独测试的输入。很多人写日期函数只测了中间几个普通日期上生产后一遇到跨年数据就崩。再说非法输入。如果用户输入2月30日或者13月程序应该怎么处理我在教学和面试中见过太多次“函数默认输入合法结果线上被脏数据打穿”的情况。所以至少应该加一个基本校验月份在1到12之间日期在1到当月最大天数之间否则返回错误。int maxDay(int y, int m) { int days[12] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (m 2 isLeap(y)) return 29; return days[m - 1]; }有了这个函数判断某天是否是有效日期就很简单d 1 d maxDay(y, m)。2. 日期显示与格式化字符串才是主战场2.1 Java生成08 August, 2025 10:03:39这种英文格式有人问Java怎么生成“08 August, 2025 10:03:39”这样的日期格式。我直接给出Java 8以后的最佳实践用DateTimeFormatter千万别再用SimpleDateFormat。SimpleDateFormat不是线程安全的在多线程环境里共享同一个实例会出莫名其妙的问题我已经不止一次看到有人在这里翻车。import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Locale; public class DateFormatDemo { public static void main(String[] args) { LocalDateTime now LocalDateTime.of(2025, 8, 8, 10, 3, 39); DateTimeFormatter formatter DateTimeFormatter.ofPattern(dd MMMM, yyyy HH:mm:ss, Locale.ENGLISH); System.out.println(now.format(formatter)); // 输出08 August, 2025 10:03:39 } }关键点有两个。第一MM表示数字月份MMMM表示完整月份名比如Augustdd表示两位日期HH表示24小时制hh是12小时制。第二Locale一定要指定为Locale.ENGLISH。如果不指定程序会读系统的默认语言中文环境跑出来就是“08 八月, 2025 10:03:39”看起来也别扭而且不同机器结果还不一样这种环境相关的输出在测试里最容易埋雷。2.2 JS时间戳转日期封装一个不踩时区坑的函数前端拿到后端接口返回的时间戳第一反应是new Date(timestamp)然后toISOString()但这里有个经典坑toISOString()返回的是UTC时间不是本地时间。比如你在东八区北京时间2025年8月8日早上8点转出来会变成“2025-08-08T00:00:00.000Z”看着好像是当天凌晨容易误会。如果只是想展示给用户正确做法是用getFullYear、getMonth这一组本地时间方法自己拼接。function formatTimestamp(ts) { const d new Date(ts); const pad n String(n).padStart(2, 0); return ${d.getFullYear()}-${pad(d.getMonth() 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}; }这里最大的坑是getMonth()返回值从0开始0代表1月所以一定要加1。我见过不少新手直接写getMonth()结果每次显示出来的月份都比实际小1。另外注意时间戳单位13位是毫秒10位是秒。如果后端返回的秒级时间戳要先乘1000再传给Date不然日期会直接跑偏几十年。2.3 英文日期后缀1st、2nd、3rd、th到底怎么算“日期th nd”这个问题看着不起眼但真要做英文站、做邮件模板、做报关单之类的功能时就会遇到。规则其实不复杂绝大多数日子后面加th只有尾数是1、2、3的日期加st、nd、rd但11、12、13这三个数字是例外依然用th。function daySuffix(day) { if (day 11 day 13) return th; const last day % 10; if (last 1) return st; if (last 2) return nd; if (last 3) return rd; return th; }比如1号是1st2号是2nd3号是3rd21号是21st但11号是11th12号是12th13号是13th。这个逻辑在Java、Python里也都一样只是语法不同。写的时候一定要把11到13的判断放在前面先拦截例外再判断尾数不然21号也会被误判成th。2.4 数据库里的日期转字符串SQL Server和Oracle各写各的数据库日期转字符串也是高频需求但不同数据库语法差异非常大我经常看到有人把Oracle的TO_CHAR拿到SQL Server里跑结果直接报错。这里把最常见的两种写法列出来。SQL Server里最传统的方式是CONVERT它支持固定样式码。120正好对应“yyyy-MM-dd HH:mm:ss”这种格式SELECT CONVERT(varchar(10), GETDATE(), 120) AS yyyyMMdd; SELECT CONVERT(varchar(23), GETDATE(), 120) AS yyyyMMddHHmmss;SQL Server 2012以后还多了个FORMAT函数可以直接写格式串用起来很方便但性能开销大如果是在几十万行的大表里做转换别用它老老实实用CONVERT。Oracle则要用TO_CHARSELECT TO_CHAR(SYSDATE, YYYY-MM-DD HH24:MI:SS) FROM dual;注意Oracle的24小时制是用HH24表示的如果只写HH默认是12小时制下午两点会显示成“02”而不是“14”。这个细节踩过的人不少特别是从其他数据库转过来的人。3. 日期解析与比较隐秘的坑3.1 该字符串未被识别为有效的日期的真相“该字符串未被识别为有效的日期”是SQL Server里很常见的一个报错本质不是字符串本身有问题而是SQL Server按照它认为的区域设置去解析结果没对上。比如你写CAST(08/08/2025 AS datetime)在某个区域设置下会被当成MM/dd/yyyy在另一个区域设置下会被当成dd/MM/yyyy一个能过一个就报错。最稳妥的办法是使用CONVERT并指定明确的样式码让解析规则和区域设置彻底脱钩SELECT CONVERT(datetime, 2025-08-08 10:03:39, 120);120这类样式码代表固定格式输入字符串必须严格匹配只要匹配就能稳定解析。实际上不只是SQL Server几乎所有语言的日期解析函数都会有类似问题。Java的LocalDate.parse默认只认ISO格式yyyy-MM-dd如果你传入08/08/2025也必须显式指定DateTimeFormatter.ofPattern(dd/MM/yyyy)。我的建议很简单系统内部传输、存储、接口交互的日期字符串一律用ISO 8601格式也就是yyyy-MM-ddTHH:mm:ss或yyyy-MM-dd HH:mm:ss这是最能减少歧义的做法。3.2 VBA日期比较大小区域设置是重灾区Excel VBA里比较日期很多人直接拿字符串比比如2025/08/08 2025/08/09这种写法碰巧能用但格式一换就废。更可靠的做法是先用CDate转换成日期类型再比较Dim d1 As Date, d2 As Date d1 CDate(Range(A1).Value) d2 CDate(Range(A2).Value) If d1 d2 Then MsgBox A1早于A2 ElseIf d1 d2 Then MsgBox A1晚于A2 Else MsgBox 两个日期相同 End If但CDate同样受Excel的系统区域设置影响。同一个08/08/2025在美国会解析成8月8日在英国可能就报错或者解析成8月8日中文系统则可能解析为8月8日容易出问题。如果不想依赖区域设置直接使用DateSerial构造日期对象d1 DateSerial(2025, 8, 8) 2025年8月8日3.3 PHPExcel读日期单元格返回空白或数字用PHPExcel现在是PhpSpreadsheet读Excel日期单元格经常会发现getValue()拿到的不是日期而是一个数字甚至日期显示为空。原因是Excel内部把日期存储成序列号单元格格式才决定它显示成“2025/08/08”还是“08/08/2025”。最简单的解法是直接用getFormattedValue()它会把单元格格式应用上返回用户看到的字符串$sheet $objPHPExcel-getActiveSheet(); $value $sheet-getCell(A1)-getFormattedValue();如果拿到的还是空字符串就先手动判断一下单元格格式类型再用toFormattedString把序列号转成日期$code $sheet-getCell(A1)-getStyle()-getNumberFormat()-getFormatCode(); if (preg_match(/[yYmMdD]/, $code)) { $value \PHPExcel_Style_NumberFormat::toFormattedString( $sheet-getCell(A1)-getValue(), YYYY-MM-DD ); }这里要理解一个底层逻辑Excel日期单元格的getValue()返回的是类似45877.5这样的序列号代表从1900年1月1日起的天数。如果你要批量导入日期列建议在读取前先确认这一列是不是日期格式再选择对应的读取方式。3.4 Oracle取每组日期最大的记录“Oracle查询时查询日期最大的数据”也是一个高频问题。典型需求是每个用户有多条记录我要取每个用户最新的一条。最通用的写法是用窗口函数ROW_NUMBER()SELECT * FROM ( SELECT t.*, ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY business_date DESC ) AS rn FROM your_table t ) tmp WHERE rn 1;PARTITION BY负责分组ORDER BY business_date DESC负责在组内按日期从大到小排序最后取rn 1就是每个组里日期最大的那条记录。如果只是要聚合出某个分组里最大的日期值也可以用MAX(business_date)但一旦需要“日期最大的整条记录”窗口函数是最清晰的选择。Oracle还支持KEEP (DENSE_RANK FIRST)这种写法但可读性稍差我一般只在特殊场景才用。3.5 Hibernate夏令时报错别忽略JDBC时区Hibernate应用在夏令时切换时会报奇奇怪怪的日期转换错误常见原因是Java进程时区和数据库时区不一致或者JDBC连接串没有指定正确的serverTimezone。夏令时切换时本地时间会往前走或往后拨一小时如果两边时区设置不一致java.time.LocalDateTime和数据库的时间类型之间转换就会错位甚至直接抛异常。经验做法是在JDBC连接串上显式指定时区jdbc:mysql://localhost:3306/db?serverTimezoneAsia/ShanghaiuseLegacyDatetimeCodefalse同时尽量让Java应用、数据库、操作系统三层使用同一个时区。如果业务涉及海外建议存储时统一用UTC展示时再转本地时区这样能最大限度降低夏令时对核心逻辑的冲击。另外业务代码里尽量避免在夏令时切换的凌晨时间段做跨时区日期运算这类问题最难复现一旦出现通常只能靠日志慢慢排查。4. 系统与应用里的日期配置4.1 Windows自动同步时间怎么关安装日期能不能改“电脑同步时间日期怎么关掉”这个问题多半出现在用户不想让系统自动校准时间的场景里比如某个软件明确要求固定日期或者局域网环境需要统一时间。最简单的操作是进入“设置 - 时间和语言 - 日期和时间”把“自动设置时间”关掉。如果想彻底一点可以禁用Windows Time服务net stop w32time sc config w32time start disabled但我要提醒一句关闭时间同步是双刃剑系统时间一旦漂移登录凭据、HTTPS证书、文件时间戳都可能出问题所以不建议长期禁用。至于“win 22h2 专业版本更改安装日期修改命令”这个需求通常是想改系统安装日期。查看安装日期很简单wmic os get installdate出来的是一串16位数字格式是yyyyMMddHHmmss比如20250808100339代表2025年8月8日10:03:39。但注意改安装日期需要动注册表相关键值而且可能影响系统激活、更新判断等底层逻辑风险很高。我不建议也不支持通过修改安装日期去绕过软件试用期或者骗过某个校验逻辑这种事一旦被查出来轻则功能异常重则涉及违规。4.2 Linux服务器用date -s改时间改完就完了Linux下临时修改系统时间确实很简单date -s 2025-08-08 10:03:39但这条命令只改当前系统时钟不会同步到硬件时钟重启后大概率会变回去。如果你只是想临时测试可以这样用但如果要永久生效需要配合hwclock写回硬件hwclock --systohc生产环境里我不建议任何人随意执行date -s因为服务器时间一改变影响面可能是连锁的数据库事务时间、应用日志排序、消息队列延迟、JWT过期校验、HTTPS证书有效性判断全部可能出问题。我见过因为测试环境改了时间没改回来结果线上任务调度全部错乱的案例。真正需要校准时间时正确的姿势是使用NTP时间同步比如timedatectl set-ntp yesLinux发行版大多预装了chrony或systemd-timesyncd让系统自动从时间服务器拉取时间比手工改可靠得多。4.3 SolidWorks安装提示无法决定服务失效日期怎么排查“SolidWorks 2017安装过程中提示无法决定服务失效日期”这类报错本质是安装程序在计算授权服务有效期时拿不到一个可信的系统时间或服务状态。常见诱因有三个系统时间异常、之前安装残留的授权服务未清理干净、软件授权文件与当前系统时间不匹配。排查思路是先确认系统日期时间是否准确把时间校准后再重新安装如果还有问题把旧版本卸载干净尤其是授权服务和注册表残留然后重新启动用管理员权限重新运行安装程序。如果使用的是非官方渠道的授权文件这类问题就会特别多而且存在法律风险我的建议始终是走官方渠道获取正版授权不要在这种问题上浪费太多时间。4.4 SAP后台作业自动更新日期SAP里要做一个每天自动更新日期的后台作业通常是在事务码SM36里配置周期性作业。作业运行时会读取系统日期写入到对应的表或变式参数里。如果需求是“作业运行时自动把日期设为当天”可以在ABAP程序里直接取系统字段DATA: lv_date TYPE d. lv_date sy-datum.然后把lv_date写到目标表即可。这里的关键点在于SAP作业运行日期和系统日期、业务日期可能不是同一个概念。如果存在跨时区业务最好约定清楚到底用哪个日期运行当天、上一个工作日、还是业务单据日期。这个约定不清晰作业跑得再勤也白搭。4.5 修改Word最后一次保存日期文件时间戳的操作边界“修改word最后一次保存的日期”也经常被问到。其实Word显示的“最后保存日期”读的是文件系统的“修改时间”并不是文档内部记录的某个字段。所以最简单的修改方式就是直接改文件时间戳。Windows下PowerShell可以这样操作$file Get-Item C:\path\demo.docx $file.LastWriteTime 2025-08-08 10:03:39这个操作只影响文件属性不改变文档内容。我提醒一句改文件时间戳可以做测试、可以做批量文件整理但不要拿去篡改合同、报告、作品集之类的正式文档这种行为有合规风险。而且很多文件在云盘、办公系统里有独立的时间记录改了本地文件属性也会被后台日志还原。5. 业务场景中的日期处理案例5.1 GIS日期字段怎么统一赋成yyyymmddGIS属性表里的日期字段经常乱得让人头疼有人存“2025/08/08”有人存“2025-8-8”还有人直接存字符串。如果想让日期字段统一变成yyyymmdd可以在ArcGIS或其他GIS软件的字段计算器里写Python表达式。假如字段本身是日期类型可以直接用strftime格式化def format_date(value): return value.strftime(%Y%m%d)然后在字段计算器里调用format_date(!date_field!)如果字段是字符串类型就要先解析再格式化同时注意补零。比如输入可能是2025/8/8直接转不好处理得先拆出来再拼def format_str_date(value): parts value.split(/) return {}{:02d}{:02d}.format(parts[0], int(parts[1]), int(parts[2]))这里尤其要注意GIS的字段计算器针对不同数据源File Geodatabase、Shapefile、PostGIS支持程度不同Shp文件的日期字段本质还是DBF限制建议先把字段类型确认清楚再动手不然计算器容易报类型错误。5.2 微信小程序日期选择器和Flet的Material风格DatePicker微信小程序里做“选择日期”最原生的是picker组件的modedate写法很简单picker modedate value{{date}} bindchangeonDateChange view{{date || 请选择日期}}/view /picker在对应的js里处理Page({ data: { date: }, onDateChange(e) { this.setData({ date: e.detail.value }); } });如果需要更丰富的日期选择能力比如日期区间、禁用某些日期可以引入第三方npm组件比如vant-weapp的datetime-picker需要在微信小程序开发者工具里先执行npm构建然后按组件文档引入。Python的Flet框架里也提供了Material风格的DatePicker组件使用起来类似这样import flet as ft import datetime def main(page: ft.Page): def pick_date(e): page.open( ft.DatePicker( first_datedatetime.datetime(2020, 1, 1), last_datedatetime.datetime(2030, 12, 31), ) ) page.add(ft.ElevatedButton(选择日期, on_clickpick_date)) ft.app(main)page.open()是Flet里打开弹窗组件的方式。DatePicker会以Material风格弹出一个日历面板用户选择后通过回调获取日期值。要注意first_date和last_date限制可选范围否则用户可能选到明显不合理的日期。5.3 小米迁移数据后图库拍摄日期不对这个不算编程问题但很典型。手机迁移数据后图库里的拍摄日期变了通常有两个原因一是迁移工具把文件复制过去时批量修改了文件时间二是图片的EXIF信息没被正确保留或读取。照片真正的拍摄时间一般记录在EXIF里的“DateTimeOriginal”字段。如果文件时间被改了但EXIF还在可以用电脑上的图片工具批量读取EXIF时间再把文件时间改回拍摄时间。比如用ExifTool这类工具一句命令就能处理exiftool -r -fileOrder DateTimeOriginal -FileModifyDateDateTimeOriginal .如果EXIF已经丢了那基本救不回来只能靠相册里其他线索手动调整。另外提醒一点如果照片是通过微信这类聊天软件发过来的很多平台会压缩图片并抹掉EXIF这就是很多人觉得“原图”也没了拍摄时间的原因。5.4 日志里的日期schannel 36887事件怎么看有时候看Windows事件日志会看到类似“日志名称: System来源: Schannel日期: 2026/8/26 16:42:17事件ID: 36887”这样的记录。很多人一看到日期就以为是时间不对其实这个事件本身是TLS/SSL通信时出现的告警通常意味着某个客户端或服务端在HTTPS握手阶段出现了连接错误。排查时日期字段的作用是帮我们定位具体发生时间。先确认系统时间是否准确再根据事件时间反查对应时段的网络访问日志、应用程序日志。如果只是偶发一次多半是某个旧客户端不支持新的TLS版本如果频繁出现就要结合网络抓包和服务器配置来看。不要把“日期看起来奇怪”当作根因真正的问题往往在握手双方的安全协议配置上。6. 日期问题排查方法论与避坑清单6.1 日期题出错率最高的几个点根据我看到的和亲身踩过的坑整理了一份高频错误清单你在排查任何日期问题的时候都可以先对照一遍。问题点典型表现解决办法月份从0开始JS的getMonth()返回5却显示6月手动加1闰年判断错误1900年被误判为闰年用完整规则%4 !%100时区偏移toISOString()结果比本地时间早8小时展示用本地时间方法传输用UTC语言环境导致格式不一致Java输出“8 八月”而不是“8 August”格式化时显式指定Locale日期解析依赖区域设置SQL Server报“字符串未识别为有效日期”使用CONVERT样式码或ISO格式时间戳单位混乱10位秒级时间戳被当毫秒使用先确认单位必要时乘1000只计算不校验2月30日也被当合法日期加maxDay校验跨时区存储本地时间数据库存了“2025-08-08 00:00”却没存时区统一UTC或字段带时区后缀6.2 通用排错流程五步遇到日期相关bug别急着改代码先按下面这个顺序走一遍能解决绝大多数问题。第一步明确输入是什么。是字符串、时间戳、Date对象还是数据库日期字段不同输入类型的坑完全不同。第二步明确输出要求。是要显示给用户看还是作为参数传给其他系统显示用本地时间接口传输最好用UTC或ISO字符串。第三步确认中间经历了几次转换。从数据库到后端从后端到前端从前端到展示每一次转换都可能引入时区或格式差异。第四步统一中间表示。我通常的做法是存储和传输一律用UTC或ISO 8601字符串只在最后一层展示时才转成本地时间。第五步写边界测试。至少覆盖2月28日、2月29日、闰年2月29日、3月1日、12月31日、1月1日这些日期如果涉及时区再加一个夏令时切换日的用例。6.3 我建议你写进项目规范的小约定踩过几次坑之后我越来越觉得日期问题主要是规范问题不是技术问题。只要团队里每个人都按同一套约定写代码大部分坑根本不会出现。第一所有数据库日期字段如果涉及时区命名就带上时区后缀比如created_at_utc避免后人误解。第二接口文档里日期格式必须写死比如yyyy-MM-ddTHH:mm:ssZ不接受“看情况”。第三日志里打印时间时一律带时区否则排查问题时根本分不清是哪个时区的凌晨。第四外部接口如果允许用户传日期字符串解析时必须显式指定格式不要用默认解析。第五尽量避免在业务代码里手工拼接日期字符串统一走工具类。我个人在实际操作中的体会是日期问题看起来散、看起来杂但只要心里有一根弦时刻问自己“这个时间现在是什么时区、什么格式、要转成什么”很多问题在写代码阶段就能避开。这份题目集里的每一题本质上都是在帮我们强化这根弦。希望下一次你对着屏幕上的日期报错发愁时能想起其中某一条少走几步弯路。
返回列表