每日算法 — 使用java实现日历计算:蔡勒公式与日期算法

引言

日期计算在日常开发中无处不在:计算两个日期间隔、判断某年某月某天是星期几、推算节假日等。看似简单的日期运算背后涉及闰年规则、历法变迁等复杂逻辑。本文用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)

总结

日期算法虽然基础,但涉及历法规则的边缘情况(如闰年、历法切换)。蔡勒公式和儒略日数是处理日期问题的两把利器,掌握它们可以避免对第三方库的过度依赖。