CelestialCalendar Documentation
On this page

@0xf3cd/celestial

来自 CelestialCalendar 的天文计算与公历/阴历转换, 以自带 WebAssembly 模块的 ESM 包提供。

English

本文描述当前源码,含尚未发布的改动。以下安装命令取得 npm 上的最新已发布版本,请使用与所装版本匹配的文档。 日志接口移除、太阳距离字段改名和错误类别变化见迁移指南(英文)。

安装

在应用项目目录中从 npm 安装:

npm install @0xf3cd/celestial

正式发布时,对应的 GitHub Release 还提供 celestial-wasm.zip,其中包含与 npm 发布内容逐字节相同的 tarball 及其 SHA-256 校验文件。

支持 Node 22 或更新版本;浏览器端在 Chrome 和 Firefox 上测试。导入包不会进行 I/O; 使用同步计算 API 前,先调用一次 init() 并等待完成。

以下每个 JavaScript 代码块均可独立作为应用项目中的 example.mjs,从该目录用 node example.mjs 运行。

示例

固定时刻的星盘快照

import { init, chart } from "@0xf3cd/celestial";

await init();
const snapshot = chart.snapshot(
  { year: 2026, month: 1, day: 1, fraction: 0.5 },
  {
    scale: "utc",
    location: { latitudeDeg: 51.5, longitudeDeg: 0 },
    system: "placidus",
    model: "default",
  },
);
console.log(snapshot.times); // { jdUt1, jdeTt }
console.log(snapshot.bodies[2]); // Mercury: position, distance and longitude rate
console.log(snapshot.houses.cuspsDeg); // twelve house cusps in house order

civil 使用公历日期与 [0, 1) 的日小数,不自动转换 JavaScript Date。scale 必填, 可选 "utc"、"ut1";1972-01-01 之前的输入必须显式选择 scale: "ut1"。民用年份限 1885–2099。 纬度以北为正,单位为度,范围 (-90, 90);经度以东为正,范围 [-180, 180]。 system 必填,可选 "equal"、"wholeSign"、"placidus"。 model 可省略,或设为自身属性 undefined、"default",均选 Algo5;也可显式选 "algo1" 至 "algo5"。使用的输入字段须为对象自身属性;继承来的 model 会报错,不会静默换成默认模型。 options 只接受 scale、location、system、model 四个键;拼错或多余的键抛 TypeError。

结果包含 times: { jdUt1, jdeTt },以及固定顺序的十四个 bodies,其 target 为 sun、moon、 mercury、venus、mars、jupiter、saturn、uranus、neptune、pluto、meanAscending、 meanDescending、trueAscending、trueDescending。每个成员包含 longitudeDeg、latitudeDeg、 distanceAu 和 longitudeRateDegPerTtDay。所有成员均以日期真黄道/真春分点为基准:太阳、月球与行星 提供视地心坐标;交点提供平均/真交点黄经,纬度为零、distanceAu 为 null。 所有距离都用 AU,月球也一样;独立的 moon.apparentGeocentricCoordinate() 则以千米给出月地距离。 longitudeRateDegPerTtDay 是瞬时黄经速率,单位为度/TT 日;负值表示该瞬间黄经在减小(逆行)。 houses 包含 ascendantDeg、midheavenDeg、descendantDeg、imumCoeliDeg 和 cuspsDeg, 角度均在 [0, 360) 度。结果是独立的普通对象和数组,后续调用或 WASM 内存增长不改变既有结果。

核心检查换算后 JDE(TT) 的星盘范围 [2409543.875, 2488069.125);Algo4 还要求派生 JDE < 2464328.5,在反向换算前检查。这会有意排除 Algo4 下 2034 年末约 71.5 个 UT1 秒; 共用民用/模型错误可能先于星盘联合窗口错误出现。由 UTC 换算出的 UT1 是模型值,不是观测 DUT1; 未来 UTC 沿用闰秒表最后已知的偏移。见 Time scales。 Ascendant 在舍入尺度上的退化方向、以及 Placidus 的极区情形会失败。宫位遵循宫位操作的章动默认模型,当前为 IAU 1980; 天体位置修正仍固定为 IAU 1980。类型/记录结构错误抛 TypeError;无效的日期、日小数、地点、 选项字符串或原生域拒绝抛 RangeError;未初始化、内存分配失败、原生计算失败或原生成功记录畸形抛 CelestialError,其 operation 为 "chart.snapshot"。原生侧报告已知错误种类时,recorded 为 true,即使消息为空也一样。失败不返回半张 snapshot。

