Files
astro/basic/julian.go
T

186 lines
5.5 KiB
Go
Raw Permalink Normal View History

2026-05-01 22:38:44 +08:00
package basic
import (
"errors"
"math"
"time"
)
var ErrInvalidCivilDate = errors.New("invalid civil date")
var timeNow = time.Now
// Date2JD 日期转儒略日
func Date2JD(date time.Time) float64 {
2026-05-01 22:38:44 +08:00
day := float64(date.Day()) + float64(date.Hour())/24.0 + float64(date.Minute())/24.0/60.0 + float64(date.Second())/24.0/3600.0 + float64(date.Nanosecond())/1000000000.0/3600.0/24.0
return JDCalc(date.Year(), int(date.Month()), day)
2026-05-01 22:38:44 +08:00
}
func ValidateCivilDate(year, month int, day float64) error {
if math.IsNaN(day) || math.IsInf(day, 0) {
return ErrInvalidCivilDate
}
if month < 1 || month > 12 {
return ErrInvalidCivilDate
}
if day < 1 {
return ErrInvalidCivilDate
}
dayInt := int(math.Floor(day))
if dayInt < 1 || dayInt > daysInCivilMonth(year, month) {
return ErrInvalidCivilDate
}
if isGregorianReformGap(year, month, day) {
return ErrInvalidCivilDate
}
return nil
}
func isGregorianReformGap(year, month int, day float64) bool {
return year == 1582 && month == 10 && day >= 5 && day < 15
}
func daysInCivilMonth(year, month int) int {
switch month {
case 1, 3, 5, 7, 8, 10, 12:
return 31
case 4, 6, 9, 11:
return 30
case 2:
if isCivilLeapYear(year, month, 1) {
return 29
}
return 28
default:
return 0
}
}
func isCivilLeapYear(year, month int, day float64) bool {
if year < 1582 || (year == 1582 && (month < 10 || (month == 10 && day <= 4))) {
return year%4 == 0
}
if year%400 == 0 {
return true
}
if year%100 == 0 {
return false
}
return year%4 == 0
}
// JDCalc 由公历年月日(day 可含小数)计算儒略日 / Julian day from a civil year, month and fractional day.
// 1582 年 10 月 15 日起按格里高利历,之前按儒略历;日期非法时返回 NaN。
// Gregorian from 1582-10-15 onward and Julian before it; an invalid civil date yields NaN.
func JDCalc(year, month int, day float64) float64 {
2026-05-01 22:38:44 +08:00
if err := ValidateCivilDate(year, month, day); err != nil {
return math.NaN()
}
effectiveYear, effectiveMonth, effectiveDay := year, month, int(math.Floor(day))
if month == 1 || month == 2 {
year--
month += 12
}
var gregorianCorrection int
if effectiveYear < 1582 || (effectiveYear == 1582 && (effectiveMonth < 10 || (effectiveMonth == 10 && effectiveDay <= 4))) {
gregorianCorrection = 0
} else {
century := int(year / 100)
gregorianCorrection = 2 - century + int(century/4)
}
return (math.Floor(365.25*(float64(year)+4716.0)) + math.Floor(30.6001*float64(month+1)) + day + float64(gregorianCorrection) - 1524.5)
}
// GetNowJD 按当前时区的日历字段取儒略日 / Julian day from the current clock's calendar fields.
// 读的是当前时区的年月日时分秒,不做时区归算。
// It reads the calendar fields in the current location without any zone conversion.
func GetNowJD() (nowJD float64) {
2026-05-01 22:38:44 +08:00
now := timeNow()
dayFraction := float64(now.Second())/3600.0/24.0 + float64(now.Minute())/60.0/24.0 + float64(now.Hour())/24.0
nowJD = JDCalc(now.Year(), int(now.Month()), float64(now.Day())+dayFraction)
2026-05-01 22:38:44 +08:00
return
}
// JD2Date 儒略日转 Local 时区的时刻 / Local-zone instant from a Julian day.
// 儒略日的日历字段按 Local 时区解释,1582 年 10 月 15 日之前按儒略历。
// The calendar fields are read in the Local zone, and dates before 1582-10-15 are read in the Julian calendar.
func JD2Date(jd float64) time.Time {
2026-05-01 22:38:44 +08:00
jd = jd + 0.5
z := float64(int(jd))
f := jd - z
var a, b, years, months, days float64
if z < 2299161.0 {
a = z
} else {
alpha := math.Floor((z - 1867216.25) / 36524.25)
a = z + 1 + alpha - math.Floor(alpha/4)
}
b = a + 1524
c := math.Floor((b - 122.1) / 365.25)
d := math.Floor(365.25 * c)
e := math.Floor((b - d) / 30.6001)
days = b - d - math.Floor(30.6001*e) + f
if e < 14 {
months = e - 1
}
if e == 14 || e == 15 {
months = e - 13
}
if months > 2 {
years = c - 4716
}
if months == 1 || months == 2 {
years = c - 4715
}
tms := (days - math.Floor(days)) * 24 * 3600
days = math.Floor(days)
tz, _ := time.LoadLocation("Local")
dates := time.Date(int(years), time.Month(int(months)), int(days), 0, 0, 0, 0, tz)
return time.Unix(dates.Unix()+int64(tms), int64((tms-math.Floor(tms))*1000000000))
}
// JD2DateByZone 儒略日转日期
2026-05-01 22:38:44 +08:00
// jd: 儒略日
// tz: 目标时区
// byZone: (true: 传入的儒略日视为目标时区当地时间的儒略日,false: 传入的儒略日视为UTC时间的儒略日)
// 回参:转换后的日期,时区始终为目标时区
func JD2DateByZone(jd float64, tz *time.Location, byZone bool) time.Time {
2026-05-01 22:38:44 +08:00
jd = jd + 0.5
z := float64(int(jd))
f := jd - z
var a, b, years, months, days float64
if z < 2299161.0 {
a = z
} else {
alpha := math.Floor((z - 1867216.25) / 36524.25)
a = z + 1 + alpha - math.Floor(alpha/4)
}
b = a + 1524
c := math.Floor((b - 122.1) / 365.25)
d := math.Floor(365.25 * c)
e := math.Floor((b - d) / 30.6001)
days = b - d - math.Floor(30.6001*e) + f
if e < 14 {
months = e - 1
}
if e == 14 || e == 15 {
months = e - 13
}
if months > 2 {
years = c - 4716
}
if months == 1 || months == 2 {
years = c - 4715
}
tms := (days - math.Floor(days)) * 24 * 3600
days = math.Floor(days)
date := time.Date(int(years), time.Month(int(months)), int(days), 0, 0, 0, 0, time.UTC).
Add(time.Duration(int64(1000000000 * tms)))
2026-05-01 22:38:44 +08:00
if !byZone {
return date.In(tz)
2026-05-01 22:38:44 +08:00
}
// 当地 JD 的小数部分是钟表读数,不能按夏令时午夜后的实际时长累加。
year, month, day := date.Date()
hour, minute, second := date.Clock()
return time.Date(year, month, day, hour, minute, second, date.Nanosecond(), tz)
2026-05-01 22:38:44 +08:00
}