天文算法--儒略日

儒略日与历日的相互推算。由天数推算历日,推算星期几,计算年内的序数日与历日的相互推算。均基于儒略日。代码不是很完善,后续可能会重新修改。仅供参考。

package cn.ancony.chinese_calendar;

/**
 * 将儒略历或格里高利历中的日期转换成相应的儒略日数(JD),以及反向转换。
 * 儒略日数(简称儒略日),是指从公元-4712年开始连续计算日数得出的天数及不满一日的小数,通常记为 JD (**)。
 * 传统上儒略日的计数是从格林尼治平午,即世界时 12 点开始的。若以力学时(或历书时)为标尺,这种计数通常表达
 * 为“儒略历书日”,即 JDE (**),其中 E 只是一种表征,即按每天 86400 个标准秒长严格地计日。
 * 1582 年 10 月 4 日(儒略历)的下一日为 1582 年 10 月 15 日(格里高利历)。
 * 格里高利历的启用时间并不相同,如:大不列颠公元 1752年才启用,而土耳其则要等到 1927 年。
 * 儒略历由尤里乌斯·恺撒于公元前 45 年在罗马帝国创立,
 * 而其最终形式确立于公元 8 年前后,尽管如此,我们仍可以借助天文学家的演算无止境地向前推算儒略历。
 * 对于公元 1 年之前的年份如何计数,天文学家同历史学并不一致。在本书中,“公元前”的年份以天文方法计数。
 * 这样,+1 年的前一年为 0 年,再之前才是-1 年。所以历史学家所说的公元前 585 年实际上是-584 年。
 * 历史学家的公元前的年份表示对应jdk8的IsoChronology中的proleptic-year,
 * 天文学家的公元前的年份表示对应jdk8的IsoChronology中的year-of-era。
 * <p>
 * java8的time包也可以计算儒略日,但是是基于整数的。
 */
public class JulianDay {

    private JulianDay() {
    }

    private static final YearMonthDay _1582_10_15 = YearMonthDay.of(1582, 10, 15);

    //计算两个日期的相隔天数
    public static double intervalDays(YearMonthDay from, YearMonthDay to) {
        return convert(to) - convert(from);
    }

    //已知一个日期,求相隔days天后的日期
    public static YearMonthDay intervalDays(YearMonthDay from, int days) {
        return convert(convert(from) + days);
    }

    //已知一个日期,求星期几
    //儒略历到格里高利历的换算并不影响星期.1582-11-4(星期四)的下一天是1582-11-15(星期五)
    //结果:周日为0,周一为1,周二为2, ...
    public static int weekday(YearMonthDay ymd) {
        return (int) (Math.round(convert(ymd) + 1.5) % 7);
    }

    /**
     * 是否闰年。
     * 儒略历中,能被4整除的年份为闰年。
     * 格里高利历中,除了遵循儒略历中的规则,还增加了规则:
     * 不能被400整除的百年为平年 ,能被400整除的百年则为闰年
     *
     * @return 是否闰年
     */
    public static boolean leapYear(int year) {
        boolean julian = year % 4 == 0;
        return year > 1582 ? (julian && year % 100 != 0) || year % 400 == 0 : julian;
    }

    public static int dayOfYear(YearMonthDay ymd) {
        int k = leapYear(ymd.getYear()) ? 1 : 2;
        return (int) ((275 * ymd.getMonth() / 9) - k * ((ymd.getMonth() + 9) / 12) + ymd.getDay() - 30);
    }

    //公式由比利时民间天文协会的 A. Pouplier 发现。
    public static YearMonthDay yearMonthDay(int year, int dayOfYear) {
        int k = leapYear(year) ? 1 : 2;
        int month = (int) (9 * (k + dayOfYear) / (double) 275 + 0.98);
        if (dayOfYear < 32) month = 1;
        int day = dayOfYear - (275 * month / 9) + k * ((month + 9) / 12) + 30;
        return YearMonthDay.of(year, month, day);
    }

    /**
     * 将儒略日转换为年月日
     *
     * @param julian 儒略日
     * @return 年月日(带小数)
     */
    public static YearMonthDay convert(double julian) {
        double jd = julian + 0.5;
        int z = (int) jd;
        double f = jd - z;
        int a = z;
        if (z >= 2299161) {
            int t = (int) ((z - 1867216.25) / 36524.25);
            a = z + 1 + t - (int) ((double) t / 4);
        }
        int b = a + 1524, c = (int) ((b - 122.1) / 365.25),
                d = (int) (365.25 * c), e = (int) ((b - d) / 30.6001);
        double day = b - d - (int) (30.6001 * e) + f;
        int month = e - (e < 14 ? 1 : 13),//e的其他情况: e == 14 || e == 15
                year = c - (month > 2 ? 4716 : 4715);//month的其他情况: month == 1 || month == 2
        return YearMonthDay.of(year, month, day);
    }