今天的阴历日期

用 /date 取当前时刻在固定 UTC+8 下的公历日期,再按所选算法转换为阴历;这里的“今天”不取决于主机时区。 只取年月日构造公历日期,不要把含有 fraction 的整个民用时间对象传给 lunar.fromGregorian()。

import * as celestial from "@0xf3cd/celestial";
import { dateToCivilAtOffset } from "@0xf3cd/celestial/date";

await celestial.init();
const { year, month, day } = dateToCivilAtOffset(new Date(), 480);
const gregorian = { year, month, day };
const lunar = celestial.lunar.fromGregorian("algo3", gregorian);
console.log(gregorian, lunar);

日期桥接不把时钟 UTC 转成 UT1 或 TT;阴历日期基准与算法年域见阴历算法。

UTC 与 JDE(TT) 转换

1.1.0 起提供 time.utcToJde() 和 time.jdeToUtc()。1972 年前按 UT1 处理 UTC 字段,使用固定的 ΔT 模型 Algo5; 星盘查询仍拒绝 1972 年前的 UTC。闰秒边界不能保证可逆;超过闰秒表最后一项的日期保留该项的 TT−UTC 偏移。

import { init, time } from "@0xf3cd/celestial";

await init();
const utc = { year: 2026, month: 10, day: 28, fraction: (19 * 3600 + 35 * 60 + 37) / 86400 };
const jdeTt = time.utcToJde(utc);
console.log(time.jdeToUtc(jdeTt));

fraction 是日小数(0.5 表示正午)。JDE 用浮点数计日,转换回来的民用时刻可能与输入有微小差异。

从 UTC 输入查询下一个节气、再显示为东八区时间,见 附 Python 和 JavaScript 可运行程序的指南。 该指南也列出了正反向各自可接受的输入范围与不可逆边界。

下一个节气,还剩多少天

起点是明确给定的 2026-12-31 00:00 UT1,不是把 Date 时间戳当作 UT1。

import * as celestial from "@0xf3cd/celestial";

await celestial.init();
const cutoffUt1 = { year: 2026, month: 12, day: 31, fraction: 0 };
const cutoffJd = celestial.time.ut1ToJd(cutoffUt1);
const upcoming = [2026, 2027]
  .flatMap((year) => Object.values(celestial.Jieqi).map((jieqi) => {
    const event = celestial.jieqi.moment(year, jieqi);
    return { ...event, jdUt1: celestial.time.ut1ToJd(event.momentUt1) };
  }))
  .filter((event) => event.jdUt1 > cutoffJd)
  .sort((a, b) => a.jdUt1 - b.jdUt1)[0];
if (upcoming === undefined) throw new Error("No later Jieqi in the queried years.");
console.log(celestial.jieqi.name(upcoming.jieqi));
console.log("UT1:", upcoming.momentUt1);
console.log("Remaining UT1 days:", upcoming.jdUt1 - cutoffJd);

这里遍历全部枚举值,按时刻而非编号选取下一项,并保留剩余天数的小数部分。 momentUt1 是 UT1,不是 UTC 或东八区时间。此例只演示这次跨年查询,不是覆盖整个 API 年域的通用搜索; jieqi.moment() 的查询年域与失败条件见 API 契约。

月面照明与节气时刻

2448724.5 是 TT 下的 JDE。节气结果的 momentUt1 则是 UT1 民用时刻,不是 UTC+8 显示时间。

import * as celestial from "@0xf3cd/celestial";

await celestial.init();

