资讯动态

用Java实现程序员老黄历:命令行工具的日期与随机实战

发布时间:2026/10/18 16:28:06 来源:尧图企业网站定制
简介命令行工具是程序员日常效率的重要载体用Java实现可以跨平台分发适合构建小型实用工具。开发这类工具时日期时间处理是基础难题不仅要解决时区偏移还需处理农历等复杂历法转换通常借助查表库完成。随机数的设计也需兼顾趣味与可复现性以日期作为随机种子的洗牌算法能保证同一天结果稳定隔天自然变化。技术价值在于通过工程化封装把日期、词库、组合逻辑分层解耦既便于测试也便于扩展。这类工具很适合作为团队内网彩蛋或每日终端仪式本文以程序员老黄历为例完整演示了从日期推算、词库随机到命令行输出与打包的Java实践路径。1. 程序员老黄历用 Java 把「今日宜忌」做成每天能跑的命令行工具每天开终端除了git status和ps aux总感觉少点仪式感。程序员老黄历就是干这个的把传统老黄历的「宜/忌」翻译成程序员听得懂的话——「宜重构旧代码、忌深夜上线」随机配上当天农历和节气让人打开终端先乐一下。这个标题里的「Java 实现」指的是不依赖 Node、Python 等解释器一个 jar 包到处跑天然适合拿来练 Java 的时间 API、文件读取、集合随机、命令行参数解析和打包发布这一整条链路。这个实例最适合两类人刚学完 Java 语法、想找一个「有完整闭环」的小项目练手的人以及想给团队内网塞一个彩蛋工具、但又不想引入额外运行时的人。它不复杂却能把 Java 日常开发里最常踩的坑全踩一遍——日期、编码、随机、打包每个坑都有后悔药。接下来我把这个工具从数据底座到命令行输出拆开讲代码可以直接照着抄。2. 日期与农历老黄历的第一层硬仗星期、农历与节气的推算老黄历的核心不是随机「宜忌」而是「今天」。要输出「2025-06-01 星期日农历五月初七」就必须处理公历转农历。这一步是绝大多数入门项目翻车的起点还在用Date和SimpleDateFormat时区不统一农历全靠手推公式最后算出来闰月对不上。我这边的做法是分三层统一日期源 → 引入农历转换库 → 组装成统一的日历信息对象后面输出层只认这个对象。2.1 先统一时区LocalDate.now()并不是「今天」LocalDate.now()看起来很直接实际上它依赖 JVM 的默认时区。如果你把 jar 丢到一台时区是 UTC 的服务器上跑它会比北京时间少一天输出就成了「昨天」。老黄历这类工具对日期极其敏感这种错误会直接让宜忌内容跟着错位。我一般会在一开始就显式处理时区并把时区做成可覆盖的参数import java.time.LocalDate; import java.time.ZoneId; public class DateSource { public static LocalDate today(ZoneId preferredZone) { // 没传时区就用系统默认但明确写出避免隐式踩坑 ZoneId zone (preferredZone ! null) ? preferredZone : ZoneId.systemDefault(); return LocalDate.now(zone); } }这里的关键是ZoneId.of(Asia/Shanghai)这种写法可以把时区固定下来适合部署在海外服务器上、但面向国内用户的服务。LocalDate本身不含时分秒天然避开了「23:59:59 算哪天」的边界问题如果你误用new Date()配合SimpleDateFormat再转LocalDate中间经过的毫秒值和默认时区都是隐患。把时区显式写出来等于提前排掉一颗雷。2.2 农历转换用纯 Java 查表库别自己推算法农历是天文历闰月规则复杂手推需要庞大的天文数据支撑不现实。常见做法是引入一个纯 Java 实现的农历转换库内部维护一张 1900—2100 年的农历数据表换算本质是查表加偏移。这类库接口大同小异通常有两个门面类Solar表示公历Lunar表示农历结果对象。// 以某纯 Java 农历库的典型 API 为例 // Solar.fromYmd() 接收公历的年月日返回公历对象 // getLunar() 完成公历到农历的转换 Solar solar Solar.fromYmd(today.getYear(), today.getMonthValue(), today.getDayOfMonth()); Lunar lunar solar.getLunar(); String lunarText lunar.getYearInChinese() 年 lunar.getMonthInChinese() 月 lunar.getDayInChinese();参数说明fromYmd的三个参数分别是年、月、日顺序固定getYearInChinese()返回「二〇二五」这类中文年号getMonthInChinese()返回「五」这样的数字月份getDayInChinese()返回「初七」。不同库的方法名可能略有差异但整体设计思路一致——你选的库只要按官方示例改一下方法名即可。选库时注意三点覆盖年份范围最好含 1900—2100、是否支持闰月、是否线程安全。后两个问题我在第 5 章展开。2.3 组装日历信息对象星期、农历、节气一起交给输出层把日期、星期、农历文本、节气/节日打包成一个对象输出层只认这个对象不关心它怎么算出来的。这样后面想加「星座」「五行」「幸运色」都只改这一处。public class CalendarInfo { public final LocalDate date; public final String weekLabel; // 星期日 public final String lunarText; // 农历五月初七 public final String termOrFestival; // 节气或节日名没有则为空字符串 public CalendarInfo(LocalDate date, String weekLabel, String lunarText, String termOrFestival) { this.date date; this.weekLabel weekLabel; this.lunarText lunarText; this.termOrFestival termOrFestival; } }这个类看似简单但它界定了「数据层」和「展示层」的边界。weekLabel用DayOfWeek. of(date.getDayOfWeek().getValue())的中文映射就能拿termOrFestival直接取农历库返回的节气或节日字段。构建过程用了一个专门的 Builder先算农历再查节气最后组装。这一步的代码不长但值得单独抽出来因为老黄历的输出格式迭代很快今天要「农历五月初七」明天可能改成「乙巳年五月初七」集中管理就方便。3. 宜忌词库与随机策略让每天翻开都不一样日历信息是「定数」宜忌是「变数」。如果每天输出完全一样老黄历就没有「翻牌」的趣味如果每跑一次都完全随机又没了「今天就是今天」的确定性。这个矛盾靠两件事解决词库要结构化随机要可控。词库不是一堆字符串而是「动词短语 可选对象」的组合随机不是Math.random()而是按日期做种子的洗牌。3.1 词库结构动词短语比单个词更像「老黄历」传统老黄历写「宜嫁娶、忌安葬」程序员版写「宜重构代码、忌临时改接口」。直接把完整动词短语作为词条比拆成动词和宾语再拼接更自然也更容易维护。我用Properties文件做词库零第三方依赖放在src/main/resources下yi重构旧代码,提交代码,写单元测试,整理文档,修复历史包袱,与产品对需求 ji深夜上线,临时改接口,在周五下午加需求,手写SQL跑生产库,删除未提交的代码选择Properties的原因很朴素Java 原生支持读取代码最短。如果你希望词库带「对象」维度比如「宜重构 遗留系统」那就要 JSON 结构了——第 3.3 节我会讲升级路径。这里先把「动词短语」这个粒度定下来随机、去重、拼接都简单很多。3.2 可复现的不重复随机按日期做种子用洗牌代替 nextInt「不重复」指的是同一天内多次运行输出必须完全一致「相邻两天不要连续撞同一条」。这要求随机其实是伪随机——以日期为种子同一天种子相同序列就相同日期变了种子变序列就跟着变。import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; public class DailyPicker { private final ListString pool; private final long daySeed; private String last; // 上一次前一天输出过的第一条 public DailyPicker(ListString pool, long daySeed) { this.pool new ArrayList(pool); this.daySeed daySeed; } public ListString pick(int count) { // 每次在副本上洗牌不污染原始词库顺序 ListString copy new ArrayList(pool); Collections.shuffle(copy, new Random(daySeed)); // 避免与前一天第一条撞车 if (!copy.isEmpty() copy.get(0).equals(last)) { Collections.swap(copy, 0, copy.size() - 1); } last copy.get(0); return copy.subList(0, Math.min(count, copy.size())); } }逻辑说明daySeed用date.toEpochDay()即可——它是自 1970-01-01 起的整数天数同一天恒等隔天必然不同。Collections.shuffle接受Random实例种子固定打乱结果就可以复现。last字段保存的是前一天第一条的具体文本而不是索引因为洗牌后索引位置变了存索引会失效。最后subList截取前count条作为当天的宜忌列表。这个方案有一个取舍它是「无放回」地从当天打乱序列里取所以一天内取 3 条一定不重复但隔天可能碰上相同条目这是刻意的——词库就十几条完全不重复反而显得不自然。真要严格避免跨天重复可以把last换成一组「近三天已用条目」的集合取数时逐个跳过代价是可能取不满需要在词库规模上做补偿。3.3 词库外置把换词变成改配置顺便避开编码坑把词库放配置文件里意义在于「换词不重新编译」。加载逻辑放在WordsLoader里注意用Reader而不是InputStream否则中文会乱码import java.io.BufferedReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.Properties; public class WordsLoader { public static Words load(String path) throws Exception { Properties props new Properties(); // 必须用 Reader 加载InputStream 会按 ISO-8859-1 读中文全花 try (BufferedReader reader Files.newBufferedReader(Paths.get(path), StandardCharsets.UTF_8)) { props.load(reader); } ListString yi split(props.getProperty(yi, )); ListString ji split(props.getProperty(ji, )); return new Words(yi, ji); } private static ListString split(String raw) { return Arrays.stream(raw.split(,)) .map(String::trim) .filter(s - !s.isEmpty()) .toList(); } }参数说明Files.newBufferedReader的第二个参数强制 UTF-8props.load(reader)是Properties专门为字符流设计的方法会正确处理中文。如果你图省事用props.load(Files.newInputStream(...))遇到中文词条会变成问号这是老黄历工具最常见的翻车点之一第 5 章我会再细讲。如果词库规模变大想要「宜 目标对象」的 JSON 结构常见做法是引入工程里已有的 JSON 类库把[{ verb: 重构, target: 遗留代码 }]反序列化成ListWord再把verb target拼成短语。Loader 的接口不变只换内部实现DailyPicker完全不用动。4. 命令行交互与最终输出把「能跑」变成「值得每天跑」工具的核心价值在终端里那几行输出。如果输出乱糟糟词库再好也没人愿意每天打开。这一章解决三件事参数怎么传、颜色怎么加、输出版式怎么定。最后落到打包因为「能在 IDE 跑」和「双击 jar 能跑」之间还差一个资源路径的坑。4.1 参数解析日期、词库路径、颜色开关做成套路化解析器老黄历默认跑「今天」但做实例一定要支持--date指定日期否则测试、演示、提前看明天的宜忌都做不了。参数解析不引第三方库手写一个循环足够import java.time.LocalDate; import java.time.format.DateTimeFormatter; public class CliOptions { public LocalDate date LocalDate.now(); public String wordsPath words.properties; public boolean color false; private static final DateTimeFormatter DATE_FMT DateTimeFormatter.ofPattern(yyyy-M-d); public static CliOptions parse(String[] args) { CliOptions o new CliOptions(); for (int i 0; i args.length; i) { switch (args[i]) { case --date - o.date LocalDate.parse(args[i], DATE_FMT); case --words - o.wordsPath args[i]; case --color - o.color true; default - System.err.println(未知参数: args[i]); } } return o; } }参数说明--date 2025-6-1也能被yyyy-M-d解析这是特意选宽松格式LocalDate.parse()默认严格要求2025-06-01用户手输很容易翻车。--color是布尔开关带上就启用 ANSI 颜色不带则输出纯文本。三个参数已经覆盖日常使用想加--days 7批量模式就在这个循环里加一个 case。4.2 终端彩色输出与对齐ANSI 转义是最小成本的仪式感颜色是「仪式感」的主要来源。老黄历的「宜」要用绿色「忌」要用红色日期标题用灰色这套视觉语言能让人一眼分清主次。实现方式是 ANSI 转义序列不需要任何依赖// ANSI 颜色常量仅在 --color 开启时使用 public static final String RESET \033[0m; public static final String GREEN \033[32m; public static final String RED \033[31m; public static final String YELLOW \033[33m; public static final String GRAY \033[90m;原理说明\033[32m告诉终端「之后的文本用绿色」\033[0m恢复默认。Windows 10 以上的 PowerShell 和 Windows Terminal 都支持老式 cmd 会原样打印转义字符所以必须有--color开关不传就纯文本输出。对齐方面中文字符在终端里占两个英文字符宽度直接用String.format(%-8s)会错位我建议用、拼接列表项让每行自然右对齐不做补空格System.out.println(宜 String.join(、, yiList)); System.out.println(忌 String.join(、, jiList));4.3 输出版式标题、宜忌、提醒三行定稿输出分成三块第一行是日期和农历第二行是宜第三行是忌。主流程组装如下public static void main(String[] args) { CliOptions opts CliOptions.parse(args); // 1. 日历信息日期、星期、农历、节气 CalendarInfo info CalendarInfoBuilder.build(opts.date); // 2. 词库加载 Words words WordsLoader.load(opts.wordsPath); // 3. 按日期种子取宜忌 DailyPicker yiPicker new DailyPicker(words.yi, opts.date.toEpochDay()); DailyPicker jiPicker new DailyPicker(words.ji, opts.date.toEpochDay()); ListString yiList yiPicker.pick(3); ListString jiList jiPicker.pick(3); // 4. 输出 String head info.date info.weekLabel info.lunarText ; if (opts.color) { System.out.println(GRAY head RESET); System.out.println(GREEN 宜 String.join(、, yiList) RESET); System.out.println(RED 忌 String.join(、, jiList) RESET); } else { System.out.println(head); System.out.println(宜 String.join(、, yiList)); System.out.println(忌 String.join(、, jiList)); } }实际运行效果$ java -jar laohuangli.jar --date 2025-06-01 --color 2025-06-01 星期日农历五月初七 宜重构旧代码、写单元测试、整理文档 忌深夜上线、临时改接口、删除未提交代码注意宜和忌用了两个独立的DailyPicker实例但种子相同这是刻意的——两个池子各自洗牌互不干扰「宜」和「忌」可以出现同类词但不会因为共享一个随机序列导致「今天宜加班、忌重构」这种自相矛盾的组合。4.4 打包成可执行 jar资源路径与主类缺一不可IDE 里跑通只是第一步交付给同事用必须是一个可执行 jar。用构建工具一行命令搞定但有两个配置点容易被忽略。# 构建工具会读取配置里的 mainClass把依赖和资源一起打进 jar mvn clean package java -jar target/laohuangli.jar --date 2025-06-01第一个点是mainClass必须指向带main方法的类第二个点是words.properties要放在src/main/resources下这样打包后资源在 jar 内部。而 jar 内部的资源不是磁盘文件代码里不能再用new File(words.properties)必须通过类加载器读取——这个坑我放在下一章细说。5. 老黄历避坑清单日期、编码、随机与打包的翻车现场这个项目不大但每一个环节都有对应的经典翻车现场。下面这五个坑是我做类似工具时反复遇到的按「现象 → 原因 → 解决」列出读者照着排查能省下一个下午。5.1 中文乱码错把InputStream当Reader用现象加载词库后终端输出「宜????」或者「锟斤拷」词库里的中文全部变成乱码。原因Properties.load(InputStream)固定按 ISO-8859-1 解码中文字符超出单字节范围就被替换成问号控制台输出时如果 JVM 默认编码和终端不一致也会二次乱码。解决加载统一走Files.newBufferedReader(path, StandardCharsets.UTF_8)再props.load(reader)运行时给 JVM 加-Dfile.encodingUTF-8参数。代码里不要用getBytes()默认编码做任何字符串转换。5.2 「今天」在 UTC 服务器上提前了半天现象同一份 jar在自己电脑上跑输出 6 月 1 日放到海外服务器上跑变成 5 月 31 日。原因LocalDate.now()使用的默认时区来自操作系统服务器时区如果是 UTC零点之后到北京时间早上 8 点前都算「前一天」。解决统一时区入口。前面写的DateSource.today(ZoneId)就是为此设计的生产环境部署时显式传Asia/Shanghai或者读环境变量里的时区配置。5.3 固定种子「不固定」Random的种子取错了现象想实现「同一天内多次运行结果一致」但实际每次跑出来的宜忌都不一样。原因用new Random()或new Random(System.currentTimeMillis())做了随机种子。前者依赖系统时间纳秒后者直接依赖当前毫秒值隔几秒跑一次种子就变了。解决种子用date.toEpochDay()这是日期的自然整数表示。同一天反复跑种子恒定隔天自动改变。测试代码里也可以直接断言「同一种子输出相同列表」把随机行为变成可验证的。5.4 打包后找不到词库文件File指向了「不存在」的路径现象在 IDE 里运行一切正常java -jar之后报FileNotFoundException词库加载失败。原因IDE 运行时工作目录是项目根目录new File(words.properties)能找到源代码目录下的文件jar 包运行时的当前目录是终端所在目录jar 内部的资源并不以磁盘文件形式存在。解决词库打包进 jar 后用类加载器读取// 从 classpath 根目录读取资源适配 jar 内运行 try (InputStream in WordsLoader.class.getResourceAsStream(/words.properties)) { props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); }注意/words.properties开头的斜杠表示 classpath 根目录如果同时支持外部词库路径先判断参数是否指向真实文件不存在再回退到类加载器两种模式都要用Reader解码。5.5 农历库的「玄学边界」闰月与数据表范围现象某些日期转换出来的农历月份对不上比如闰二月丢了一个月或者 1900 年之前的日期直接抛异常。原因农历数据表覆盖范围有限典型库只保证 1900—2100 年准确部分库对闰月的边界处理规则不同跨闰月时查表偏移一两天。解决转换逻辑包一层防御超出数据表范围就降级为「今日无黄历」或直接输出公历日期不让整个工具崩掉try { Lunar lunar solar.getLunar(); return lunar.getMonthInChinese() 月 lunar.getDayInChinese(); } catch (Exception e) { // 超出农历库数据范围降级为公历输出 return today.getMonthValue() 月 today.getDayOfMonth() 日; }另外如果工具会以多线程方式批量跑「未来 7 天宜忌」注意你选的库是否线程安全不确定就每次转换创建一个新实例或者加一个synchronized包装性能损失对这个量级的工具可以忽略。6. 让老黄历变成「有工程感的玩具」测试、批量输出与日常入口项目做到能跑、能打包只能算完成一半另一半是让别人包括未来的自己敢改、能验。我收尾的习惯是补三件事固定种子的单元测试、批量输出模式、终端别名接入。测试的重点不是数学逻辑而是「随机可复现」和「词库不脏」。固定种子的测试可以这样写用两个DailyPicker实例同一日期种子取三次列表断言每次结果一致再换一个日期种子断言结果变化。这类测试能把随机算法牢牢锁住防止有人后来把toEpochDay()换成currentTimeMillis()还不自知。批量输出模式是个小技巧加一个--days 7参数循环生成未来一周的黄历。实现上比单日多一层循环但价值很大——团队周报末尾贴一段本周宜忌或者每天早上的定时任务把当天黄历推到群公告都靠这个模式。循环里注意每次都要新建DailyPicker不要复用同一个实例否则last字段会把前一天的「不重复」约束带到下一天打乱隔天重排的节奏。日常入口方面我在自己常用的终端配置里加了一行别名alias laohuanglijava -jar ~/tools/laohuangli.jar --color这样每天开终端敲一下「laohuangli」就是当天的宜忌。随着词库积累我养成了一个习惯每逢真实踩坑就把那条经历写进忌词库比如「手写 SQL 跑生产库」就是某次事故后加进去的。这个项目成了我的「踩坑日记」既好玩又能提醒自己。最早的版本我也用过Date和SimpleDateFormat后来整体切到java.time才算消停——日期处理这事选对 API 比修 bug 省力得多。希望这份实例能帮到你把你自己的版本跑起来。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