    /**
     * 将年月日转换为儒略日。
     *
     * @param ymd 1582_10_15之后为格里高利历,之前为儒略历
     * @return 儒略日
     */
    public static double convert(YearMonthDay ymd) {
        int year = ymd.getYear(), month = ymd.getMonth();
        double day = ymd.getDay();
        //若month>2,year和month不变,若month=1或2,以year–1代year,以month+12代month,
        //换句话说,如果日期在1月或2月,则被看作是在前一年的13月或14月。
        if (month < 3) {
            year -= 1;
            month += 12;
        }
        int b = 0;
        //如果是格里高利历,b需要进行转换
        if (ymd.compareTo(_1582_10_15) > 0) {
            int a = year / 100;
            b = 2 - a + a / 4;
        }
        return (int) (365.25 * (year + 4716)) + (int) (30.6001 * (month + 1)) + day + b - 1524.5;
    }

}

package cn.ancony.chinese_calendar;

import lombok.Getter;

import java.util.Objects;

/**
 * 表示年月日的类,其中日可以使用小数表示。
 */
@Getter
public class YearMonthDay implements Comparable<YearMonthDay> {
    private final int year, month, date, hour, minute, second;
    private final double day;

    private YearMonthDay(int year, int month, double day, int date, int hour, int minute, int second) {
        this.year = year;
        this.month = month;
        this.day = day;
        this.date = date;
        this.hour = hour;
        this.minute = minute;
        this.second = second;
    }

    private static final LevelConvert lc = LevelConvert.of("天时分秒", 24, 60, 60);

    public static YearMonthDay of(int year, int month, double day) {
        int[] a = lc.fromHighest(day);
        return new YearMonthDay(year, month, day, a[0], a[1], a[2], a[3]);
    }

    public String toString() {
        return String.format("%d,%d,%f", year, month, day);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        YearMonthDay that = (YearMonthDay) o;
        return year == that.year && month == that.month && date == that.date
                && hour == that.hour && minute == that.minute && second == that.second;
    }

    @Override
    public int hashCode() {
        return Objects.hash(year, month, date, hour, minute, second);
    }

    //H: human
    public String toHString() {
        return String.format("%d,%d,%d,%d,%d,%d", year, month, date, hour, minute, second);
    }

    public static YearMonthDay of(int year, int month, int day, int hour, int minute, int second) {
        return of(year, month, lc.toHighest(day, hour, minute, second));
    }

    @Override
    public int compareTo(YearMonthDay o) {
        if (year > o.year) return 1;
        else if (year < o.year) return -1;
        else {
            if (month > o.month) return 1;
            else if (month < o.month) return -1;
            else return Double.compare(day, o.day);
        }
    }
}

package cn.ancony.chinese_calendar;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 基于整数的均匀多层转换系统。比如可以将"整秒数"转换为多少小时多少分钟多少秒。
 * 要求:层与层之间必须是固定的进制。比如从月转换到天就不行。因为一个月可以有28天,也可以有29天,30天,31天。
 * 时分秒可以。因为一小时是固定的60分钟,一分钟是固定的60秒。
 * 每一层的进制,使用参数intervals表达。
 * <p>
 * 这里暂时使用两个概念:"复合量"和"单一量"。
 * 复合量(composite amount):由不同单位组成的数。比如:10h29m55s; 23°26′44"; 先令,便士; 码,英尺,英寸; a+bi ...
 * 单一量(single amount):只由一个单位组成的数。比如:1.1h; 70s; 23.6° ...
 * 所以,本类也可以称作是基于整数的单一量与复合量相互转换的类。
 * <p>
 * 在同一个均匀表示系统中,高一级的单位,代码里面使用high表示,低一级的单位使用low表示。
 * 比如,对于"时分秒"这个三级系统,"时"是最高级的单位,"秒"是最低级的单位。
 * 当只说"分"和"秒"的时候,"分"是比"秒"高的单位。而"秒"是比"分"低的单位。
 * 为了方便程序实现,如果传入复合量,则复合量的最低一级是不支持小数的。
 * 比如在"时分秒"的实例中进行转换时,输入的时分秒中的秒是不能带小数的。
 * 此时,为了计算这类需求,可以先将秒的小数部分提取出来,将时分秒传入系统并转为秒,然后再加上之前取出来的小数部分即可。
 * <p>
 * 系统定义使用以of开头的方法,将复合量转为单一量使用以to开头的方法,将单一量转为复合量使用以from结尾的方法。
 */