const moon = celestial.moon.illumination(2448724.5);
const lichun = celestial.jieqi.moment(2026, celestial.Jieqi.LICHUN);
console.log(moon.fraction, lichun.momentUt1, celestial.jieqi.name(lichun.jieqi));

公历与阴历往返

import * as celestial from "@0xf3cd/celestial";

await celestial.init();
const result = celestial.lunar.fromGregorian("algo3", { year: 2026, month: 8, day: 15 });
console.log(result);
console.log(celestial.lunar.toGregorian("algo3", result));

初始化与浏览器部署

并发和重复的 init() 调用共享同一个 promise,完成值为 undefined。加载失败时,promise 以原始加载器错误 拒绝;之后可显式再次调用以重试。初始化完成前调用命名空间方法,会在参数校验之前抛出 CelestialError, 其 operation 为公开方法名,recorded === false;即使请求数量为零也一样。

浏览器部署时,确保包的 .wasm 资源在构建产物指定的 URL 上可访问,并将静态服务器的响应类型设为 Content-Type: application/wasm。

API 契约

根入口提供 time、sun、moon、jieqi、lunar、chart 命名空间,并随包提供 TypeScript 声明。 包只提供 jieqi / lunar 这一套正式命名,不另设 solarTerms / lunarCalendar 别名。 模型和月相选项使用字符串联合类型,如 "full"、"algo3"。不再提供日志配置或 init 配置参数。 独立位置、赤道位置、宫位、事件、能力与模型文本查询均不是公开 JavaScript API。 Jieqi 是冻结对象,包含 24 个具名常量,从 Jieqi.LICHUN = 0 到 Jieqi.DAHAN = 23,拼写与 Python 包一致。 TypeScript 类型 Jieqi 是相应的 0 | 1 | ... | 23 联合;一般的 number 变量须先收窄类型, 才能传给 jieqi.moment() 或 jieqi.name()。jieqi.name() 返回中文名,如 Jieqi.LICHUN 对应 "立春"。

时间尺度与单位须明确区分:

  • JD 输入和输出的操作名区分 UT1 与 JDE(TT)。
  • jieqi.moment() 返回 { jieqi, momentUt1 },TT 到 UT1 固定使用 Algo5,不随可选模型的默认值改变。 其中的民用时刻是 UT1,不是 UTC 或东八区钟表时间; 同一瞬间显示为 UTC+8 时,日期可能改变。显示为 UTC 或固定偏移时间前,须先确定时标转换。 返回的 UT1 年份可能与请求年份不同。
  • sun.apparentSolarTime() 接受 UTC 民用时刻,以及 [-180, 180] 度范围内、东正西负的地理经度。
  • 角度结果以度为单位。日地距离字段为 distanceAu,单位为 AU。moon.apparentGeocentricCoordinate() 的月地距离用千米; chart.snapshot() 里所有天体(含月球)的距离都用 AU。
  • time.deltaT() 返回秒数。
  • 均时差以时角度数表示,乘以 240 可换算为时间秒数。
  • sun.apparentGeocentricCoordinate(jde) 和 moon.apparentGeocentricCoordinate(jde) 各自返回给定 TT 下 JDE 的一个坐标记录。

jieqi.moment(year, index) 接受 [401, 32766] 内的公历年份及 [0, 23] 内的节气索引。 即使输入在范围内,原生计算无法得到唯一时刻时仍会抛出 CelestialError。 sun.longitudeCrossings(year, longitudeDeg)、moon.phaseMoments(year, phase) 和 moon.newMoonsInYear(year) 接受 [1, 32766] 内的公历年份。

time.localApparentSiderealTime(jdUt1, longitudeDeg) 接受有限的 UT1 下 JD,其公历年份须在 [401, 32766] 内; 地理经度须有限、东正西负,范围为 [-180, 180] 度。返回值为 [0, 360) 度。 年份范围由原生边界校验,超出年域的 JD 与 JavaScript 有限性/经度校验一样抛出 RangeError。 此操作的 ΔT 固定为 Algo5,章动固定为 IAU 1980。

