@gracefullight/saju 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Date adapter interface for abstracting date/time operations.
3
+ * This allows the library to work with any date library (Luxon, date-fns, Day.js, etc.)
4
+ */
5
+ export interface DateAdapter<T = unknown> {
6
+ /**
7
+ * Get the year component
8
+ */
9
+ getYear(date: T): number;
10
+ /**
11
+ * Get the month component (1-12)
12
+ */
13
+ getMonth(date: T): number;
14
+ /**
15
+ * Get the day component
16
+ */
17
+ getDay(date: T): number;
18
+ /**
19
+ * Get the hour component (0-23)
20
+ */
21
+ getHour(date: T): number;
22
+ /**
23
+ * Get the minute component (0-59)
24
+ */
25
+ getMinute(date: T): number;
26
+ /**
27
+ * Get the second component (0-59)
28
+ */
29
+ getSecond(date: T): number;
30
+ /**
31
+ * Get the timezone name (e.g., "Asia/Seoul", "America/New_York")
32
+ */
33
+ getZoneName(date: T): string;
34
+ /**
35
+ * Add minutes to a date
36
+ */
37
+ plusMinutes(date: T, minutes: number): T;
38
+ /**
39
+ * Add days to a date
40
+ */
41
+ plusDays(date: T, days: number): T;
42
+ /**
43
+ * Subtract days from a date
44
+ */
45
+ minusDays(date: T, days: number): T;
46
+ /**
47
+ * Convert to UTC
48
+ */
49
+ toUTC(date: T): T;
50
+ /**
51
+ * Convert to ISO string
52
+ */
53
+ toISO(date: T): string;
54
+ /**
55
+ * Convert to milliseconds since epoch
56
+ */
57
+ toMillis(date: T): number;
58
+ /**
59
+ * Create a date from milliseconds since epoch
60
+ */
61
+ fromMillis(millis: number, zone: string): T;
62
+ /**
63
+ * Create a UTC date
64
+ */
65
+ createUTC(year: number, month: number, day: number, hour: number, minute: number, second: number): T;
66
+ /**
67
+ * Set the timezone of a date without changing the underlying instant
68
+ */
69
+ setZone(date: T, zoneName: string): T;
70
+ /**
71
+ * Compare two dates (returns true if date1 >= date2)
72
+ */
73
+ isGreaterThanOrEqual(date1: T, date2: T): boolean;
74
+ }
75
+ //# sourceMappingURL=date-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/date-adapter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;IAEzB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;IAE1B;;OAEG;IACH,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;IAExB;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;IAEzB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;IAE3B;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;IAE3B;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;IAE7B;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,CAAC;IAEzC;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IAEnC;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IAEpC;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC;IAElB;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;IAEvB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;IAE1B;;OAEG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IAE5C;;OAEG;IACH,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,CAAC,CAAC;IAEL;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,CAAC;IAEtC;;OAEG;IACH,oBAAoB,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC;CACnD"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { DateAdapter } from "@/adapters/date-adapter";
2
+ interface DateFnsDate {
3
+ date: Date;
4
+ timeZone: string;
5
+ }
6
+ export declare function createDateFnsAdapter(): Promise<DateAdapter<DateFnsDate>>;
7
+ export {};
8
+ //# sourceMappingURL=date-fns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date-fns.d.ts","sourceRoot":"","sources":["../../src/adapters/date-fns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,UAAU,WAAW;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CA4E9E"}
@@ -0,0 +1,73 @@
1
+ export async function createDateFnsAdapter() {
2
+ let addMinutes;
3
+ let addDays;
4
+ let subDays;
5
+ let getYear;
6
+ let getMonth;
7
+ let getDate;
8
+ let getHours;
9
+ let getMinutes;
10
+ let getSeconds;
11
+ let formatISO;
12
+ let fromZonedTime;
13
+ let toZonedTime;
14
+ try {
15
+ const dateFns = await import("date-fns");
16
+ const dateFnsTz = await import("date-fns-tz");
17
+ addMinutes = dateFns.addMinutes;
18
+ addDays = dateFns.addDays;
19
+ subDays = dateFns.subDays;
20
+ getYear = dateFns.getYear;
21
+ getMonth = dateFns.getMonth;
22
+ getDate = dateFns.getDate;
23
+ getHours = dateFns.getHours;
24
+ getMinutes = dateFns.getMinutes;
25
+ getSeconds = dateFns.getSeconds;
26
+ formatISO = dateFns.formatISO;
27
+ fromZonedTime = dateFnsTz.fromZonedTime;
28
+ toZonedTime = dateFnsTz.toZonedTime;
29
+ }
30
+ catch {
31
+ throw new Error("date-fns or date-fns-tz is not installed. Install with: npm install date-fns date-fns-tz");
32
+ }
33
+ return {
34
+ getYear: (dateFns) => getYear(dateFns.date),
35
+ getMonth: (dateFns) => getMonth(dateFns.date) + 1,
36
+ getDay: (dateFns) => getDate(dateFns.date),
37
+ getHour: (dateFns) => getHours(dateFns.date),
38
+ getMinute: (dateFns) => getMinutes(dateFns.date),
39
+ getSecond: (dateFns) => getSeconds(dateFns.date),
40
+ getZoneName: (dateFns) => dateFns.timeZone,
41
+ plusMinutes: (dateFns, minutes) => ({
42
+ date: addMinutes(dateFns.date, minutes),
43
+ timeZone: dateFns.timeZone,
44
+ }),
45
+ plusDays: (dateFns, days) => ({
46
+ date: addDays(dateFns.date, days),
47
+ timeZone: dateFns.timeZone,
48
+ }),
49
+ minusDays: (dateFns, days) => ({
50
+ date: subDays(dateFns.date, days),
51
+ timeZone: dateFns.timeZone,
52
+ }),
53
+ toUTC: (dateFns) => ({
54
+ date: fromZonedTime(dateFns.date, dateFns.timeZone),
55
+ timeZone: "UTC",
56
+ }),
57
+ toISO: (dateFns) => formatISO(dateFns.date),
58
+ toMillis: (dateFns) => dateFns.date.getTime(),
59
+ fromMillis: (millis, zone) => ({
60
+ date: new Date(millis),
61
+ timeZone: zone,
62
+ }),
63
+ createUTC: (year, month, day, hour, minute, second) => ({
64
+ date: new Date(Date.UTC(year, month - 1, day, hour, minute, second)),
65
+ timeZone: "UTC",
66
+ }),
67
+ setZone: (dateFns, zoneName) => ({
68
+ date: toZonedTime(dateFns.date, zoneName),
69
+ timeZone: zoneName,
70
+ }),
71
+ isGreaterThanOrEqual: (date1, date2) => date1.date >= date2.date,
72
+ };
73
+ }
@@ -0,0 +1,3 @@
1
+ import type { DateAdapter } from "@/adapters/date-adapter";
2
+ export declare function createLuxonAdapter(): Promise<DateAdapter<import("luxon").DateTime>>;
3
+ //# sourceMappingURL=luxon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"luxon.d.ts","sourceRoot":"","sources":["../../src/adapters/luxon.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,WAAW,CAAC,OAAO,OAAO,EAAE,QAAQ,CAAC,CAAC,CAsCzF"}
@@ -0,0 +1,39 @@
1
+ export async function createLuxonAdapter() {
2
+ let DateTime;
3
+ try {
4
+ const luxon = await import("luxon");
5
+ DateTime = luxon.DateTime;
6
+ }
7
+ catch {
8
+ throw new Error("Luxon is not installed. Install it with: npm install luxon @types/luxon");
9
+ }
10
+ return {
11
+ getYear: (date) => date.year,
12
+ getMonth: (date) => date.month,
13
+ getDay: (date) => date.day,
14
+ getHour: (date) => date.hour,
15
+ getMinute: (date) => date.minute,
16
+ getSecond: (date) => date.second,
17
+ getZoneName: (date) => {
18
+ const zoneName = date.zoneName;
19
+ if (zoneName === null)
20
+ throw new Error("DateTime has no timezone");
21
+ return zoneName;
22
+ },
23
+ plusMinutes: (date, minutes) => date.plus({ minutes }),
24
+ plusDays: (date, days) => date.plus({ days }),
25
+ minusDays: (date, days) => date.minus({ days }),
26
+ toUTC: (date) => date.toUTC(),
27
+ toISO: (date) => {
28
+ const iso = date.toISO();
29
+ if (iso === null)
30
+ throw new Error("Failed to convert DateTime to ISO string");
31
+ return iso;
32
+ },
33
+ toMillis: (date) => date.toMillis(),
34
+ fromMillis: (millis, zone) => DateTime.fromMillis(millis, { zone }),
35
+ createUTC: (year, month, day, hour, minute, second) => DateTime.utc(year, month, day, hour, minute, second),
36
+ setZone: (date, zoneName) => date.setZone(zoneName),
37
+ isGreaterThanOrEqual: (date1, date2) => date1 >= date2,
38
+ };
39
+ }
@@ -0,0 +1,88 @@
1
+ import type { DateAdapter } from "@/adapters/date-adapter";
2
+ export declare const STEMS: string[];
3
+ export declare const BRANCHES: string[];
4
+ export declare const STANDARD_PRESET: {
5
+ dayBoundary: "midnight";
6
+ useMeanSolarTimeForHour: boolean;
7
+ useMeanSolarTimeForBoundary: boolean;
8
+ };
9
+ export declare const TRADITIONAL_PRESET: {
10
+ dayBoundary: "zi23";
11
+ useMeanSolarTimeForHour: boolean;
12
+ useMeanSolarTimeForBoundary: boolean;
13
+ };
14
+ export declare const presetA: {
15
+ dayBoundary: "midnight";
16
+ useMeanSolarTimeForHour: boolean;
17
+ useMeanSolarTimeForBoundary: boolean;
18
+ };
19
+ export declare const presetB: {
20
+ dayBoundary: "zi23";
21
+ useMeanSolarTimeForHour: boolean;
22
+ useMeanSolarTimeForBoundary: boolean;
23
+ };
24
+ export declare function dayPillarFromDate({ year, month, day, }: {
25
+ year: number;
26
+ month: number;
27
+ day: number;
28
+ }): {
29
+ idx60: number;
30
+ pillar: string;
31
+ };
32
+ export declare function applyMeanSolarTime<T>(adapter: DateAdapter<T>, dtLocal: T, longitudeDeg: number, tzOffsetHours?: number): T;
33
+ export declare function yearPillar<T>(adapter: DateAdapter<T>, dtLocal: T): {
34
+ idx60: number;
35
+ pillar: string;
36
+ solarYear: number;
37
+ };
38
+ export declare function monthPillar<T>(adapter: DateAdapter<T>, dtLocal: T): {
39
+ pillar: string;
40
+ sunLonDeg: number;
41
+ };
42
+ export declare function effectiveDayDate<T>(adapter: DateAdapter<T>, dtLocal: T, { dayBoundary, longitudeDeg, tzOffsetHours, useMeanSolarTimeForBoundary, }?: {
43
+ dayBoundary?: "midnight" | "zi23";
44
+ longitudeDeg?: number;
45
+ tzOffsetHours?: number;
46
+ useMeanSolarTimeForBoundary?: boolean;
47
+ }): {
48
+ year: number;
49
+ month: number;
50
+ day: number;
51
+ };
52
+ export declare function hourPillar<T>(adapter: DateAdapter<T>, dtLocal: T, { longitudeDeg, tzOffsetHours, useMeanSolarTimeForHour, dayBoundary, useMeanSolarTimeForBoundary, }?: {
53
+ longitudeDeg?: number;
54
+ tzOffsetHours?: number;
55
+ useMeanSolarTimeForHour?: boolean;
56
+ dayBoundary?: "midnight" | "zi23";
57
+ useMeanSolarTimeForBoundary?: boolean;
58
+ }): {
59
+ pillar: string;
60
+ adjustedDt: T;
61
+ effectiveDate: {
62
+ year: number;
63
+ month: number;
64
+ day: number;
65
+ };
66
+ };
67
+ export declare function getFourPillars<T>(adapter: DateAdapter<T>, dtLocal: T, { longitudeDeg, tzOffsetHours, preset, }: {
68
+ longitudeDeg: number;
69
+ tzOffsetHours?: number;
70
+ preset?: typeof presetA | typeof presetB;
71
+ }): {
72
+ year: string;
73
+ month: string;
74
+ day: string;
75
+ hour: string;
76
+ meta: {
77
+ solarYearUsed: number;
78
+ sunLonDeg: number;
79
+ effectiveDayDate: {
80
+ year: number;
81
+ month: number;
82
+ day: number;
83
+ };
84
+ adjustedDtForHour: string;
85
+ preset: typeof preset;
86
+ };
87
+ };
88
+ //# sourceMappingURL=four-pillars.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"four-pillars.d.ts","sourceRoot":"","sources":["../../src/core/four-pillars.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,221 @@
1
+ export const STEMS = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
2
+ export const BRANCHES = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
3
+ export const STANDARD_PRESET = {
4
+ dayBoundary: "midnight",
5
+ useMeanSolarTimeForHour: false,
6
+ useMeanSolarTimeForBoundary: false,
7
+ };
8
+ export const TRADITIONAL_PRESET = {
9
+ dayBoundary: "zi23",
10
+ useMeanSolarTimeForHour: true,
11
+ useMeanSolarTimeForBoundary: true,
12
+ };
13
+ export const presetA = STANDARD_PRESET;
14
+ export const presetB = TRADITIONAL_PRESET;
15
+ function normDeg(x) {
16
+ x %= 360;
17
+ return x < 0 ? x + 360 : x;
18
+ }
19
+ function pillarFrom60(idx60) {
20
+ return STEMS[idx60 % 10] + BRANCHES[idx60 % 12];
21
+ }
22
+ function jdnFromDate(y, m, d) {
23
+ const a = Math.floor((14 - m) / 12);
24
+ const y2 = y + 4800 - a;
25
+ const m2 = m + 12 * a - 3;
26
+ return (d +
27
+ Math.floor((153 * m2 + 2) / 5) +
28
+ 365 * y2 +
29
+ Math.floor(y2 / 4) -
30
+ Math.floor(y2 / 100) +
31
+ Math.floor(y2 / 400) -
32
+ 32045);
33
+ }
34
+ export function dayPillarFromDate({ year, month, day, }) {
35
+ const jdn = jdnFromDate(year, month, day);
36
+ const idx60 = (jdn + 12) % 60;
37
+ return { idx60, pillar: pillarFrom60(idx60) };
38
+ }
39
+ export function applyMeanSolarTime(adapter, dtLocal, longitudeDeg, tzOffsetHours = 9) {
40
+ const deltaMinutes = 4 * (longitudeDeg - 15 * tzOffsetHours);
41
+ return adapter.plusMinutes(dtLocal, deltaMinutes);
42
+ }
43
+ function sunApparentLongitude(adapter, dtUtc) {
44
+ let y = adapter.getYear(dtUtc);
45
+ let m = adapter.getMonth(dtUtc);
46
+ const d = adapter.getDay(dtUtc) +
47
+ (adapter.getHour(dtUtc) + (adapter.getMinute(dtUtc) + adapter.getSecond(dtUtc) / 60) / 60) / 24;
48
+ if (m <= 2) {
49
+ y -= 1;
50
+ m += 12;
51
+ }
52
+ const A = Math.floor(y / 100);
53
+ const B = 2 - A + Math.floor(A / 4);
54
+ const JD = Math.floor(365.25 * (y + 4716)) + Math.floor(30.6001 * (m + 1)) + d + B - 1524.5;
55
+ const T = (JD - 2451545.0) / 36525.0;
56
+ const L0 = normDeg(280.46646 + 36000.76983 * T + 0.0003032 * T * T);
57
+ const M = normDeg(357.52911 + 35999.05029 * T - 0.0001537 * T * T);
58
+ const deg2rad = (deg) => (deg * Math.PI) / 180;
59
+ const C = (1.914602 - 0.004817 * T - 0.000014 * T * T) * Math.sin(deg2rad(M)) +
60
+ (0.019993 - 0.000101 * T) * Math.sin(deg2rad(2 * M)) +
61
+ 0.000289 * Math.sin(deg2rad(3 * M));
62
+ const trueLong = L0 + C;
63
+ const omega = 125.04 - 1934.136 * T;
64
+ const lambda = trueLong - 0.00569 - 0.00478 * Math.sin(deg2rad(omega));
65
+ return normDeg(lambda);
66
+ }
67
+ function angleDiffDeg(a, b) {
68
+ return ((a - b + 540) % 360) - 180;
69
+ }
70
+ function findTermUtc(adapter, targetDeg, startUtc, endUtc) {
71
+ let a = startUtc;
72
+ let b = endUtc;
73
+ const f = (dt) => angleDiffDeg(sunApparentLongitude(adapter, dt), targetDeg);
74
+ let fa = f(a);
75
+ let fb = f(b);
76
+ let expand = 0;
77
+ while (fa * fb > 0 && expand < 10) {
78
+ a = adapter.minusDays(a, 1);
79
+ b = adapter.plusDays(b, 1);
80
+ fa = f(a);
81
+ fb = f(b);
82
+ expand += 1;
83
+ }
84
+ if (fa * fb > 0)
85
+ throw new Error("Failed to bracket solar term");
86
+ for (let i = 0; i < 80; i++) {
87
+ const midMillis = (adapter.toMillis(a) + adapter.toMillis(b)) / 2;
88
+ const mid = adapter.fromMillis(midMillis, "utc");
89
+ const fm = f(mid);
90
+ if (Math.abs(fm) < 1e-6)
91
+ return mid;
92
+ if (fa * fm <= 0) {
93
+ b = mid;
94
+ fb = fm;
95
+ }
96
+ else {
97
+ a = mid;
98
+ fa = fm;
99
+ }
100
+ }
101
+ return adapter.fromMillis((adapter.toMillis(a) + adapter.toMillis(b)) / 2, "utc");
102
+ }
103
+ function lichunUtc(adapter, year) {
104
+ const start = adapter.createUTC(year, 2, 1, 0, 0, 0);
105
+ const end = adapter.createUTC(year, 2, 7, 0, 0, 0);
106
+ return findTermUtc(adapter, 315.0, start, end);
107
+ }
108
+ export function yearPillar(adapter, dtLocal) {
109
+ const y = adapter.getYear(dtLocal);
110
+ const lichunLocal = adapter.setZone(lichunUtc(adapter, y), adapter.getZoneName(dtLocal));
111
+ const solarYear = adapter.isGreaterThanOrEqual(dtLocal, lichunLocal) ? y : y - 1;
112
+ const idx60 = (((solarYear - 1984) % 60) + 60) % 60;
113
+ return { idx60, pillar: pillarFrom60(idx60), solarYear };
114
+ }
115
+ function monthBranchIndexFromSunLon(lon) {
116
+ return (Math.floor(((lon + 45) % 360) / 30) + 2) % 12;
117
+ }
118
+ function firstMonthStemIndex(yearStemIdx) {
119
+ const map = new Map([
120
+ [0, 2],
121
+ [5, 2],
122
+ [1, 4],
123
+ [6, 4],
124
+ [2, 6],
125
+ [7, 6],
126
+ [3, 8],
127
+ [8, 8],
128
+ [4, 0],
129
+ [9, 0],
130
+ ]);
131
+ return map.get(yearStemIdx) ?? 0;
132
+ }
133
+ export function monthPillar(adapter, dtLocal) {
134
+ const { idx60: yIdx60 } = yearPillar(adapter, dtLocal);
135
+ const yearStemIdx = yIdx60 % 10;
136
+ const lon = sunApparentLongitude(adapter, adapter.toUTC(dtLocal));
137
+ const mBranchIdx = monthBranchIndexFromSunLon(lon);
138
+ const monthNo = (mBranchIdx - 2 + 12) % 12;
139
+ const mStemIdx = (firstMonthStemIndex(yearStemIdx) + monthNo) % 10;
140
+ return { pillar: STEMS[mStemIdx] + BRANCHES[mBranchIdx], sunLonDeg: lon };
141
+ }
142
+ export function effectiveDayDate(adapter, dtLocal, { dayBoundary = "midnight", longitudeDeg, tzOffsetHours = 9, useMeanSolarTimeForBoundary = false, } = {}) {
143
+ let dtChk = dtLocal;
144
+ if (useMeanSolarTimeForBoundary) {
145
+ if (typeof longitudeDeg !== "number")
146
+ throw new Error("longitudeDeg required when useMeanSolarTimeForBoundary=true");
147
+ dtChk = applyMeanSolarTime(adapter, dtLocal, longitudeDeg, tzOffsetHours);
148
+ }
149
+ let d = dtChk;
150
+ if (dayBoundary === "zi23") {
151
+ if (adapter.getHour(dtChk) >= 23)
152
+ d = adapter.plusDays(dtChk, 1);
153
+ }
154
+ else if (dayBoundary !== "midnight") {
155
+ throw new Error("dayBoundary must be 'midnight' or 'zi23'");
156
+ }
157
+ return { year: adapter.getYear(d), month: adapter.getMonth(d), day: adapter.getDay(d) };
158
+ }
159
+ function hourBranchIndexFromHour(h) {
160
+ // Traditional Chinese hours (時辰) mapping:
161
+ // Each branch represents a 2-hour period, starting from 子時 at 23:00
162
+ // 子時 (0): 23:00-01:00, 丑時 (1): 01:00-03:00, ..., 亥時 (11): 21:00-23:00
163
+ // Formula: (hour + 1) / 2 gives the branch index, but we need +1 offset
164
+ // because 23:00 (hour 23) should map to index 0 (子), wrapping around
165
+ return Math.floor((h + 1) / 2 + 1) % 12;
166
+ }
167
+ export function hourPillar(adapter, dtLocal, { longitudeDeg, tzOffsetHours = 9, useMeanSolarTimeForHour = false, dayBoundary = "midnight", useMeanSolarTimeForBoundary = false, } = {}) {
168
+ let dtUsed = dtLocal;
169
+ if (useMeanSolarTimeForHour) {
170
+ if (typeof longitudeDeg !== "number")
171
+ throw new Error("longitudeDeg required when useMeanSolarTimeForHour=true");
172
+ dtUsed = applyMeanSolarTime(adapter, dtLocal, longitudeDeg, tzOffsetHours);
173
+ }
174
+ const effDate = effectiveDayDate(adapter, dtLocal, {
175
+ dayBoundary,
176
+ longitudeDeg,
177
+ tzOffsetHours,
178
+ useMeanSolarTimeForBoundary,
179
+ });
180
+ const { idx60: dIdx60 } = dayPillarFromDate(effDate);
181
+ const dayStemIdx = dIdx60 % 10;
182
+ const hb = hourBranchIndexFromHour(adapter.getHour(dtUsed));
183
+ const hs = (dayStemIdx * 2 + hb) % 10;
184
+ return { pillar: STEMS[hs] + BRANCHES[hb], adjustedDt: dtUsed, effectiveDate: effDate };
185
+ }
186
+ export function getFourPillars(adapter, dtLocal, { longitudeDeg, tzOffsetHours = 9, preset = presetA, }) {
187
+ if (typeof longitudeDeg !== "number")
188
+ throw new Error("longitudeDeg is required");
189
+ const dayBoundary = preset.dayBoundary ?? "midnight";
190
+ const useMeanSolarTimeForHour = preset.useMeanSolarTimeForHour ?? false;
191
+ const useMeanSolarTimeForBoundary = preset.useMeanSolarTimeForBoundary ?? false;
192
+ const y = yearPillar(adapter, dtLocal);
193
+ const m = monthPillar(adapter, dtLocal);
194
+ const effDate = effectiveDayDate(adapter, dtLocal, {
195
+ dayBoundary,
196
+ longitudeDeg,
197
+ tzOffsetHours,
198
+ useMeanSolarTimeForBoundary,
199
+ });
200
+ const d = dayPillarFromDate(effDate);
201
+ const h = hourPillar(adapter, dtLocal, {
202
+ longitudeDeg,
203
+ tzOffsetHours,
204
+ useMeanSolarTimeForHour,
205
+ dayBoundary,
206
+ useMeanSolarTimeForBoundary,
207
+ });
208
+ return {
209
+ year: y.pillar,
210
+ month: m.pillar,
211
+ day: d.pillar,
212
+ hour: h.pillar,
213
+ meta: {
214
+ solarYearUsed: y.solarYear,
215
+ sunLonDeg: m.sunLonDeg,
216
+ effectiveDayDate: effDate,
217
+ adjustedDtForHour: adapter.toISO(h.adjustedDt),
218
+ preset,
219
+ },
220
+ };
221
+ }
@@ -0,0 +1,3 @@
1
+ export type { DateAdapter } from "@/adapters/date-adapter";
2
+ export { applyMeanSolarTime, BRANCHES, dayPillarFromDate, effectiveDayDate, getFourPillars, hourPillar, monthPillar, presetA, presetB, STANDARD_PRESET, STEMS, TRADITIONAL_PRESET, yearPillar, } from "@/core/four-pillars";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EACL,kBAAkB,EAClB,QAAQ,EACR,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,WAAW,EACX,OAAO,EACP,OAAO,EACP,eAAe,EACf,KAAK,EACL,kBAAkB,EAClB,UAAU,GACX,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { applyMeanSolarTime, BRANCHES, dayPillarFromDate, effectiveDayDate, getFourPillars, hourPillar, monthPillar, presetA, presetB, STANDARD_PRESET, STEMS, TRADITIONAL_PRESET, yearPillar, } from "@/core/four-pillars";
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@gracefullight/saju",
3
+ "version": "0.1.0",
4
+ "description": "Four Pillars (四柱) calculator with flexible date adapter support",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./adapters/luxon": {
15
+ "import": "./dist/adapters/luxon.js",
16
+ "types": "./dist/adapters/luxon.d.ts"
17
+ },
18
+ "./adapters/date-fns": {
19
+ "import": "./dist/adapters/date-fns.js",
20
+ "types": "./dist/adapters/date-fns.d.ts"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "dev": "tsx src/index.ts",
29
+ "test": "vitest",
30
+ "test:ui": "vitest --ui",
31
+ "test:coverage": "vitest --coverage",
32
+ "lint": "biome check src",
33
+ "lint:fix": "biome check --write src",
34
+ "format": "biome format --write src"
35
+ },
36
+ "keywords": [
37
+ "saju",
38
+ "four-pillars",
39
+ "bazi",
40
+ "chinese-astrology",
41
+ "korean-astrology"
42
+ ],
43
+ "author": "gracefullight",
44
+ "license": "MIT",
45
+ "peerDependencies": {
46
+ "date-fns": "^4.0.0",
47
+ "date-fns-tz": "^3.0.0",
48
+ "luxon": "^3.0.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "date-fns": {
52
+ "optional": true
53
+ },
54
+ "date-fns-tz": {
55
+ "optional": true
56
+ },
57
+ "luxon": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "devDependencies": {
62
+ "@types/luxon": "^3.4.2",
63
+ "@vitest/coverage-v8": "^2.1.8",
64
+ "@vitest/ui": "^2.1.8",
65
+ "date-fns": "^4.1.0",
66
+ "date-fns-tz": "^3.2.0",
67
+ "luxon": "^3.5.0",
68
+ "vitest": "^2.1.8"
69
+ }
70
+ }