public class LevelConvert {
    private final List<Integer> intervals = new ArrayList<>();

    private final String name;
    private final long product;

    private LevelConvert(String name, int... intervals) {
        this.name = name;
        long p = 1L;
        for (int i : intervals) {
            this.intervals.add(i);
            p *= i;
        }
        product = p;
    }

    /**
     * 定义转换系统。
     * 例如,"时分秒"系统,需要传入两个参数,即60,60,表示从时转换到分是60进制,从分转换到秒是60进制。
     * 例如,"天时分秒"系统,需要传入三个参数,即24,60,60,表示从天转换到小时是24进制,从时转换到分是60进制,从分转换到秒是60进制。
     *
     * @param name      转换系统的名字。比如,"时分秒","天时分秒"...
     * @param intervals 系统层级间的进制。比如,"天时分秒"系统传入24,60,60.
     * @return 一个转换系统实例
     */
    public static LevelConvert of(String name, int... intervals) {
        return new LevelConvert(name, intervals);
    }

    //将单一量转为复合量,单一量以最高级单位计。
    //比如,"天时分秒"系统,传入4.81(表示4.81天),返回[4, 19, 26, 24](表示4天19时26分24秒)
    public int[] fromHighest(double highestAmount) {
        return fromLowest(Math.round(highestAmount * product));
    }

    //将单一量转为复合量,单一量以最低级单位计。
    //
    public int[] fromLowest(long lowestAmount) {
        int[] ans = new int[intervals.size() + 1];
        fromLowest(lowestAmount, ans, product, 0);
        return ans;
    }

    //将复合量转为单一量,以最低级单位计。
    //比如,"天时分秒"系统,传入[4, 19, 26, 24](表示4天19时26分24秒),返回415584(表示415584秒)
    public long toLowest(int... compositeAmount) {
        return toLowestExcludeHighest(compositeAmount) + compositeAmount[0] * product;
    }

    //将复合量转为单一量,以最高级单位计。
    //比如,"天时分秒"系统,传入[4, 19, 26, 24](表示4天19时26分24秒),返回4.81(表示4.81天)
    public double toHighest(int... compositeAmount) {
        //Java中double的精度问题: 由于double类型是用二进制来表示十进制小数的,而有些十进制小数无法用有限长度的二进制小数精确表示,例如0.1。
        //这样就会导致在进行浮点数运算时出现误差,例如:0.05 + 0.01 = 0.060000000000000005。0.81 + 4 = 4.8100000000000005
        //这里使用BigDecimal做一下转换
        return BigDecimal.valueOf((double) toLowestExcludeHighest(compositeAmount) / product)
                .add(BigDecimal.valueOf(compositeAmount[0])).doubleValue();
    }

    //计算除去最高位的数值后的以最低单位表示的值
    private long toLowestExcludeHighest(int... compositeAmount) {
        int len = compositeAmount.length, sz = intervals.size();
        if (sz == 0) return 0;//如果未定义间隔,直接返回0
        if (len != sz + 1) throw new RuntimeException("请正确定义间隔并传入正确的参数");
        long sum = compositeAmount[len - 1], temp = 1;//初始化为倒数第一位的值。
        for (int i = len - 2; i > 0; i--) { //从倒数第二位开始,一直到正数第二位
            temp *= intervals.get(i);
            sum += compositeAmount[i] * temp;
        }
        return sum;
    }

    private void fromLowest(long amount, int[] ans, long cur, int idx) {
        int len = intervals.size();
        if (idx == len) {
            ans[idx] = (int) amount;
            return;
        }
        if (amount > cur) {
            ans[idx] = (int) (amount / cur);
            fromLowest((int) amount % cur, ans, cur / intervals.get(idx), idx + 1);
        }
    }

    public String toString() {
        return name + "[" + intervals.stream().map(String::valueOf).collect(Collectors.joining(",")) + "]";
    }
}

以下是测试类:

package cn.ancony.javafx.chinese_calendar;

