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