尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

JAVA万年历源码实战:公历农历节气计算与避坑指南

JAVA万年历源码实战:公历农历节气计算与避坑指南 简介这份JAVA万年历源码面向Java初学者与课程设计需求者以单文件calendar类实现万年历与记事本二合一桌面应用帮助读者在实战中掌握日期时间处理、Swing界面设计与事件驱动编程。包内共1个doc文件约317KB为完整的Java课程设计报告涵盖总体设计、变量与方法表、源代码及运行效果说明可对照理解Calendar与java.time的日期操作、JComboBox月份选择、JButton事件响应、JTextArea日记输入、文件读写与异常处理等实现细节。报告还梳理了闰年判断、月份起始天与总天数计算、多种布局管理器组合以及主函数初始化流程并附有保存、删除日记的确认对话框设计思路。目前已有184人学习适合需要快速获取可运行参考代码、理清课程设计文档结构或查漏补缺的读者借鉴使用。1. 从一份 JAVA万年历源码 说起为什么日期逻辑总在边界翻车很多人第一次接触 JAVA万年历源码是把它当成一个练手小项目输入年份和月份打印一张月历标出周末仅此而已。但真正把它放到业务里你会发现它其实是一套完整的日期计算引擎——闰年判定、星期推算、农历转换、节气计算、周数归属每一个都是边界条件密集的雷区。我见过太多人拿着网上抄来的万年历代码跑 2024 年没问题一跑到 1900 年或者 2100 年就集体翻车星期全错、农历对不上、跨年周数算成第 0 周。这份 JAVA万年历源码要解决的核心问题是把「给定任意公历日期输出正确的星期、农历、节气、周数」这件事做扎实。它适合两类人一类是想拿它当 Java 基础综合练习的开发者另一类是需要在自己系统里嵌入日期计算模块、又不想引入重型第三方库的工程师。下面我按自己实际改过几版的思路把选型、实现、参数和坑一条条拆开讲。2. 万年历的日期底座公历算法怎么选、怎么算2.1 闰年与月份天数的判定逻辑公历部分看起来最简单但恰恰是错误率最高的地方。闰年规则是三条件组合能被 4 整除且不能被 100 整除或者能被 400 整除。很多人只写了前两条导致 1900 年被判成闰年2 月多出一天后面所有日期全部偏移。/** * 判断是否为公历闰年 * param year 公历年份建议范围 1900-2100 * return true 表示闰年 */ public static boolean isLeapYear(int year) { // 核心三条件4年一闰百年不闰四百年再闰 return (year % 4 0 year % 100 ! 0) || (year % 400 0); } /** * 获取指定年月的天数 * param year 年份 * param month 月份 1-12 * return 该月天数 */ public static int getDaysOfMonth(int year, int month) { // 用数组存平年每月天数下标 0 占位方便 month 直接索引 int[] days {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (month 2 isLeapYear(year)) { return 29; // 闰年二月单独处理 } return days[month]; }这段代码的关键参数是year的取值边界。我一般会把有效范围锁在 1900 到 2100 之间因为超出这个范围后农历数据表和节气近似算法的误差会明显放大。getDaysOfMonth里用数组而不是 switch是为了后续扩展农历月天数时结构一致减少分支。2.2 基姆拉尔森公式推算星期几星期推算是万年历的骨架。常见做法有三种查表法、蔡勒公式、基姆拉尔森公式。查表法需要预存大量数据蔡勒公式对月份 1、2 月要做年份减一处理容易写错。我一般用基姆拉尔森公式它对月份做了 13、14 月的映射逻辑更顺。/** * 基姆拉尔森公式计算星期 * param year 年份 * param month 月份 1-12 * param day 日期 1-31 * return 0周日, 1周一 ... 6周六 */ public static int getWeekDay(int year, int month, int day) { // 1月和2月看作上一年的13月和14月这是公式成立的前提 if (month 1 || month 2) { month 12; year--; } // 公式主体注意整数除法会截断顺序不能随意调换 int w (day 2 * month 3 * (month 1) / 5 year year / 4 - year / 100 year / 400 1) % 7; return w; }参数说明返回值 0 代表周日这是公式的约定不是错误。如果你习惯周一为 0需要在返回前做一次(w 6) % 7的转换。这里最容易踩的坑是月份映射后忘记year--导致 1 月 1 日的星期整体偏移一天。我建议写完立刻用 2000 年 1 月 1 日星期六做基准验证。2.3 打印月历的排版与对齐排版本身不难但空格对齐和换行位置是新手最容易做出「看起来对、实际错位」的地方。核心是算出当月 1 号是星期几然后在前面补对应数量的空位。/** * 打印指定年月的月历 */ public static void printMonth(int year, int month) { System.out.println(一 二 三 四 五 六 日); int firstDayWeek getWeekDay(year, month, 1); // 把周日0 转成 周一0 的排布方便从周一开始打印 int offset (firstDayWeek 6) % 7; int days getDaysOfMonth(year, month); // 打印前导空格每个日期占4个字符宽度 for (int i 0; i offset; i) { System.out.print( ); } for (int d 1; d days; d) { System.out.printf(%-4d, d); // 每到周日换行注意 offset 参与计算 if ((offset d) % 7 0) { System.out.println(); } } System.out.println(); }%-4d里的 4 是列宽要和前导空格的 4 个空格保持一致否则整张表会歪。(offset d) % 7 0判断的是「当前日期落在周日」这个条件写错一位整月就会从周六开始换行。我一般会拿 2024 年 2 月闰月1 号周四和 2023 年 1 月1 号周日两个极端用例来验证排版。3. 农历与节气数据表怎么建、转换怎么不跑偏3.1 农历数据表的压缩存储方案农历没有简单公式必须依赖数据表。常见做法是把每年的农历信息压缩成一个十六进制整数每一位或几位表示闰月大小、闰月月份、各月大小。这种压缩表体积小但可读性差改起来容易出错。// 农历数据表每年一个十六进制值范围 1900-2100 // 每个值从高位到低位依次表示闰月大小、闰月月份、12个月大小 private static final int[] LUNAR_INFO { 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, // 1900-1904 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, // 1905-1909 // ... 中间年份按同样格式补齐 0x0d520, 0x0aa00, 0x0a4d0, 0x0d250, 0x0d558 // 2096-2100 }; /** * 解析某年农历闰月月份0 表示无闰月 */ public static int getLeapMonth(int year) { int info LUNAR_INFO[year - 1900]; // 低4位存闰月月份 return info 0xf; } /** * 解析某年农历闰月天数无闰月返回 0 */ public static int getLeapMonthDays(int year) { if (getLeapMonth(year) 0) { return 0; } // 第16位表示闰月大小1为大月30天0为小月29天 return ((LUNAR_INFO[year - 1900] 0x10000) ! 0) ? 30 : 29; }参数说明LUNAR_INFO的下标是year - 1900所以年份范围必须严格卡在 1900 到 2100。 0xf取低 4 位得到闰月月份 0x10000取第 17 位判断闰月大小。这张表是农历转换的命根子任何一位写错对应年份的农历就会整体错乱。我建议不要手敲用脚本从可靠数据源生成后做校验。3.2 公历转农历的完整流程有了数据表转换流程就是「算差值、逐年减、定位月日」。核心是算出公历日期距离 1900 年 1 月 31 日农历基准日的总天数然后逐年扣除农历年的天数。/** * 公历转农历 * return int[]{农历年, 农历月, 农历日, 是否闰月} */ public static int[] solarToLunar(int year, int month, int day) { // 计算距离 1900-01-31 的天数差 int offset (int) (daysBetween1900(year, month, day)); int lunarYear 1900; int daysOfYear; // 逐年扣减直到剩余天数不足一年 while (true) { daysOfYear getLunarYearDays(lunarYear); if (offset daysOfYear) { break; } offset - daysOfYear; lunarYear; } int leapMonth getLeapMonth(lunarYear); boolean isLeap false; int lunarMonth 1; // 逐月扣减注意闰月要单独处理 while (true) { int daysOfMonth; if (leapMonth 0 lunarMonth leapMonth 1 !isLeap) { // 到达闰月位置先处理闰月 daysOfMonth getLeapMonthDays(lunarYear); isLeap true; lunarMonth--; // 回退让闰月复用上一个月序 } else { daysOfMonth getLunarMonthDays(lunarYear, lunarMonth); } if (offset daysOfMonth) { break; } offset - daysOfMonth; lunarMonth; } return new int[]{lunarYear, lunarMonth, offset 1, isLeap ? 1 : 0}; }这段逻辑里lunarMonth--那一步是血泪经验闰月跟在同月序后面如果不回退月序闰月之后的月份会整体多算一个月。offset 1是因为 offset 是零基天数日期要从 1 开始。验证方法很简单拿几个已知农历日期反查比如 2024 年春节是公历 2 月 10 日转换结果必须是正月初一。3.3 二十四节气的近似计算节气计算有两种路线查表法和近似公式法。查表法精确但数据量大近似公式法用三角函数逼近误差在几分钟到几小时之间对日历展示足够用。/** * 计算指定年份第 n 个节气的日期近似公式 * param year 年份 * param n 节气序号 0-230 为小寒 * return 该节气的日 */ public static int getTermDay(int year, int n) { // 世纪常数20世纪和21世纪取值不同 double centuryValue (year 2000) ? 5.15 : 4.6295; // 通用节气公式Y 为年份后两位 int y year % 100; double day (y * 0.2422 centuryValue) - (y / 4); // 部分节气有特殊修正值这里以常见修正为例 if (n 0 year 2019) { day - 1; // 特殊年份修正 } return (int) day; }参数说明centuryValue是世纪修正常数20 世纪和 21 世纪不同用错会导致整体偏移一天。n的取值 0 到 23 对应小寒到大寒的固定顺序。这个公式的误差在个别年份会达到一天所以生产环境我一般会叠加一张修正表把已知偏差年份单独处理。如果你只是做展示近似公式够用如果要做节气提醒建议用天文算法或权威数据表。4. 避坑与排查万年历源码里最容易翻车的 5 个点4.1 星期计算在 1 月 2 月整体偏移现象跑 2024 年 1 月星期全部比正确值多一天或少一天但 3 月以后正常。原因基姆拉尔森公式要求 1 月、2 月映射为上一年的 13 月、14 月很多人只改了月份没改年份。解决在月份映射后立刻执行year--并用 2000 年 1 月 1 日做基准回归测试。4.2 农历转换在闰月年份错乱现象遇到有闰月的年份闰月之后的农历日期全部偏移一个月。原因逐月扣减时没有正确处理闰月的月序回退导致闰月被当成独立月份多扣一次。解决在进入闰月分支时执行lunarMonth--让闰月复用前一个月序同时用isLeap标记防止重复进入。4.3 日期差值计算在跨年时算错现象12 月 31 日到次年 1 月 1 日的天数差算成 0 或 2。原因用month * 30之类的粗略估算代替真实累加或者闰年 2 月没有单独处理。解决写一个daysBetween1900方法从 1900 年 1 月 1 日逐年逐月累加真实天数不要用任何近似。4.4 数据表下标越界导致数组异常现象输入 1899 年或 2101 年时抛出ArrayIndexOutOfBoundsException。原因LUNAR_INFO表只覆盖 1900 到 2100下标计算year - 1900在边界外为负或超长。解决在入口处做范围校验超出范围直接返回错误码或抛出带说明的异常不要静默处理。4.5 排版在月初为周日时多打一行空行现象当月 1 号是周日时月历第一行是空的日期从第二行开始。原因前导空格计算用了firstDayWeek而没有转成周一为 0 的偏移导致周日被当成 0 个空格处理。解决统一用(firstDayWeek 6) % 7转成周一为 0 的偏移再参与空格和换行计算。5. 把万年历做成可复用模块接口设计与验证技巧5.1 用统一入口封装公历、农历、节气散落的静态方法用起来累我一般会抽一个CalendarService接口把公历、农历、节气、周数四类查询收口。这样调用方不需要关心内部用的是公式还是查表后续换算法也不影响上层。public interface CalendarService { /** 查询某天的完整信息 */ DayInfo query(int year, int month, int day); /** 查询某月月历数据 */ MonthInfo queryMonth(int year, int month); } public class DayInfo { public int weekDay; // 0周日 public int lunarYear; public int lunarMonth; public int lunarDay; public boolean isLeap; // 是否闰月 public String term; // 当日节气无则为 null }接口设计的关键是DayInfo里把「是否闰月」单独拎出来而不是把闰月编码进月份。这样调用方判断「闰四月初一」时只需要看lunarMonth 4 isLeap逻辑清晰也不容易在展示层出错。5.2 用已知日期做回归验证万年历最怕「看起来对」。我习惯建一张小验证表覆盖闰年、闰月、节气、跨年四类边界每次改完代码跑一遍。验证项输入公历期望结果闰年 2 月2024-02-29存在星期为周四平年 2 月2023-02-29不存在应报错闰月转换2023-03-22农历闰二月初一节气2024-02-04立春跨年周数2024-12-30属于 2025 年第 1 周这张表不用大但每一条都要能精确断言。我一般写成单元测试用assertEquals逐条比对改算法时先跑测试再提交。5.3 性能与缓存什么时候该加、什么时候别加万年历的计算量很小单次查询在微秒级绝大多数场景不需要缓存。但如果你要在页面上渲染一整年的日历或者做批量日期转换重复计算农历数据表解析就会成为热点。我的做法是只缓存「农历年天数」和「农历月天数」这两个纯函数结果用int数组按年份索引避免每次循环都重新解析十六进制。// 预计算并缓存每年的农历总天数避免重复解析 private static final int[] LUNAR_YEAR_DAYS new int[201]; static { for (int i 0; i LUNAR_YEAR_DAYS.length; i) { LUNAR_YEAR_DAYS[i] calcLunarYearDays(1900 i); } }注意缓存数组长度要和年份范围严格对应1900 到 2100 是 201 年写 200 就会在 2100 年越界。这个坑我在早期版本里踩过表现是查询 2100 年 12 月时抛异常排查了半天才发现是数组长度差一。5.4 一个容易被忽略的技巧用java.time做交叉校验JDK 8 之后的java.time包本身不提供农历但它的公历计算非常可靠。我一般用它来交叉校验自己写的星期和天数逻辑比如用LocalDate.of(year, month, day).getDayOfWeek()对比基姆拉尔森公式的结果。两者一致公历部分基本可以放心不一致优先怀疑自己的公式。// 交叉校验自己的星期算法 vs java.time int myWeek getWeekDay(2024, 2, 29); int jdkWeek LocalDate.of(2024, 2, 29).getDayOfWeek().getValue() % 7; // jdkWeek 周一1...周日7取模后周日0与 myWeek 对齐 assert myWeek jdkWeek : 星期计算不一致;这个技巧的好处是不依赖任何外部数据纯靠 JDK 自带能力就能把公历部分的正确性兜住。农历和节气没有现成 API只能靠数据表和验证表但公历是地基地基稳了上面的问题才好定位。我自己的习惯是每次动农历数据表之前先把公历部分的回归测试跑绿再改表改完立刻用验证表全量比对。这样即使出错也能快速判断是公历逻辑被带偏了还是数据表本身写错了。希望帮到你。本文还有配套的精品资源点击获取
返回列表