@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.
- package/README.en.md +665 -0
- package/README.md +665 -0
- package/dist/__tests__/date-fns-adapter.test.d.ts +2 -0
- package/dist/__tests__/date-fns-adapter.test.d.ts.map +1 -0
- package/dist/__tests__/date-fns-adapter.test.js +155 -0
- package/dist/__tests__/four-pillars.test.d.ts +2 -0
- package/dist/__tests__/four-pillars.test.d.ts.map +1 -0
- package/dist/__tests__/four-pillars.test.js +289 -0
- package/dist/__tests__/luxon-adapter.test.d.ts +2 -0
- package/dist/__tests__/luxon-adapter.test.d.ts.map +1 -0
- package/dist/__tests__/luxon-adapter.test.js +166 -0
- package/dist/adapters/date-adapter.d.ts +75 -0
- package/dist/adapters/date-adapter.d.ts.map +1 -0
- package/dist/adapters/date-adapter.js +1 -0
- package/dist/adapters/date-fns.d.ts +8 -0
- package/dist/adapters/date-fns.d.ts.map +1 -0
- package/dist/adapters/date-fns.js +73 -0
- package/dist/adapters/luxon.d.ts +3 -0
- package/dist/adapters/luxon.d.ts.map +1 -0
- package/dist/adapters/luxon.js +39 -0
- package/dist/core/four-pillars.d.ts +88 -0
- package/dist/core/four-pillars.d.ts.map +1 -0
- package/dist/core/four-pillars.js +221 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { createDateFnsAdapter } from "@/adapters/date-fns";
|
|
3
|
+
describe("date-fns Adapter", () => {
|
|
4
|
+
let adapter;
|
|
5
|
+
beforeAll(async () => {
|
|
6
|
+
adapter = await createDateFnsAdapter();
|
|
7
|
+
});
|
|
8
|
+
describe("Basic date getters", () => {
|
|
9
|
+
it("should get year correctly", () => {
|
|
10
|
+
const dt = { date: new Date(2000, 0, 1), timeZone: "Asia/Seoul" };
|
|
11
|
+
expect(adapter.getYear(dt)).toBe(2000);
|
|
12
|
+
});
|
|
13
|
+
it("should get month correctly (1-based)", () => {
|
|
14
|
+
const dt = { date: new Date(2000, 0, 1), timeZone: "Asia/Seoul" };
|
|
15
|
+
expect(adapter.getMonth(dt)).toBe(1);
|
|
16
|
+
});
|
|
17
|
+
it("should get day correctly", () => {
|
|
18
|
+
const dt = { date: new Date(2000, 0, 1), timeZone: "Asia/Seoul" };
|
|
19
|
+
expect(adapter.getDay(dt)).toBe(1);
|
|
20
|
+
});
|
|
21
|
+
it("should get hour correctly", () => {
|
|
22
|
+
const dt = { date: new Date(2000, 0, 1, 18, 0), timeZone: "Asia/Seoul" };
|
|
23
|
+
expect(adapter.getHour(dt)).toBe(18);
|
|
24
|
+
});
|
|
25
|
+
it("should get minute correctly", () => {
|
|
26
|
+
const dt = { date: new Date(2000, 0, 1, 18, 0), timeZone: "Asia/Seoul" };
|
|
27
|
+
expect(adapter.getMinute(dt)).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
it("should get second correctly", () => {
|
|
30
|
+
const dt = { date: new Date(2000, 0, 1, 18, 0, 30), timeZone: "Asia/Seoul" };
|
|
31
|
+
expect(adapter.getSecond(dt)).toBe(30);
|
|
32
|
+
});
|
|
33
|
+
it("should get zone name correctly", () => {
|
|
34
|
+
const dt = { date: new Date(2000, 0, 1), timeZone: "Asia/Seoul" };
|
|
35
|
+
expect(adapter.getZoneName(dt)).toBe("Asia/Seoul");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe("Date arithmetic", () => {
|
|
39
|
+
it("should add minutes correctly", () => {
|
|
40
|
+
const dt = { date: new Date(2000, 0, 1, 18, 0), timeZone: "Asia/Seoul" };
|
|
41
|
+
const result = adapter.plusMinutes(dt, 30);
|
|
42
|
+
expect(adapter.getMinute(result)).toBe(30);
|
|
43
|
+
expect(adapter.getHour(result)).toBe(18);
|
|
44
|
+
});
|
|
45
|
+
it("should add minutes with hour overflow", () => {
|
|
46
|
+
const dt = { date: new Date(2000, 0, 1, 23, 45), timeZone: "Asia/Seoul" };
|
|
47
|
+
const result = adapter.plusMinutes(dt, 30);
|
|
48
|
+
expect(adapter.getHour(result)).toBe(0);
|
|
49
|
+
expect(adapter.getMinute(result)).toBe(15);
|
|
50
|
+
expect(adapter.getDay(result)).toBe(2);
|
|
51
|
+
});
|
|
52
|
+
it("should add days correctly", () => {
|
|
53
|
+
const dt = { date: new Date(2000, 0, 1), timeZone: "Asia/Seoul" };
|
|
54
|
+
const result = adapter.plusDays(dt, 5);
|
|
55
|
+
expect(adapter.getDay(result)).toBe(6);
|
|
56
|
+
expect(adapter.getMonth(result)).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
it("should add days with month overflow", () => {
|
|
59
|
+
const dt = { date: new Date(2000, 0, 30), timeZone: "Asia/Seoul" };
|
|
60
|
+
const result = adapter.plusDays(dt, 5);
|
|
61
|
+
expect(adapter.getDay(result)).toBe(4);
|
|
62
|
+
expect(adapter.getMonth(result)).toBe(2);
|
|
63
|
+
});
|
|
64
|
+
it("should subtract days correctly", () => {
|
|
65
|
+
const dt = { date: new Date(2000, 0, 10), timeZone: "Asia/Seoul" };
|
|
66
|
+
const result = adapter.minusDays(dt, 5);
|
|
67
|
+
expect(adapter.getDay(result)).toBe(5);
|
|
68
|
+
expect(adapter.getMonth(result)).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
it("should subtract days with month underflow", () => {
|
|
71
|
+
const dt = { date: new Date(2000, 1, 3), timeZone: "Asia/Seoul" };
|
|
72
|
+
const result = adapter.minusDays(dt, 5);
|
|
73
|
+
expect(adapter.getDay(result)).toBe(29);
|
|
74
|
+
expect(adapter.getMonth(result)).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("Timezone operations", () => {
|
|
78
|
+
it("should convert to UTC correctly", () => {
|
|
79
|
+
const dt = { date: new Date(2000, 0, 1, 18, 0), timeZone: "Asia/Seoul" };
|
|
80
|
+
const utc = adapter.toUTC(dt);
|
|
81
|
+
expect(adapter.getZoneName(utc)).toBe("UTC");
|
|
82
|
+
});
|
|
83
|
+
it("should set zone correctly", () => {
|
|
84
|
+
const dt = { date: new Date(2000, 0, 1, 18, 0), timeZone: "Asia/Seoul" };
|
|
85
|
+
const ny = adapter.setZone(dt, "America/New_York");
|
|
86
|
+
expect(adapter.getZoneName(ny)).toBe("America/New_York");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe("Conversion methods", () => {
|
|
90
|
+
it("should convert to ISO string", () => {
|
|
91
|
+
const dt = { date: new Date(2000, 0, 1, 18, 0), timeZone: "Asia/Seoul" };
|
|
92
|
+
const iso = adapter.toISO(dt);
|
|
93
|
+
expect(iso).toContain("2000-01-01");
|
|
94
|
+
expect(iso).toContain("18:00");
|
|
95
|
+
});
|
|
96
|
+
it("should convert to milliseconds", () => {
|
|
97
|
+
const dt = { date: new Date(2000, 0, 1, 18, 0), timeZone: "Asia/Seoul" };
|
|
98
|
+
const millis = adapter.toMillis(dt);
|
|
99
|
+
expect(typeof millis).toBe("number");
|
|
100
|
+
expect(millis).toBeGreaterThan(0);
|
|
101
|
+
});
|
|
102
|
+
it("should create date from milliseconds", () => {
|
|
103
|
+
const millis = new Date(2000, 0, 1, 18, 0).getTime();
|
|
104
|
+
const dt = adapter.fromMillis(millis, "Asia/Seoul");
|
|
105
|
+
expect(adapter.getYear(dt)).toBe(2000);
|
|
106
|
+
expect(adapter.getMonth(dt)).toBe(1);
|
|
107
|
+
expect(adapter.getDay(dt)).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe("UTC date creation", () => {
|
|
111
|
+
it("should create UTC date correctly", () => {
|
|
112
|
+
const dt = adapter.createUTC(2000, 2, 5, 12, 30, 45);
|
|
113
|
+
expect(adapter.getZoneName(dt)).toBe("UTC");
|
|
114
|
+
expect(adapter.getYear(dt)).toBe(2000);
|
|
115
|
+
expect(adapter.getMonth(dt)).toBe(2);
|
|
116
|
+
expect(adapter.getDay(dt)).toBe(5);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe("Date comparison", () => {
|
|
120
|
+
it("should compare dates correctly (greater than)", () => {
|
|
121
|
+
const dt1 = { date: new Date(2000, 0, 2), timeZone: "UTC" };
|
|
122
|
+
const dt2 = { date: new Date(2000, 0, 1), timeZone: "UTC" };
|
|
123
|
+
expect(adapter.isGreaterThanOrEqual(dt1, dt2)).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it("should compare dates correctly (equal)", () => {
|
|
126
|
+
const dt1 = { date: new Date(2000, 0, 1, 12, 0, 0), timeZone: "UTC" };
|
|
127
|
+
const dt2 = { date: new Date(2000, 0, 1, 12, 0, 0), timeZone: "UTC" };
|
|
128
|
+
expect(adapter.isGreaterThanOrEqual(dt1, dt2)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
it("should compare dates correctly (less than)", () => {
|
|
131
|
+
const dt1 = { date: new Date(1999, 11, 31), timeZone: "UTC" };
|
|
132
|
+
const dt2 = { date: new Date(2000, 0, 1), timeZone: "UTC" };
|
|
133
|
+
expect(adapter.isGreaterThanOrEqual(dt1, dt2)).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe("Edge cases", () => {
|
|
137
|
+
it("should handle leap year correctly", () => {
|
|
138
|
+
const dt = { date: new Date(2000, 1, 29), timeZone: "UTC" };
|
|
139
|
+
expect(adapter.getDay(dt)).toBe(29);
|
|
140
|
+
expect(adapter.getMonth(dt)).toBe(2);
|
|
141
|
+
});
|
|
142
|
+
it("should handle year boundary", () => {
|
|
143
|
+
const dt = { date: new Date(1999, 11, 31, 23, 59), timeZone: "UTC" };
|
|
144
|
+
const result = adapter.plusMinutes(dt, 2);
|
|
145
|
+
expect(adapter.getYear(result)).toBe(2000);
|
|
146
|
+
expect(adapter.getMonth(result)).toBe(1);
|
|
147
|
+
expect(adapter.getDay(result)).toBe(1);
|
|
148
|
+
});
|
|
149
|
+
it("should preserve timeZone property after operations", () => {
|
|
150
|
+
const dt = { date: new Date(2000, 0, 1), timeZone: "Asia/Tokyo" };
|
|
151
|
+
const result = adapter.plusDays(dt, 1);
|
|
152
|
+
expect(adapter.getZoneName(result)).toBe("Asia/Tokyo");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"four-pillars.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/four-pillars.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
3
|
+
import { createLuxonAdapter } from "@/adapters/luxon";
|
|
4
|
+
import { BRANCHES, dayPillarFromDate, effectiveDayDate, getFourPillars, hourPillar, monthPillar, presetA, presetB, STEMS, yearPillar, } from "@/core/four-pillars";
|
|
5
|
+
describe("Four Pillars Core", () => {
|
|
6
|
+
let adapter;
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
adapter = await createLuxonAdapter();
|
|
9
|
+
});
|
|
10
|
+
describe("Constants", () => {
|
|
11
|
+
it("should have 10 stems", () => {
|
|
12
|
+
expect(STEMS).toHaveLength(10);
|
|
13
|
+
expect(STEMS[0]).toBe("甲");
|
|
14
|
+
expect(STEMS[9]).toBe("癸");
|
|
15
|
+
});
|
|
16
|
+
it("should have 12 branches", () => {
|
|
17
|
+
expect(BRANCHES).toHaveLength(12);
|
|
18
|
+
expect(BRANCHES[0]).toBe("子");
|
|
19
|
+
expect(BRANCHES[11]).toBe("亥");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("dayPillarFromDate", () => {
|
|
23
|
+
it("should calculate day pillar for test case (1992-10-12 = 甲申)", () => {
|
|
24
|
+
const result = dayPillarFromDate({ year: 1992, month: 10, day: 12 });
|
|
25
|
+
expect(result.pillar).toBe("甲申");
|
|
26
|
+
expect(result.idx60).toBe(20);
|
|
27
|
+
});
|
|
28
|
+
it("should handle 60-day cycle correctly", () => {
|
|
29
|
+
const start = dayPillarFromDate({ year: 1992, month: 10, day: 12 });
|
|
30
|
+
const after60 = dayPillarFromDate({ year: 1992, month: 12, day: 11 });
|
|
31
|
+
expect(start.idx60).toBe(after60.idx60);
|
|
32
|
+
});
|
|
33
|
+
it("should calculate day pillar for leap year date", () => {
|
|
34
|
+
const result = dayPillarFromDate({ year: 2000, month: 2, day: 29 });
|
|
35
|
+
expect(result.pillar).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
|
|
36
|
+
});
|
|
37
|
+
it("should calculate day pillar for year boundary", () => {
|
|
38
|
+
const dec31 = dayPillarFromDate({ year: 1999, month: 12, day: 31 });
|
|
39
|
+
const jan01 = dayPillarFromDate({ year: 2000, month: 1, day: 1 });
|
|
40
|
+
expect((jan01.idx60 - dec31.idx60 + 60) % 60).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe("yearPillar", () => {
|
|
44
|
+
it("should calculate year pillar for 1984 (甲子)", () => {
|
|
45
|
+
const dt = DateTime.fromObject({ year: 1984, month: 3, day: 1 }, { zone: "Asia/Seoul" });
|
|
46
|
+
const result = yearPillar(adapter, dt);
|
|
47
|
+
expect(result.pillar).toBe("甲子");
|
|
48
|
+
expect(result.solarYear).toBe(1984);
|
|
49
|
+
});
|
|
50
|
+
it("should calculate year pillar for 1992 (壬申)", () => {
|
|
51
|
+
const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12 }, { zone: "Asia/Seoul" });
|
|
52
|
+
const result = yearPillar(adapter, dt);
|
|
53
|
+
expect(result.pillar).toBe("壬申");
|
|
54
|
+
expect(result.solarYear).toBe(1992);
|
|
55
|
+
});
|
|
56
|
+
it("should handle date before Lichun (立春)", () => {
|
|
57
|
+
const dt = DateTime.fromObject({ year: 1992, month: 1, day: 15 }, { zone: "Asia/Seoul" });
|
|
58
|
+
const result = yearPillar(adapter, dt);
|
|
59
|
+
expect(result.solarYear).toBe(1991);
|
|
60
|
+
});
|
|
61
|
+
it("should handle date after Lichun", () => {
|
|
62
|
+
const dt = DateTime.fromObject({ year: 1992, month: 3, day: 1 }, { zone: "Asia/Seoul" });
|
|
63
|
+
const result = yearPillar(adapter, dt);
|
|
64
|
+
expect(result.solarYear).toBe(1992);
|
|
65
|
+
});
|
|
66
|
+
it("should follow 60-year cycle", () => {
|
|
67
|
+
const dt1 = DateTime.fromObject({ year: 1984, month: 3, day: 1 }, { zone: "Asia/Seoul" });
|
|
68
|
+
const dt2 = DateTime.fromObject({ year: 2044, month: 3, day: 1 }, { zone: "Asia/Seoul" });
|
|
69
|
+
const result1 = yearPillar(adapter, dt1);
|
|
70
|
+
const result2 = yearPillar(adapter, dt2);
|
|
71
|
+
expect(result1.pillar).toBe(result2.pillar);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe("monthPillar", () => {
|
|
75
|
+
it("should calculate month pillar for test case (1992-10-12)", () => {
|
|
76
|
+
const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12 }, { zone: "Asia/Seoul" });
|
|
77
|
+
const result = monthPillar(adapter, dt);
|
|
78
|
+
expect(result.pillar).toBe("庚戌");
|
|
79
|
+
});
|
|
80
|
+
it("should return valid stem-branch combination", () => {
|
|
81
|
+
const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12 }, { zone: "Asia/Seoul" });
|
|
82
|
+
const result = monthPillar(adapter, dt);
|
|
83
|
+
expect(result.pillar).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
|
|
84
|
+
});
|
|
85
|
+
it("should include sun longitude in result", () => {
|
|
86
|
+
const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12 }, { zone: "Asia/Seoul" });
|
|
87
|
+
const result = monthPillar(adapter, dt);
|
|
88
|
+
expect(result.sunLonDeg).toBeGreaterThanOrEqual(0);
|
|
89
|
+
expect(result.sunLonDeg).toBeLessThan(360);
|
|
90
|
+
});
|
|
91
|
+
it("should calculate different months correctly", () => {
|
|
92
|
+
const jan = DateTime.fromObject({ year: 1992, month: 1, day: 15 }, { zone: "Asia/Seoul" });
|
|
93
|
+
const jul = DateTime.fromObject({ year: 1992, month: 7, day: 15 }, { zone: "Asia/Seoul" });
|
|
94
|
+
const result1 = monthPillar(adapter, jan);
|
|
95
|
+
const result2 = monthPillar(adapter, jul);
|
|
96
|
+
expect(result1.pillar).not.toBe(result2.pillar);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe("effectiveDayDate", () => {
|
|
100
|
+
it("should return same date for midnight boundary before 23:00", () => {
|
|
101
|
+
const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12, hour: 22, minute: 30 }, { zone: "Asia/Seoul" });
|
|
102
|
+
const result = effectiveDayDate(adapter, dt, { dayBoundary: "midnight" });
|
|
103
|
+
expect(result.year).toBe(1992);
|
|
104
|
+
expect(result.month).toBe(10);
|
|
105
|
+
expect(result.day).toBe(12);
|
|
106
|
+
});
|
|
107
|
+
it("should advance date for zi23 boundary after 23:00", () => {
|
|
108
|
+
const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12, hour: 23, minute: 30 }, { zone: "Asia/Seoul" });
|
|
109
|
+
const result = effectiveDayDate(adapter, dt, { dayBoundary: "zi23" });
|
|
110
|
+
expect(result.year).toBe(1992);
|
|
111
|
+
expect(result.month).toBe(10);
|
|
112
|
+
expect(result.day).toBe(13);
|
|
113
|
+
});
|
|
114
|
+
it("should not advance date for zi23 boundary before 23:00", () => {
|
|
115
|
+
const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12, hour: 22, minute: 59 }, { zone: "Asia/Seoul" });
|
|
116
|
+
const result = effectiveDayDate(adapter, dt, { dayBoundary: "zi23" });
|
|
117
|
+
expect(result.year).toBe(1992);
|
|
118
|
+
expect(result.month).toBe(10);
|
|
119
|
+
expect(result.day).toBe(12);
|
|
120
|
+
});
|
|
121
|
+
it("should handle month boundary with zi23", () => {
|
|
122
|
+
const dt = DateTime.fromObject({ year: 1992, month: 10, day: 31, hour: 23, minute: 30 }, { zone: "Asia/Seoul" });
|
|
123
|
+
const result = effectiveDayDate(adapter, dt, { dayBoundary: "zi23" });
|
|
124
|
+
expect(result.year).toBe(1992);
|
|
125
|
+
expect(result.month).toBe(11);
|
|
126
|
+
expect(result.day).toBe(1);
|
|
127
|
+
});
|
|
128
|
+
it("should apply mean solar time correction when enabled", () => {
|
|
129
|
+
const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12, hour: 23, minute: 50 }, { zone: "Asia/Seoul" });
|
|
130
|
+
const result = effectiveDayDate(adapter, dt, {
|
|
131
|
+
dayBoundary: "zi23",
|
|
132
|
+
longitudeDeg: 126.9,
|
|
133
|
+
useMeanSolarTimeForBoundary: true,
|
|
134
|
+
});
|
|
135
|
+
expect(result.day).toBe(13);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe("hourPillar", () => {
|
|
139
|
+
it("should calculate hour pillar for test case (2000-01-01 18:00)", () => {
|
|
140
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
141
|
+
const result = hourPillar(adapter, dt);
|
|
142
|
+
expect(result.pillar).toBe("辛酉");
|
|
143
|
+
});
|
|
144
|
+
it("should return valid stem-branch combination", () => {
|
|
145
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
146
|
+
const result = hourPillar(adapter, dt);
|
|
147
|
+
expect(result.pillar).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
|
|
148
|
+
});
|
|
149
|
+
it("should calculate different hours correctly", () => {
|
|
150
|
+
const morning = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 8, minute: 0 }, { zone: "Asia/Seoul" });
|
|
151
|
+
const evening = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 20, minute: 0 }, { zone: "Asia/Seoul" });
|
|
152
|
+
const result1 = hourPillar(adapter, morning);
|
|
153
|
+
const result2 = hourPillar(adapter, evening);
|
|
154
|
+
expect(result1.pillar).not.toBe(result2.pillar);
|
|
155
|
+
});
|
|
156
|
+
it("should apply mean solar time correction when enabled", () => {
|
|
157
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
158
|
+
const withCorrection = hourPillar(adapter, dt, {
|
|
159
|
+
longitudeDeg: 126.9,
|
|
160
|
+
useMeanSolarTimeForHour: true,
|
|
161
|
+
});
|
|
162
|
+
const withoutCorrection = hourPillar(adapter, dt, {
|
|
163
|
+
longitudeDeg: 126.9,
|
|
164
|
+
useMeanSolarTimeForHour: false,
|
|
165
|
+
});
|
|
166
|
+
expect(withCorrection.adjustedDt).not.toBe(withoutCorrection.adjustedDt);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe("getFourPillars - Preset A", () => {
|
|
170
|
+
it("should calculate four pillars with preset A", () => {
|
|
171
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
172
|
+
const result = getFourPillars(adapter, dt, {
|
|
173
|
+
longitudeDeg: 126.9,
|
|
174
|
+
preset: presetA,
|
|
175
|
+
});
|
|
176
|
+
expect(result.year).toBe("己卯");
|
|
177
|
+
expect(result.month).toBe("丙子");
|
|
178
|
+
expect(result.day).toBe("庚辰");
|
|
179
|
+
expect(result.hour).toBe("辛酉");
|
|
180
|
+
});
|
|
181
|
+
it("should include metadata", () => {
|
|
182
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
183
|
+
const result = getFourPillars(adapter, dt, {
|
|
184
|
+
longitudeDeg: 126.9,
|
|
185
|
+
preset: presetA,
|
|
186
|
+
});
|
|
187
|
+
expect(result.meta.solarYearUsed).toBe(1999);
|
|
188
|
+
expect(result.meta.sunLonDeg).toBeGreaterThan(0);
|
|
189
|
+
expect(result.meta.effectiveDayDate).toEqual({ year: 2000, month: 1, day: 1 });
|
|
190
|
+
expect(result.meta.preset).toEqual(presetA);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe("getFourPillars - Preset B", () => {
|
|
194
|
+
it("should calculate four pillars with preset B", () => {
|
|
195
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
196
|
+
const result = getFourPillars(adapter, dt, {
|
|
197
|
+
longitudeDeg: 126.9,
|
|
198
|
+
preset: presetB,
|
|
199
|
+
});
|
|
200
|
+
expect(result.year).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
|
|
201
|
+
expect(result.month).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
|
|
202
|
+
expect(result.day).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
|
|
203
|
+
expect(result.hour).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
|
|
204
|
+
});
|
|
205
|
+
it("should apply solar time corrections with preset B", () => {
|
|
206
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
207
|
+
const resultA = getFourPillars(adapter, dt, {
|
|
208
|
+
longitudeDeg: 126.9,
|
|
209
|
+
preset: presetA,
|
|
210
|
+
});
|
|
211
|
+
const resultB = getFourPillars(adapter, dt, {
|
|
212
|
+
longitudeDeg: 126.9,
|
|
213
|
+
preset: presetB,
|
|
214
|
+
});
|
|
215
|
+
expect(resultA).not.toEqual(resultB);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe("getFourPillars - Edge cases", () => {
|
|
219
|
+
it("should handle date near 23:00 boundary", () => {
|
|
220
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 23, minute: 30 }, { zone: "Asia/Seoul" });
|
|
221
|
+
const resultA = getFourPillars(adapter, dt, {
|
|
222
|
+
longitudeDeg: 126.9,
|
|
223
|
+
preset: presetA,
|
|
224
|
+
});
|
|
225
|
+
const _resultB = getFourPillars(adapter, dt, {
|
|
226
|
+
longitudeDeg: 126.9,
|
|
227
|
+
preset: presetB,
|
|
228
|
+
});
|
|
229
|
+
expect(resultA.meta.effectiveDayDate.day).toBe(1);
|
|
230
|
+
});
|
|
231
|
+
it("should require longitudeDeg parameter", () => {
|
|
232
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
233
|
+
expect(() => {
|
|
234
|
+
getFourPillars(adapter, dt, {
|
|
235
|
+
longitudeDeg: undefined,
|
|
236
|
+
preset: presetA,
|
|
237
|
+
});
|
|
238
|
+
}).toThrow("longitudeDeg is required");
|
|
239
|
+
});
|
|
240
|
+
it("should handle leap year dates", () => {
|
|
241
|
+
const dt = DateTime.fromObject({ year: 2000, month: 2, day: 29 }, { zone: "UTC" });
|
|
242
|
+
const result = getFourPillars(adapter, dt, {
|
|
243
|
+
longitudeDeg: 0,
|
|
244
|
+
preset: presetA,
|
|
245
|
+
});
|
|
246
|
+
expect(result.year).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
|
|
247
|
+
expect(result.month).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
|
|
248
|
+
expect(result.day).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
|
|
249
|
+
expect(result.hour).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
|
|
250
|
+
});
|
|
251
|
+
it("should handle year boundary", () => {
|
|
252
|
+
const dt = DateTime.fromObject({ year: 1999, month: 12, day: 31, hour: 23, minute: 59 }, { zone: "UTC" });
|
|
253
|
+
const result = getFourPillars(adapter, dt, {
|
|
254
|
+
longitudeDeg: 0,
|
|
255
|
+
preset: presetA,
|
|
256
|
+
});
|
|
257
|
+
expect(result.meta.effectiveDayDate.year).toBe(1999);
|
|
258
|
+
expect(result.meta.effectiveDayDate.month).toBe(12);
|
|
259
|
+
expect(result.meta.effectiveDayDate.day).toBe(31);
|
|
260
|
+
});
|
|
261
|
+
it("should handle different timezones", () => {
|
|
262
|
+
const seoul = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
263
|
+
const tokyo = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Tokyo" });
|
|
264
|
+
const resultSeoul = getFourPillars(adapter, seoul, {
|
|
265
|
+
longitudeDeg: 126.9,
|
|
266
|
+
preset: presetA,
|
|
267
|
+
});
|
|
268
|
+
const resultTokyo = getFourPillars(adapter, tokyo, {
|
|
269
|
+
longitudeDeg: 139.7,
|
|
270
|
+
preset: presetA,
|
|
271
|
+
});
|
|
272
|
+
expect(resultSeoul.year).toBe(resultTokyo.year);
|
|
273
|
+
expect(resultSeoul.month).toBe(resultTokyo.month);
|
|
274
|
+
expect(resultSeoul.day).toBe(resultTokyo.day);
|
|
275
|
+
});
|
|
276
|
+
it("should handle different longitudes affecting hour pillar", () => {
|
|
277
|
+
const dt = DateTime.fromObject({ year: 1992, month: 10, day: 12, hour: 0, minute: 30 }, { zone: "UTC" });
|
|
278
|
+
const westLong = getFourPillars(adapter, dt, {
|
|
279
|
+
longitudeDeg: -120,
|
|
280
|
+
preset: presetB,
|
|
281
|
+
});
|
|
282
|
+
const eastLong = getFourPillars(adapter, dt, {
|
|
283
|
+
longitudeDeg: 120,
|
|
284
|
+
preset: presetB,
|
|
285
|
+
});
|
|
286
|
+
expect(westLong.hour).not.toBe(eastLong.hour);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"luxon-adapter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/luxon-adapter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
3
|
+
import { createLuxonAdapter } from "@/adapters/luxon";
|
|
4
|
+
describe("Luxon Adapter", () => {
|
|
5
|
+
let adapter;
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
adapter = await createLuxonAdapter();
|
|
8
|
+
});
|
|
9
|
+
describe("Basic date getters", () => {
|
|
10
|
+
it("should get year correctly", () => {
|
|
11
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1 }, { zone: "Asia/Seoul" });
|
|
12
|
+
expect(adapter.getYear(dt)).toBe(2000);
|
|
13
|
+
});
|
|
14
|
+
it("should get month correctly (1-based)", () => {
|
|
15
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1 }, { zone: "Asia/Seoul" });
|
|
16
|
+
expect(adapter.getMonth(dt)).toBe(1);
|
|
17
|
+
});
|
|
18
|
+
it("should get day correctly", () => {
|
|
19
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1 }, { zone: "Asia/Seoul" });
|
|
20
|
+
expect(adapter.getDay(dt)).toBe(1);
|
|
21
|
+
});
|
|
22
|
+
it("should get hour correctly", () => {
|
|
23
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
24
|
+
expect(adapter.getHour(dt)).toBe(18);
|
|
25
|
+
});
|
|
26
|
+
it("should get minute correctly", () => {
|
|
27
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
28
|
+
expect(adapter.getMinute(dt)).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
it("should get second correctly", () => {
|
|
31
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0, second: 30 }, { zone: "Asia/Seoul" });
|
|
32
|
+
expect(adapter.getSecond(dt)).toBe(30);
|
|
33
|
+
});
|
|
34
|
+
it("should get zone name correctly", () => {
|
|
35
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1 }, { zone: "Asia/Seoul" });
|
|
36
|
+
expect(adapter.getZoneName(dt)).toBe("Asia/Seoul");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("Date arithmetic", () => {
|
|
40
|
+
it("should add minutes correctly", () => {
|
|
41
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
42
|
+
const result = adapter.plusMinutes(dt, 30);
|
|
43
|
+
expect(adapter.getMinute(result)).toBe(30);
|
|
44
|
+
expect(adapter.getHour(result)).toBe(18);
|
|
45
|
+
});
|
|
46
|
+
it("should add minutes with hour overflow", () => {
|
|
47
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 23, minute: 45 }, { zone: "Asia/Seoul" });
|
|
48
|
+
const result = adapter.plusMinutes(dt, 30);
|
|
49
|
+
expect(adapter.getHour(result)).toBe(0);
|
|
50
|
+
expect(adapter.getMinute(result)).toBe(15);
|
|
51
|
+
expect(adapter.getDay(result)).toBe(2);
|
|
52
|
+
});
|
|
53
|
+
it("should add days correctly", () => {
|
|
54
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1 }, { zone: "Asia/Seoul" });
|
|
55
|
+
const result = adapter.plusDays(dt, 5);
|
|
56
|
+
expect(adapter.getDay(result)).toBe(6);
|
|
57
|
+
expect(adapter.getMonth(result)).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
it("should add days with month overflow", () => {
|
|
60
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 30 }, { zone: "Asia/Seoul" });
|
|
61
|
+
const result = adapter.plusDays(dt, 5);
|
|
62
|
+
expect(adapter.getDay(result)).toBe(4);
|
|
63
|
+
expect(adapter.getMonth(result)).toBe(2);
|
|
64
|
+
});
|
|
65
|
+
it("should subtract days correctly", () => {
|
|
66
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 10 }, { zone: "Asia/Seoul" });
|
|
67
|
+
const result = adapter.minusDays(dt, 5);
|
|
68
|
+
expect(adapter.getDay(result)).toBe(5);
|
|
69
|
+
expect(adapter.getMonth(result)).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
it("should subtract days with month underflow", () => {
|
|
72
|
+
const dt = DateTime.fromObject({ year: 2000, month: 2, day: 3 }, { zone: "Asia/Seoul" });
|
|
73
|
+
const result = adapter.minusDays(dt, 5);
|
|
74
|
+
expect(adapter.getDay(result)).toBe(29);
|
|
75
|
+
expect(adapter.getMonth(result)).toBe(1);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe("Timezone operations", () => {
|
|
79
|
+
it("should convert to UTC correctly", () => {
|
|
80
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
81
|
+
const utc = adapter.toUTC(dt);
|
|
82
|
+
expect(adapter.getZoneName(utc)).toBe("UTC");
|
|
83
|
+
expect(adapter.getHour(utc)).toBe(9);
|
|
84
|
+
});
|
|
85
|
+
it("should set zone correctly", () => {
|
|
86
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
87
|
+
const ny = adapter.setZone(dt, "America/New_York");
|
|
88
|
+
expect(adapter.getZoneName(ny)).toBe("America/New_York");
|
|
89
|
+
expect(adapter.getHour(ny)).toBe(4);
|
|
90
|
+
});
|
|
91
|
+
it("should preserve instant when setting zone", () => {
|
|
92
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
93
|
+
const ny = adapter.setZone(dt, "America/New_York");
|
|
94
|
+
expect(adapter.toMillis(dt)).toBe(adapter.toMillis(ny));
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe("Conversion methods", () => {
|
|
98
|
+
it("should convert to ISO string", () => {
|
|
99
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
100
|
+
const iso = adapter.toISO(dt);
|
|
101
|
+
expect(iso).toContain("2000-01-01");
|
|
102
|
+
expect(iso).toContain("18:00");
|
|
103
|
+
});
|
|
104
|
+
it("should convert to milliseconds", () => {
|
|
105
|
+
const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
106
|
+
const millis = adapter.toMillis(dt);
|
|
107
|
+
expect(typeof millis).toBe("number");
|
|
108
|
+
expect(millis).toBeGreaterThan(0);
|
|
109
|
+
});
|
|
110
|
+
it("should create date from milliseconds", () => {
|
|
111
|
+
const millis = 946717200000;
|
|
112
|
+
const dt = adapter.fromMillis(millis, "Asia/Seoul");
|
|
113
|
+
expect(adapter.getYear(dt)).toBe(2000);
|
|
114
|
+
expect(adapter.getMonth(dt)).toBe(1);
|
|
115
|
+
expect(adapter.getDay(dt)).toBe(1);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe("UTC date creation", () => {
|
|
119
|
+
it("should create UTC date correctly", () => {
|
|
120
|
+
const dt = adapter.createUTC(2000, 2, 5, 12, 30, 45);
|
|
121
|
+
expect(adapter.getYear(dt)).toBe(2000);
|
|
122
|
+
expect(adapter.getMonth(dt)).toBe(2);
|
|
123
|
+
expect(adapter.getDay(dt)).toBe(5);
|
|
124
|
+
expect(adapter.getHour(dt)).toBe(12);
|
|
125
|
+
expect(adapter.getMinute(dt)).toBe(30);
|
|
126
|
+
expect(adapter.getSecond(dt)).toBe(45);
|
|
127
|
+
expect(adapter.getZoneName(dt)).toBe("UTC");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe("Date comparison", () => {
|
|
131
|
+
it("should compare dates correctly (greater than)", () => {
|
|
132
|
+
const dt1 = DateTime.fromObject({ year: 2000, month: 1, day: 2 }, { zone: "UTC" });
|
|
133
|
+
const dt2 = DateTime.fromObject({ year: 2000, month: 1, day: 1 }, { zone: "UTC" });
|
|
134
|
+
expect(adapter.isGreaterThanOrEqual(dt1, dt2)).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
it("should compare dates correctly (equal)", () => {
|
|
137
|
+
const dt1 = DateTime.fromObject({ year: 2000, month: 1, day: 1 }, { zone: "UTC" });
|
|
138
|
+
const dt2 = DateTime.fromObject({ year: 2000, month: 1, day: 1 }, { zone: "UTC" });
|
|
139
|
+
expect(adapter.isGreaterThanOrEqual(dt1, dt2)).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
it("should compare dates correctly (less than)", () => {
|
|
142
|
+
const dt1 = DateTime.fromObject({ year: 1999, month: 12, day: 31 }, { zone: "UTC" });
|
|
143
|
+
const dt2 = DateTime.fromObject({ year: 2000, month: 1, day: 1 }, { zone: "UTC" });
|
|
144
|
+
expect(adapter.isGreaterThanOrEqual(dt1, dt2)).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe("Edge cases", () => {
|
|
148
|
+
it("should handle leap year correctly", () => {
|
|
149
|
+
const dt = DateTime.fromObject({ year: 2000, month: 2, day: 29 }, { zone: "UTC" });
|
|
150
|
+
expect(adapter.getDay(dt)).toBe(29);
|
|
151
|
+
expect(adapter.getMonth(dt)).toBe(2);
|
|
152
|
+
});
|
|
153
|
+
it("should handle year boundary", () => {
|
|
154
|
+
const dt = DateTime.fromObject({ year: 1999, month: 12, day: 31, hour: 23, minute: 59 }, { zone: "UTC" });
|
|
155
|
+
const result = adapter.plusMinutes(dt, 2);
|
|
156
|
+
expect(adapter.getYear(result)).toBe(2000);
|
|
157
|
+
expect(adapter.getMonth(result)).toBe(1);
|
|
158
|
+
expect(adapter.getDay(result)).toBe(1);
|
|
159
|
+
});
|
|
160
|
+
it("should handle different timezones at same instant", () => {
|
|
161
|
+
const seoul = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
|
|
162
|
+
const utc = adapter.toUTC(seoul);
|
|
163
|
+
expect(adapter.toMillis(seoul)).toBe(adapter.toMillis(utc));
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|