import cn.ancony.chinese_calendar.JulianDay;
import cn.ancony.chinese_calendar.YearMonthDay;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class TestJulianDay {

    @ParameterizedTest
    @CsvSource({
            "2451545.0,   2000,   1,   1.5",
            "2446822.5,   1987,   1,    27",
            "2446966.0,   1987,   6,  19.5",
            "2447177.5,   1988,   1,  17.0",
            "2447332.0,   1988,   6,  19.5",
            "2415020.5,   1900,   1,     1",
            "2305447.5,   1600,   1,     1",
            "2305812.5,   1600,  12,    31",
            "2026871.8,    837,   4,  10.3",
            "1356001.0,  -1000,   7,  12.5",
            "1355866.5,  -1000,   2,    29",
            "1355671.4,  -1001,   8,  17.9",
            "      0.0,  -4712,   1,   1.5"})
    public void testToJulian(double expected, int year, int month, double day) {
        assertEquals(expected, JulianDay.convert(YearMonthDay.of(year, month, day)));
    }

    @ParameterizedTest
    @CsvSource({
            "1957, 10,  4.81,  2436116.31",
            " 333,  1,  27.5,   1842713.0",
            "-584,  5, 28.63,  1507900.13"})
    public void testToYmd(int year, int month, double day, double julian) {
        assertEquals(YearMonthDay.of(year, month, day), JulianDay.convert(julian));
    }

    @ParameterizedTest
    @CsvSource({"27689.0, 1910, 4, 20, 1986, 2, 9"})
    public void testIntervalDays(double expect, int y1, int m1, int d1, int y2, int m2, int d2) {
        assertEquals(expect, JulianDay.intervalDays(YearMonthDay.of(y1, m1, d1), YearMonthDay.of(y2, m2, d2)));
    }

    @ParameterizedTest
    @CsvSource({"2018, 11, 26, 1991, 7, 11, 10000"})
    public void testIntervalDays2(int ey, int em, int ed, int y, int m, int d, int days) {
        assertEquals(YearMonthDay.of(ey, em, ed), JulianDay.intervalDays(YearMonthDay.of(y, m, d), days));
    }

    @ParameterizedTest
    @CsvSource({"3, 1954, 6, 30"})
    public void testWeekday(int weekday, int year, int month, int day) {
        assertEquals(weekday, JulianDay.weekday(YearMonthDay.of(year, month, day)));
    }

    @ParameterizedTest
    @CsvSource({
            " true,   900",
            " true,  1236",
            " true,  1988",
            "false,   750",
            "false,  1429",
            "false,  1700",
            "false,  1800",
            "false,  1900",
            "false,  2100",
            " true,  1600",
            " true,  2000",
            " true,  2400"})
    public void testLeapYear(boolean expect, int year) {
        assertEquals(expect, JulianDay.leapYear(year));
    }

    @ParameterizedTest
    @CsvSource({
            "318, 1978, 11, 14",
            "113, 1988,  4, 22"})
    public void testDayOfYear(int expect, int year, int month, int day) {
        assertEquals(expect, JulianDay.dayOfYear(YearMonthDay.of(year, month, day)));
    }

    @ParameterizedTest
    @CsvSource({"1988, 4, 22, 113"})
    public void testYearMonthDay(int year, int month, int day, int dayOfYear) {
        assertEquals(YearMonthDay.of(year, month, day), JulianDay.yearMonthDay(year, dayOfYear));
    }

}
package cn.ancony.javafx.chinese_calendar;

import cn.ancony.chinese_calendar.LevelConvert;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class TestLevelConvert {
    @ParameterizedTest
    @CsvSource({"4, 19, 26, 24, 4.81"})
    public void testDayHourMinuteSecond(int day, int hour, int minute, int second, double date) {
        LevelConvert lc = LevelConvert.of("天时分秒", 24, 60, 60);
        assertArrayEquals(new int[]{day, hour, minute, second}, lc.fromHighest(date));
        assertEquals(date, lc.toHighest(day, hour, minute, second));
    }

    @ParameterizedTest
    @CsvSource({
            "5, 6, 0,   5.1",
            "5, 7, 48, 5.13"})
    public void testDegreeMinuteSecond(int degree, int minute, int second, double degrees) {
        LevelConvert lc = LevelConvert.of("度分秒", 60, 60);
        assertArrayEquals(new int[]{degree, minute, second}, lc.fromHighest(degrees));
        assertEquals(degrees, lc.toHighest(degree, minute, second));
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值