引言
日期计算在日常开发中无处不在:计算两个日期间隔、判断某年某月某天是星期几、推算节假日等。看似简单的日期运算背后涉及闰年规则、历法变迁等复杂逻辑。本文用Java实现蔡勒公式(Zeller’s Congruence)等经典日期算法,并构建完整的日历计算工具库。
蔡勒公式原理
蔡勒公式是一种计算任意日期星期几的数学公式,由克里斯蒂安·蔡勒于1882年提出。
格里高利历公式
对于日期 Y年M月D日:
– 若月份为1月或2月,视为上一年的13月或14月
– 计算:h = (q + [13(m+1)/5] + K + [K/4] + [J/4] – 2J) mod 7
– 其中 q=日,m=月,K=世纪年数,J=世纪数
– h=0为周六,h=1为周日,以此类推
Java实现
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
/**
* 日历计算工具类:包含蔡勒公式、日期间隔、闰年判断等算法
*/
public class CalendarCalculator {
private static final String[] WEEKDAY_NAMES = {
"星期六", "星期日", "星期一", "星期二", "星期三", "星期四", "星期五"
};
/**
* 蔡勒公式:计算指定日期是星期几
* @param year 年份(如2024)
* @param month 月份(1-12)
* @param day 日(1-31)
* @return 星期名称
*/
public static String getWeekdayByZeller(int year, int month, int day) {
if (month < 1 || month > 12 || day < 1 || day > 31) {
throw new IllegalArgumentException("无效日期");
}
int y = year;
int m = month;
int q = day;
// 1月和2月当作上一年的13月和14月
if (m <= 2) {
m += 12;
y -= 1;
}
int k = y % 100; // 世纪内的年份
int j = y / 100; // 世纪数
// 蔡勒公式核心计算
int h = (q + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7;
// 处理负数取模
if (h < 0) h += 7;
return WEEKDAY_NAMES[h];
}
/**
* 判断是否为闰年(格里高利历规则)
* 规则:能被4整除但不能被100整除,或能被400整除
*/
public static boolean isLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
/**
* 获取某年某月的天数
*/
public static int getDaysInMonth(int year, int month) {
switch (month) {
case 1: case 3: case 5: case 7: case 8: case 10: case 12:
return 31;
case 4: case 6: case 9: case 11:
return 30;
case 2:
return isLeapYear(year) ? 29 : 28;
default:
throw new IllegalArgumentException("无效月份");
}
}
/**
* 计算两个日期之间的天数差(不使用Java 8 API,纯算法实现)
*/
public static int daysBetween(int y1, int m1, int d1, int y2, int m2, int d2) {
// 统一转为儒略日数后相减
return julianDayNumber(y2, m2, d2) - julianDayNumber(y1, m1, d1);
}
/**
* 计算儒略日数(简化版,适用于格里高利历)
* 儒略日数是从公元前4713年1月1日中午起算的天数
*/
private static int julianDayNumber(int year, int month, int day) {
int a = (14 - month) / 12;
int y = year + 4800 - a;
int m = month + 12 * a - 3;
return day + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - 32045;
}
/**
* 计算某日期是当年的第几天
*/
public static int dayOfYear(int year, int month, int day) {
int[] daysBefore = {0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
int doy = daysBefore[month] + day;
if (month > 2 && isLeapYear(year)) doy++;
return doy;
}
/**
* 计算某年的第N天对应的月日
*/
public static int[] fromDayOfYear(int year, int dayOfYear) {
int[] monthDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (isLeapYear(year)) monthDays[1] = 29;
int month = 1;
while (dayOfYear > monthDays[month - 1]) {
dayOfYear -= monthDays[month - 1];
month++;
}
return new int[]{month, dayOfYear};
}
/**
* 打印指定年月的日历
*/
public static void printCalendar(int year, int month) {
System.out.printf("%n %d年 %d月%n", year, month);
System.out.println("日 一 二 三 四 五 六");
int days = getDaysInMonth(year, month);
// 计算1号是星期几(蔡勒公式)
int firstWeekday = 0;
int y = year, m = month;
if (m <= 2) { m += 12; y -= 1; }
int k = y % 100, j = y / 100;
firstWeekday = (1 + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7;
if (firstWeekday < 0) firstWeekday += 7;
// 打印前导空格
for (int i = 0; i < firstWeekday; i++) {
System.out.print(" ");
}
for (int d = 1; d <= days; d++) {
System.out.printf("%2d ", d);
if ((firstWeekday + d) % 7 == 0) System.out.println();
}
System.out.println();
}
/**
* 计算复活节日期(适用于格里高利历)
* 使用匿名算法(Gauss算法变体)
*/
public static int[] getEasterDate(int year) {
int a = year % 19;
int b = year / 100;
int c = year % 100;
int d = b / 4;
int e = b % 4;
int f = (b + 8) / 25;
int g = (b - f + 1) / 3;
int h = (19 * a + b - d - g + 15) % 30;
int i = c / 4;
int k = c % 4;
int l = (32 + 2 * e + 2 * i - h - k) % 7;
int m = (a + 11 * h + 22 * l) / 451;
int month = (h + l - 7 * m + 114) / 31;
int day = ((h + l - 7 * m + 114) % 31) + 1;
return new int[]{month, day};
}
// ========== 测试入口 ==========
public static void main(String[] args) {
System.out.println("=== 蔡勒公式测试 ===");
int testYear = 2024, testMonth = 10, testDay = 19;
String weekday = getWeekdayByZeller(testYear, testMonth, testDay);
System.out.printf("%d-%02d-%02d 是 %s%n", testYear, testMonth, testDay, weekday);
// 验证:Java 8 LocalDate
LocalDate ld = LocalDate.of(testYear, testMonth, testDay);
System.out.println("LocalDate验证: " + ld.getDayOfWeek());
System.out.println("\n=== 闰年测试 ===");
int[] years = {1900, 2000, 2024, 2100};
for (int y : years) {
System.out.printf("%d年%s闰年%n", y, isLeapYear(y) ? "是" : "不是");
}
System.out.println("\n=== 日期间隔计算 ===");
int days = daysBetween(2024, 1, 1, 2024, 10, 19);
System.out.println("2024-01-01 到 2024-10-19 间隔: " + days + " 天");
System.out.println("\n=== 2024年10月日历 ===");
printCalendar(2024, 10);
System.out.println("\n=== 复活节日期 ===");
int[] easter = getEasterDate(2024);
System.out.printf("2024年复活节: %d月%d日%n", easter[0], easter[1]);
}
}
算法解析
蔡勒公式推导
公式核心基于模7运算。将年月日映射到一个整数,该整数 mod 7 即为星期几。各项含义:
– q:月份内的日期偏移
– ⌊13(m+1)/5⌋:月份贡献(每月约增加2.6天)
– K + ⌊K/4⌋:年份贡献(平年365≡1 mod 7,闰年多1天)
– ⌊J/4⌋ - 2J:世纪修正(格里高利历每400年一循环)
儒略日数
儒略日数是天文历法中的连续日计数系统。将日期转为儒略日数后,任意两日期间隔即为简单减法。
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 蔡勒公式求星期 | O(1) | O(1) |
| 日期间隔 | O(1) | O(1) |
| 年积日转换 | O(1) | O(1) |
| 打印月历 | O(days) | O(1) |
总结
日期算法虽然基础,但涉及历法规则的边缘情况(如闰年、历法切换)。蔡勒公式和儒略日数是处理日期问题的两把利器,掌握它们可以避免对第三方库的过度依赖。