sun.longitudeCrossings(year, longitudeDeg) 接受有限的太阳视地心黄经,范围为 [0, 360) 度; 返回 TT 下的 JDE 数组,当年没有经过该黄经的时刻时返回 []。

初始化完成后,错误的结构和类型抛出 TypeError;无效输入或声明域之外的输入,无论在哪一层发现,都抛出 RangeError。 计算/资源/内部失败或原生成功记录畸形抛出 CelestialError,其 operation 为公开方法名, 原生侧报告已知错误种类时,recorded 为 true,即使消息为空也一样;绑定本地发现的失败为 false。 合法的“无结果”仍以 null 或 [] 表示。

有限的位置输入仍可能计算失败;坐标合法不保证远年代的物理合理性或精度,不设新的统一位置年域或黄赤交角阈值。 绑定对调用方持有的 WASM 输出缓冲区设有 16 MiB 上限。

moon.newMoonsAfter(jde, count) 接受 [0, 4096] 内的 count,零返回 []。 上限使 WASM 输出缓冲区不超过 32 KiB。

日期记录

以下三种输入在 TypeScript 与运行时均互斥:

类型 必需的自有字段 排除字段
GregorianDate year, month, day fraction, isLeap
CivilDateTime year, month, day, fraction isLeap
LunarDate year, month, day, isLeap fraction

民用时刻与公历日期输入须使用整数年份和有效公历月、日。原生操作各自决定可接受的年份窗口; /date 桥接要求民用年份在 [1, 32767] 内。阴历年份和日期须在所选算法中存在。 fraction 是 [0, 1) 内的有限日小数。运行时允许无关的额外属性,但排除字段即使来自继承或值为 undefined 也会被拒绝。启用 TypeScript 的 exactOptionalPropertyTypes,也可在类型检查时拒绝可选 never 字段上的 显式 undefined。

原生 GregorianDate 和 CivilDateTimeResult 结果使用 [-32767, 32767] 内的天文纪年,0 年表示公元前 1 年。 CivilDateTimeResult 在 CivilDateTime 上增加整数 hour、minute 及可带小数的 second。 time.jdeToUt1()、sun.apparentSolarTime() 和 JieqiMoment.momentUt1 使用这种输出。 这些字段从日小数推导,不舍入到毫秒;second 的单位是秒,不是毫秒计数。 结果符合所调用操作的输入窗口时,仍可作为民用时刻输入。函数读取 fraction,而非派生的时分秒字段。 时间尺度由操作命名区分,不编码在这个共用记录类型中。

记录输入按结构识别:显式自有字段与排除标签决定种类,而非构造函数;不会读取 Date 方法或时间戳。 普通 Date 缺少必需字段,须用下文的日期桥接将时间戳转换为民用字段。 民用时刻结果或阴历日期不能直接传给 lunar.fromGregorian();先选定日期基准,再显式构造 GregorianDate。

固定偏移的 Date 桥接

@0xf3cd/celestial/date 是无运行时依赖的纯历法桥接,不导入根入口、不加载 WASM,无需调用 init()。 偏移量以分钟为单位,向东为正;480 表示固定 UTC+8。

import { civilAtOffsetToDate, dateToCivilAtOffset } from "@0xf3cd/celestial/date";

const date = new Date("2024-02-09T16:00:00.123Z");
const eastEight = dateToCivilAtOffset(date, 480);
const restored = civilAtOffsetToDate(eastEight, 480);
console.log(eastEight);
console.log(restored.toISOString(), restored.getTime() === date.getTime());

这里的 2024-02-09T16:00:00.123Z 在 UTC+8 下是民用日期 2024-02-10、00:00:00.123。 同一偏移下往返保留 Date 的毫秒时间戳,所以最后一行以 true 结尾。

函数 输入 输出
dateToCivilUtc(date) Date UTC 下的 CivilDateTimeResult
civilUtcToDate(civil) UTC 下的 CivilDateTime Date
dateToCivilAtOffset(date, offsetMinutesEast) Date、固定偏移 该偏移下的 CivilDateTimeResult
civilAtOffsetToDate(civil, offsetMinutesEast) 固定偏移下的 CivilDateTime Date

offsetMinutesEast 是 [-1439, 1439] 内的安全整数,以分钟为单位,UTC 以东为正。 转换使用纪元时间运算与 UTC 字段,不使用主机本地时区、Intl、区域格式解析、IANA 时区或夏令时规则。

Date 转民用时刻保留主机值的毫秒分辨率,带小数的 second 不含额外的亚毫秒信息。 民用时刻转 Date 时,将非负的本地日内时间舍入到最近的毫秒,恰好半毫秒时向较晚的民用时刻舍入,并显式处理跨日进位。 对任何可表示的 Date,只要所选偏移下的民用年份在 [1, 32767] 内,使用同一偏移的 Date -> civil -> Date 往返就精确保留 getTime()。

年份边界约束本地民用记录,不一定约束承载它的 UTC 值。输入与舍入进位后的本地年份都须在 [1, 32767] 内; 将本地 32767 年末舍入到 32768 年会被拒绝。在偏移边缘,返回的 UTC Date 年份可能为 0 或 32768, 仍可使用同一偏移转回。仅处理 UTC 的函数将年域约束施加于 UTC 民用字段。

错误类型、缺少民用字段、混合日期种类,以及非整数的日期或偏移字段,均抛出 TypeError。 无效 Date、无效公历日期、非有限数或非安全数、越界的日小数或偏移,以及超出年域的本地年份,均抛出 RangeError。

此桥接不转换时间尺度,也不处理闰秒;既不提供 Date 到 JDE 的转换,也不提供 UTC 到 UT1 的转换。 不要把 momentUt1 当成 UTC 传给 civilUtcToDate():UT1 不是 UTC,本库也不建模观测 DUT1。 两者可能相差多少,见 Time scales。

阴历算法

用 "algo1"、"algo2" 或 "algo3" 选择算法。公历输入与输出是日期标签,不是瞬间。 各算法的来源、阴历年域、选用场景与日期基准见阴历算法(英文)。

lunar.supportedYearRange(algorithm) 查询原生算法包含两端的阴历年范围,而非公历一月至十二月的边界。 lunar.yearInfo(algorithm, year) 返回公历 firstDay、传统 leapMonth 月号或 null, 以及按历法顺序排列的 monthLengths,闰月紧随同名普通月份。 lunar.toGregorian() 使用 [1, 12] 内的传统月号和显式 isLeap 布尔值,不使用月份的位置索引。 所选算法的阴历年份和日期由原生操作校验;JavaScript 包不另存范围表或缓存。

四个阴历方法都要求先 await init();初始化完成前调用,会在参数校验之前抛出 recorded === false 的 CelestialError。初始化完成后:

  • TypeError 表示输入类型错误、缺少自有字段、带有排除的日期种类标签、日期字段非整数,或 isLeap 非布尔值。
  • RangeError 表示未知算法、非有限数或非安全数、无效公历日期、 阴历年超出原生范围,或阴历月/日超出 [1, 12] / [1, 30]。
  • RangeError 也包括原生无效输入拒绝,如公历日期标签超出所选算法覆盖范围,或阴历闰月/日组合不存在。
  • CelestialError 表示计算/资源/内部失败或原生成功记录畸形,operation 指向调用者, 如 "lunar.yearInfo",而非内部范围查询。

例如,lunar.fromGregorian("algo1", { year: 1900, month: 1, day: 1 }) 能通过公历校验, 但 algo1 无法表示该日期,因此抛出 RangeError;lunar.yearInfo("algo1", 1900) 也抛出 RangeError。

Delta T 模型

time.deltaT(year, model) 对有限的十进制公历年返回 TT − UT1 的秒数。 省略 model 或传 "default" 均选 Algo5;也可显式选 "algo1" 至 "algo5"。 各模型的来源、维护状态、硬年域与选用场景见 Delta T 模型(英文)。

许可证

许可证范围与第三方例外以英文版的 License 节为准。