@gracefullight/saju 0.1.1 → 0.3.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.
Files changed (55) hide show
  1. package/README.en.md +314 -28
  2. package/README.md +314 -28
  3. package/dist/__tests__/four-pillars.test.js +52 -40
  4. package/dist/__tests__/luck.test.d.ts +2 -0
  5. package/dist/__tests__/luck.test.d.ts.map +1 -0
  6. package/dist/__tests__/luck.test.js +33 -0
  7. package/dist/__tests__/lunar.test.d.ts +2 -0
  8. package/dist/__tests__/lunar.test.d.ts.map +1 -0
  9. package/dist/__tests__/lunar.test.js +83 -0
  10. package/dist/__tests__/relations.test.d.ts +2 -0
  11. package/dist/__tests__/relations.test.d.ts.map +1 -0
  12. package/dist/__tests__/relations.test.js +90 -0
  13. package/dist/__tests__/saju.test.d.ts +2 -0
  14. package/dist/__tests__/saju.test.d.ts.map +1 -0
  15. package/dist/__tests__/saju.test.js +133 -0
  16. package/dist/__tests__/solar-terms.test.d.ts +2 -0
  17. package/dist/__tests__/solar-terms.test.d.ts.map +1 -0
  18. package/dist/__tests__/solar-terms.test.js +121 -0
  19. package/dist/__tests__/strength.test.d.ts +2 -0
  20. package/dist/__tests__/strength.test.d.ts.map +1 -0
  21. package/dist/__tests__/strength.test.js +44 -0
  22. package/dist/__tests__/ten-gods.test.d.ts +2 -0
  23. package/dist/__tests__/ten-gods.test.d.ts.map +1 -0
  24. package/dist/__tests__/ten-gods.test.js +119 -0
  25. package/dist/__tests__/yongshen.test.d.ts +2 -0
  26. package/dist/__tests__/yongshen.test.d.ts.map +1 -0
  27. package/dist/__tests__/yongshen.test.js +62 -0
  28. package/dist/core/four-pillars.d.ts +2 -0
  29. package/dist/core/four-pillars.d.ts.map +1 -1
  30. package/dist/core/four-pillars.js +7 -4
  31. package/dist/core/luck.d.ts +41 -0
  32. package/dist/core/luck.d.ts.map +1 -0
  33. package/dist/core/luck.js +96 -0
  34. package/dist/core/lunar.d.ts +13 -0
  35. package/dist/core/lunar.d.ts.map +1 -0
  36. package/dist/core/lunar.js +24 -0
  37. package/dist/core/relations.d.ts +94 -0
  38. package/dist/core/relations.d.ts.map +1 -0
  39. package/dist/core/relations.js +305 -0
  40. package/dist/core/solar-terms.d.ts +155 -0
  41. package/dist/core/solar-terms.d.ts.map +1 -0
  42. package/dist/core/solar-terms.js +266 -0
  43. package/dist/core/strength.d.ts +18 -0
  44. package/dist/core/strength.d.ts.map +1 -0
  45. package/dist/core/strength.js +255 -0
  46. package/dist/core/ten-gods.d.ts +130 -0
  47. package/dist/core/ten-gods.d.ts.map +1 -0
  48. package/dist/core/ten-gods.js +335 -0
  49. package/dist/core/yongshen.d.ts +20 -0
  50. package/dist/core/yongshen.d.ts.map +1 -0
  51. package/dist/core/yongshen.js +216 -0
  52. package/dist/index.d.ts +54 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +48 -0
  55. package/package.json +15 -12
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getLunarDate, getSolarDate } from "@/core/lunar";
3
+ describe("Lunar Calendar", () => {
4
+ describe("getLunarDate", () => {
5
+ it("should convert solar date to lunar date (2000-01-01)", () => {
6
+ const result = getLunarDate(2000, 1, 1);
7
+ expect(result.lunarYear).toBe(1999);
8
+ expect(result.lunarMonth).toBe(11);
9
+ expect(result.lunarDay).toBe(25);
10
+ expect(result.isLeapMonth).toBe(false);
11
+ });
12
+ it("should convert solar date to lunar date (1985-05-15)", () => {
13
+ const result = getLunarDate(1985, 5, 15);
14
+ expect(result.lunarYear).toBe(1985);
15
+ expect(result.lunarMonth).toBe(3);
16
+ expect(result.lunarDay).toBe(26);
17
+ expect(result.isLeapMonth).toBe(false);
18
+ });
19
+ it("should handle leap month correctly (2023-03-22 is in leap 2nd month)", () => {
20
+ const result = getLunarDate(2023, 3, 22);
21
+ expect(result.lunarYear).toBe(2023);
22
+ expect(result.lunarMonth).toBe(2);
23
+ expect(result.isLeapMonth).toBe(true);
24
+ });
25
+ it("should convert first day of lunar year", () => {
26
+ const result = getLunarDate(2024, 2, 10);
27
+ expect(result.lunarYear).toBe(2024);
28
+ expect(result.lunarMonth).toBe(1);
29
+ expect(result.lunarDay).toBe(1);
30
+ expect(result.isLeapMonth).toBe(false);
31
+ });
32
+ });
33
+ describe("getSolarDate", () => {
34
+ it("should convert lunar date to solar date", () => {
35
+ const result = getSolarDate(1999, 11, 25, false);
36
+ expect(result.year).toBe(2000);
37
+ expect(result.month).toBe(1);
38
+ expect(result.day).toBe(1);
39
+ });
40
+ it("should convert lunar date (1985-03-26) to solar date", () => {
41
+ const result = getSolarDate(1985, 3, 26, false);
42
+ expect(result.year).toBe(1985);
43
+ expect(result.month).toBe(5);
44
+ expect(result.day).toBe(15);
45
+ });
46
+ it("should handle leap month in reverse conversion", () => {
47
+ const result = getSolarDate(2023, 2, 1, true);
48
+ expect(result.year).toBe(2023);
49
+ expect(result.month).toBe(3);
50
+ expect(result.day).toBe(22);
51
+ });
52
+ it("should convert first day of lunar year 2024", () => {
53
+ const result = getSolarDate(2024, 1, 1, false);
54
+ expect(result.year).toBe(2024);
55
+ expect(result.month).toBe(2);
56
+ expect(result.day).toBe(10);
57
+ });
58
+ });
59
+ describe("round-trip conversion", () => {
60
+ it("should convert solar -> lunar -> solar correctly", () => {
61
+ const original = { year: 2000, month: 6, day: 15 };
62
+ const lunar = getLunarDate(original.year, original.month, original.day);
63
+ const backToSolar = getSolarDate(lunar.lunarYear, lunar.lunarMonth, lunar.lunarDay, lunar.isLeapMonth);
64
+ expect(backToSolar.year).toBe(original.year);
65
+ expect(backToSolar.month).toBe(original.month);
66
+ expect(backToSolar.day).toBe(original.day);
67
+ });
68
+ it("should handle various dates in round-trip", () => {
69
+ const testDates = [
70
+ { year: 1990, month: 3, day: 20 },
71
+ { year: 2010, month: 8, day: 1 },
72
+ { year: 2020, month: 12, day: 31 },
73
+ ];
74
+ for (const date of testDates) {
75
+ const lunar = getLunarDate(date.year, date.month, date.day);
76
+ const backToSolar = getSolarDate(lunar.lunarYear, lunar.lunarMonth, lunar.lunarDay, lunar.isLeapMonth);
77
+ expect(backToSolar.year).toBe(date.year);
78
+ expect(backToSolar.month).toBe(date.month);
79
+ expect(backToSolar.day).toBe(date.day);
80
+ }
81
+ });
82
+ });
83
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=relations.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relations.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/relations.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { analyzeRelations, findStemCombination, findBranchClash, findBranchSixCombination, } from "@/core/relations";
3
+ describe("relations", () => {
4
+ describe("findStemCombination", () => {
5
+ it("finds 甲己 combination (earth)", () => {
6
+ const result = findStemCombination("甲", "己");
7
+ expect(result).not.toBeNull();
8
+ expect(result?.resultElement).toBe("earth");
9
+ });
10
+ it("finds 乙庚 combination (metal)", () => {
11
+ const result = findStemCombination("乙", "庚");
12
+ expect(result).not.toBeNull();
13
+ expect(result?.resultElement).toBe("metal");
14
+ });
15
+ it("returns null for non-combining stems", () => {
16
+ const result = findStemCombination("甲", "乙");
17
+ expect(result).toBeNull();
18
+ });
19
+ });
20
+ describe("findBranchClash", () => {
21
+ it("identifies 子午 clash", () => {
22
+ expect(findBranchClash("子", "午")).toBe(true);
23
+ expect(findBranchClash("午", "子")).toBe(true);
24
+ });
25
+ it("identifies 寅申 clash", () => {
26
+ expect(findBranchClash("寅", "申")).toBe(true);
27
+ });
28
+ it("returns false for non-clashing branches", () => {
29
+ expect(findBranchClash("子", "丑")).toBe(false);
30
+ });
31
+ });
32
+ describe("findBranchSixCombination", () => {
33
+ it("finds 子丑 combination (earth)", () => {
34
+ const result = findBranchSixCombination("子", "丑");
35
+ expect(result).not.toBeNull();
36
+ expect(result?.resultElement).toBe("earth");
37
+ });
38
+ it("finds 寅亥 combination (wood)", () => {
39
+ const result = findBranchSixCombination("寅", "亥");
40
+ expect(result).not.toBeNull();
41
+ expect(result?.resultElement).toBe("wood");
42
+ });
43
+ it("returns null for non-combining branches", () => {
44
+ const result = findBranchSixCombination("子", "寅");
45
+ expect(result).toBeNull();
46
+ });
47
+ });
48
+ describe("analyzeRelations", () => {
49
+ it("finds stem combinations in four pillars", () => {
50
+ const result = analyzeRelations("甲子", "己丑", "丙寅", "辛亥");
51
+ const stemCombos = result.combinations.filter((c) => c.type === "천간합");
52
+ expect(stemCombos.length).toBeGreaterThan(0);
53
+ });
54
+ it("finds branch clashes in four pillars", () => {
55
+ const result = analyzeRelations("甲子", "庚午", "丙寅", "壬申");
56
+ expect(result.clashes.length).toBe(2);
57
+ });
58
+ it("finds 삼합 (triple combination)", () => {
59
+ const result = analyzeRelations("甲寅", "丙午", "戊戌", "庚子");
60
+ const tripleCombos = result.combinations.filter((c) => c.type === "삼합");
61
+ expect(tripleCombos.length).toBeGreaterThan(0);
62
+ expect(tripleCombos[0].resultElement).toBe("fire");
63
+ });
64
+ it("finds 육합 (six combination)", () => {
65
+ const result = analyzeRelations("甲子", "乙丑", "丙寅", "丁卯");
66
+ const sixCombos = result.combinations.filter((c) => c.type === "육합");
67
+ expect(sixCombos.length).toBeGreaterThan(0);
68
+ });
69
+ it("finds 형 (punishment)", () => {
70
+ const result = analyzeRelations("甲寅", "丙巳", "戊申", "庚子");
71
+ expect(result.punishments.length).toBeGreaterThan(0);
72
+ expect(result.punishments[0].punishmentType).toBe("무은지형");
73
+ });
74
+ it("includes transformStatus and transformReason for combinations", () => {
75
+ const result = analyzeRelations("甲子", "己丑", "丙寅", "辛亥");
76
+ const stemCombos = result.combinations.filter((c) => c.type === "천간합");
77
+ expect(stemCombos.length).toBeGreaterThan(0);
78
+ expect(stemCombos[0]).toHaveProperty("transformStatus");
79
+ expect(stemCombos[0]).toHaveProperty("transformReason");
80
+ });
81
+ it("aggregates all relations", () => {
82
+ const result = analyzeRelations("甲子", "庚午", "丙寅", "壬申");
83
+ expect(result.all.length).toBe(result.combinations.length +
84
+ result.clashes.length +
85
+ result.harms.length +
86
+ result.punishments.length +
87
+ result.destructions.length);
88
+ });
89
+ });
90
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=saju.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"saju.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/saju.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,133 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { DateTime } from "luxon";
3
+ import { createLuxonAdapter } from "@/adapters/luxon";
4
+ import { getSaju, STANDARD_PRESET } from "@/index";
5
+ describe("getSaju integration", () => {
6
+ it("returns complete saju analysis with all required fields", async () => {
7
+ const adapter = await createLuxonAdapter();
8
+ const dt = DateTime.fromObject({ year: 1990, month: 2, day: 1, hour: 12, minute: 10 }, { zone: "Asia/Seoul" });
9
+ const result = getSaju(adapter, dt, {
10
+ longitudeDeg: 126.9778,
11
+ gender: "male",
12
+ preset: STANDARD_PRESET,
13
+ });
14
+ expect(result.pillars.year).toBeDefined();
15
+ expect(result.pillars.month).toBeDefined();
16
+ expect(result.pillars.day).toBeDefined();
17
+ expect(result.pillars.hour).toBeDefined();
18
+ expect(result.tenGods).toBeDefined();
19
+ expect(result.strength).toBeDefined();
20
+ expect(result.relations).toBeDefined();
21
+ expect(result.yongShen).toBeDefined();
22
+ expect(result.lunar).toBeDefined();
23
+ expect(result.solarTerms).toBeDefined();
24
+ expect(result.majorLuck).toBeDefined();
25
+ expect(result.yearlyLuck).toBeDefined();
26
+ });
27
+ it("includes solar terms with current and next term info", async () => {
28
+ const adapter = await createLuxonAdapter();
29
+ const dt = DateTime.fromObject({ year: 2024, month: 1, day: 15, hour: 12 }, { zone: "Asia/Seoul" });
30
+ const result = getSaju(adapter, dt, {
31
+ longitudeDeg: 126.9778,
32
+ gender: "female",
33
+ preset: STANDARD_PRESET,
34
+ });
35
+ expect(result.solarTerms.current.name).toBe("소한");
36
+ expect(result.solarTerms.next.name).toBe("대한");
37
+ expect(result.solarTerms.daysSinceCurrent).toBeGreaterThanOrEqual(0);
38
+ expect(result.solarTerms.daysUntilNext).toBeGreaterThan(0);
39
+ expect(result.solarTerms.currentDate).toBeDefined();
40
+ expect(result.solarTerms.nextDate).toBeDefined();
41
+ });
42
+ it("includes major luck by default", async () => {
43
+ const adapter = await createLuxonAdapter();
44
+ const dt = DateTime.fromObject({ year: 1990, month: 2, day: 1, hour: 12, minute: 10 }, { zone: "Asia/Seoul" });
45
+ const result = getSaju(adapter, dt, {
46
+ longitudeDeg: 126.9778,
47
+ gender: "male",
48
+ preset: STANDARD_PRESET,
49
+ });
50
+ expect(result.majorLuck).toBeDefined();
51
+ expect(result.majorLuck.pillars.length).toBeGreaterThan(0);
52
+ expect(result.majorLuck.startAge).toBeGreaterThanOrEqual(0);
53
+ });
54
+ it("includes yearly luck by default with custom range", async () => {
55
+ const adapter = await createLuxonAdapter();
56
+ const dt = DateTime.fromObject({ year: 1990, month: 2, day: 1, hour: 12, minute: 10 }, { zone: "Asia/Seoul" });
57
+ const result = getSaju(adapter, dt, {
58
+ longitudeDeg: 126.9778,
59
+ gender: "female",
60
+ preset: STANDARD_PRESET,
61
+ yearlyLuckRange: { from: 2024, to: 2026 },
62
+ });
63
+ expect(result.yearlyLuck).toBeDefined();
64
+ expect(result.yearlyLuck.length).toBe(3);
65
+ expect(result.yearlyLuck[0].year).toBe(2024);
66
+ expect(result.yearlyLuck[2].year).toBe(2026);
67
+ });
68
+ it("uses default yearly luck range when not specified", async () => {
69
+ const adapter = await createLuxonAdapter();
70
+ const dt = DateTime.fromObject({ year: 1990, month: 2, day: 1, hour: 12, minute: 10 }, { zone: "Asia/Seoul" });
71
+ const result = getSaju(adapter, dt, {
72
+ longitudeDeg: 126.9778,
73
+ gender: "male",
74
+ currentYear: 2024,
75
+ });
76
+ expect(result.yearlyLuck.length).toBe(16);
77
+ expect(result.yearlyLuck[0].year).toBe(2019);
78
+ expect(result.yearlyLuck[15].year).toBe(2034);
79
+ });
80
+ it("calculates known test case correctly", async () => {
81
+ const adapter = await createLuxonAdapter();
82
+ const dt = DateTime.fromObject({ year: 1990, month: 2, day: 1, hour: 12, minute: 10 }, { zone: "Asia/Seoul" });
83
+ const result = getSaju(adapter, dt, {
84
+ longitudeDeg: 126.9778,
85
+ gender: "male",
86
+ preset: STANDARD_PRESET,
87
+ });
88
+ expect(result.pillars.year).toBe("己巳");
89
+ expect(result.pillars.month).toBe("丁丑");
90
+ expect(result.pillars.day).toBe("丁酉");
91
+ expect(result.pillars.hour).toBe("丙午");
92
+ });
93
+ it("identifies day master in ten gods analysis", async () => {
94
+ const adapter = await createLuxonAdapter();
95
+ const dt = DateTime.fromObject({ year: 1990, month: 2, day: 1, hour: 12, minute: 10 }, { zone: "Asia/Seoul" });
96
+ const result = getSaju(adapter, dt, {
97
+ longitudeDeg: 126.9778,
98
+ gender: "female",
99
+ preset: STANDARD_PRESET,
100
+ });
101
+ expect(result.tenGods.dayMaster).toBe("丁");
102
+ expect(result.tenGods.day.stem.tenGod).toBe("일간");
103
+ });
104
+ it("calculates major luck start age based on actual solar terms (not fixed 10)", async () => {
105
+ const adapter = await createLuxonAdapter();
106
+ const dt = DateTime.fromObject({ year: 1990, month: 2, day: 1, hour: 12, minute: 10 }, { zone: "Asia/Seoul" });
107
+ const result = getSaju(adapter, dt, {
108
+ longitudeDeg: 126.9778,
109
+ gender: "male",
110
+ preset: STANDARD_PRESET,
111
+ });
112
+ expect(result.majorLuck.startAge).not.toBe(10);
113
+ expect(result.majorLuck.startAgeDetail).toBeDefined();
114
+ expect(result.majorLuck.startAgeDetail.years).toBeGreaterThanOrEqual(0);
115
+ expect(result.majorLuck.startAgeDetail.months).toBeGreaterThanOrEqual(0);
116
+ expect(result.majorLuck.daysToTerm).toBeGreaterThan(0);
117
+ });
118
+ it("includes Jie (節) solar term info for major luck calculation", async () => {
119
+ const adapter = await createLuxonAdapter();
120
+ const dt = DateTime.fromObject({ year: 2024, month: 3, day: 15, hour: 12 }, { zone: "Asia/Seoul" });
121
+ const result = getSaju(adapter, dt, {
122
+ longitudeDeg: 126.9778,
123
+ gender: "female",
124
+ preset: STANDARD_PRESET,
125
+ });
126
+ expect(result.solarTerms.prevJie).toBeDefined();
127
+ expect(result.solarTerms.nextJie).toBeDefined();
128
+ expect(result.solarTerms.prevJieMillis).toBeGreaterThan(0);
129
+ expect(result.solarTerms.nextJieMillis).toBeGreaterThan(0);
130
+ expect(result.solarTerms.prevJieDate).toBeDefined();
131
+ expect(result.solarTerms.nextJieDate).toBeDefined();
132
+ });
133
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=solar-terms.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solar-terms.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/solar-terms.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,121 @@
1
+ import { DateTime } from "luxon";
2
+ import { describe, expect, it } from "vitest";
3
+ import { createLuxonAdapter } from "@/adapters/luxon";
4
+ import { analyzeSolarTerms, getSolarTermsForYear, SOLAR_TERMS } from "@/core/solar-terms";
5
+ describe("solar-terms", async () => {
6
+ const adapter = await createLuxonAdapter();
7
+ describe("SOLAR_TERMS", () => {
8
+ it("should have 24 terms", () => {
9
+ expect(SOLAR_TERMS).toHaveLength(24);
10
+ });
11
+ it("should have terms at 15-degree intervals", () => {
12
+ for (let i = 0; i < SOLAR_TERMS.length; i++) {
13
+ const expectedLon = (285 + i * 15) % 360;
14
+ expect(SOLAR_TERMS[i].longitude).toBe(expectedLon);
15
+ }
16
+ });
17
+ it("should start with 소한 at 285°", () => {
18
+ expect(SOLAR_TERMS[0]).toEqual({ name: "소한", hanja: "小寒", longitude: 285 });
19
+ });
20
+ it("should have 입춘 at 315°", () => {
21
+ const ipchun = SOLAR_TERMS.find((t) => t.name === "입춘");
22
+ expect(ipchun).toEqual({ name: "입춘", hanja: "立春", longitude: 315 });
23
+ });
24
+ });
25
+ describe("analyzeSolarTerms", () => {
26
+ it("should return current and next solar terms for mid-January", () => {
27
+ const dt = DateTime.fromObject({ year: 2024, month: 1, day: 15 }, { zone: "Asia/Seoul" });
28
+ const result = analyzeSolarTerms(adapter, dt);
29
+ expect(result.current.name).toBe("소한");
30
+ expect(result.next.name).toBe("대한");
31
+ expect(result.daysSinceCurrent).toBeGreaterThanOrEqual(0);
32
+ expect(result.daysUntilNext).toBeGreaterThan(0);
33
+ });
34
+ it("should return current and next solar terms for early February", () => {
35
+ const dt = DateTime.fromObject({ year: 2024, month: 2, day: 3 }, { zone: "Asia/Seoul" });
36
+ const result = analyzeSolarTerms(adapter, dt);
37
+ expect(result.current.name).toBe("대한");
38
+ expect(result.next.name).toBe("입춘");
39
+ });
40
+ it("should return current and next solar terms for summer solstice period", () => {
41
+ const dt = DateTime.fromObject({ year: 2024, month: 6, day: 25 }, { zone: "Asia/Seoul" });
42
+ const result = analyzeSolarTerms(adapter, dt);
43
+ expect(result.current.name).toBe("하지");
44
+ expect(result.next.name).toBe("소서");
45
+ });
46
+ it("should return current and next solar terms for winter solstice period", () => {
47
+ const dt = DateTime.fromObject({ year: 2024, month: 12, day: 25 }, { zone: "Asia/Seoul" });
48
+ const result = analyzeSolarTerms(adapter, dt);
49
+ expect(result.current.name).toBe("동지");
50
+ expect(result.next.name).toBe("소한");
51
+ });
52
+ it("should calculate days since current term correctly", () => {
53
+ const dt = DateTime.fromObject({ year: 2024, month: 3, day: 25 }, { zone: "Asia/Seoul" });
54
+ const result = analyzeSolarTerms(adapter, dt);
55
+ expect(result.current.name).toBe("춘분");
56
+ expect(result.daysSinceCurrent).toBeGreaterThanOrEqual(0);
57
+ expect(result.daysSinceCurrent).toBeLessThan(16);
58
+ });
59
+ it("should calculate days until next term correctly", () => {
60
+ const dt = DateTime.fromObject({ year: 2024, month: 4, day: 1 }, { zone: "Asia/Seoul" });
61
+ const result = analyzeSolarTerms(adapter, dt);
62
+ expect(result.next.name).toBe("청명");
63
+ expect(result.daysUntilNext).toBeGreaterThan(0);
64
+ expect(result.daysUntilNext).toBeLessThan(16);
65
+ });
66
+ it("should include date information for current and next terms", () => {
67
+ const dt = DateTime.fromObject({ year: 2024, month: 5, day: 10 }, { zone: "Asia/Seoul" });
68
+ const result = analyzeSolarTerms(adapter, dt);
69
+ expect(result.currentDate).toHaveProperty("year");
70
+ expect(result.currentDate).toHaveProperty("month");
71
+ expect(result.currentDate).toHaveProperty("day");
72
+ expect(result.currentDate).toHaveProperty("hour");
73
+ expect(result.currentDate).toHaveProperty("minute");
74
+ expect(result.nextDate).toHaveProperty("year");
75
+ expect(result.nextDate).toHaveProperty("month");
76
+ expect(result.nextDate).toHaveProperty("day");
77
+ expect(result.nextDate).toHaveProperty("hour");
78
+ expect(result.nextDate).toHaveProperty("minute");
79
+ });
80
+ });
81
+ describe("getSolarTermsForYear", () => {
82
+ it("should return 24 terms for a year", () => {
83
+ const terms = getSolarTermsForYear(adapter, 2024, "Asia/Seoul");
84
+ expect(terms).toHaveLength(24);
85
+ });
86
+ it("should have all terms in order", () => {
87
+ const terms = getSolarTermsForYear(adapter, 2024, "Asia/Seoul");
88
+ const names = terms.map((t) => t.term.name);
89
+ expect(names[0]).toBe("소한");
90
+ expect(names[2]).toBe("입춘");
91
+ expect(names[5]).toBe("춘분");
92
+ expect(names[11]).toBe("하지");
93
+ expect(names[17]).toBe("추분");
94
+ expect(names[23]).toBe("동지");
95
+ });
96
+ it("should have dates in chronological order within months", () => {
97
+ const terms = getSolarTermsForYear(adapter, 2024, "Asia/Seoul");
98
+ for (const term of terms) {
99
+ expect(term.date.year).toBe(2024);
100
+ expect(term.date.month).toBeGreaterThanOrEqual(1);
101
+ expect(term.date.month).toBeLessThanOrEqual(12);
102
+ expect(term.date.day).toBeGreaterThanOrEqual(1);
103
+ expect(term.date.day).toBeLessThanOrEqual(31);
104
+ }
105
+ });
106
+ it("should have 입춘 around February 4", () => {
107
+ const terms = getSolarTermsForYear(adapter, 2024, "Asia/Seoul");
108
+ const ipchun = terms.find((t) => t.term.name === "입춘");
109
+ expect(ipchun?.date.month).toBe(2);
110
+ expect(ipchun?.date.day).toBeGreaterThanOrEqual(3);
111
+ expect(ipchun?.date.day).toBeLessThanOrEqual(5);
112
+ });
113
+ it("should have 하지 around June 21", () => {
114
+ const terms = getSolarTermsForYear(adapter, 2024, "Asia/Seoul");
115
+ const haji = terms.find((t) => t.term.name === "하지");
116
+ expect(haji?.date.month).toBe(6);
117
+ expect(haji?.date.day).toBeGreaterThanOrEqual(20);
118
+ expect(haji?.date.day).toBeLessThanOrEqual(22);
119
+ });
120
+ });
121
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=strength.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strength.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/strength.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { analyzeStrength } from "@/core/strength";
3
+ describe("strength", () => {
4
+ describe("analyzeStrength", () => {
5
+ it("returns a strength level", () => {
6
+ const result = analyzeStrength("甲子", "丙寅", "甲辰", "乙亥");
7
+ expect(result.level).toBeDefined();
8
+ expect(result.score).toBeDefined();
9
+ expect(result.factors).toBeDefined();
10
+ expect(result.description).toBeDefined();
11
+ });
12
+ it("identifies 득령 when month supports day master", () => {
13
+ // 甲木 day master in 寅月 (wood month) = 득령 (1.0 multiplier)
14
+ const result = analyzeStrength("甲子", "丙寅", "甲辰", "乙亥");
15
+ expect(result.factors.deukryeong).toBeGreaterThanOrEqual(0.7);
16
+ });
17
+ it("identifies 실령 when month does not support day master", () => {
18
+ // 甲木 day master in 申月 (metal month) = 실령 (0.1 multiplier)
19
+ const result = analyzeStrength("甲子", "庚申", "甲辰", "乙亥");
20
+ expect(result.factors.deukryeong).toBeLessThanOrEqual(0.3);
21
+ });
22
+ it("calculates 득지 from day and hour branches", () => {
23
+ const result = analyzeStrength("甲子", "丙寅", "甲寅", "甲寅");
24
+ expect(result.factors.deukji).toBeGreaterThan(0);
25
+ });
26
+ it("calculates 득세 from year, month, hour stems", () => {
27
+ const result = analyzeStrength("甲子", "甲寅", "甲辰", "甲寅");
28
+ expect(result.factors.deukse).toBeGreaterThan(0);
29
+ });
30
+ it("returns 신강 for strong day master", () => {
31
+ const result = analyzeStrength("甲子", "甲寅", "甲寅", "甲寅");
32
+ expect(["신강", "태강", "극왕", "중화신강"]).toContain(result.level);
33
+ });
34
+ it("returns 신약 for weak day master", () => {
35
+ const result = analyzeStrength("庚申", "庚申", "甲申", "庚申");
36
+ expect(["신약", "태약", "극약", "중화신약"]).toContain(result.level);
37
+ });
38
+ it("includes description with day master info", () => {
39
+ const result = analyzeStrength("甲子", "丙寅", "甲辰", "乙亥");
40
+ expect(result.description).toContain("甲");
41
+ expect(result.description).toContain("wood");
42
+ });
43
+ });
44
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ten-gods.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ten-gods.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ten-gods.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { getTenGod, getStemElement, getStemPolarity, getBranchElement, getHiddenStems, analyzeTenGods, countTenGods, countElements, } from "@/core/ten-gods";
3
+ describe("ten-gods", () => {
4
+ describe("getStemElement", () => {
5
+ it("returns correct element for each stem", () => {
6
+ expect(getStemElement("甲")).toBe("wood");
7
+ expect(getStemElement("乙")).toBe("wood");
8
+ expect(getStemElement("丙")).toBe("fire");
9
+ expect(getStemElement("丁")).toBe("fire");
10
+ expect(getStemElement("戊")).toBe("earth");
11
+ expect(getStemElement("己")).toBe("earth");
12
+ expect(getStemElement("庚")).toBe("metal");
13
+ expect(getStemElement("辛")).toBe("metal");
14
+ expect(getStemElement("壬")).toBe("water");
15
+ expect(getStemElement("癸")).toBe("water");
16
+ });
17
+ });
18
+ describe("getStemPolarity", () => {
19
+ it("returns correct polarity for each stem", () => {
20
+ expect(getStemPolarity("甲")).toBe("yang");
21
+ expect(getStemPolarity("乙")).toBe("yin");
22
+ expect(getStemPolarity("丙")).toBe("yang");
23
+ expect(getStemPolarity("丁")).toBe("yin");
24
+ });
25
+ });
26
+ describe("getBranchElement", () => {
27
+ it("returns correct element for each branch", () => {
28
+ expect(getBranchElement("子")).toBe("water");
29
+ expect(getBranchElement("丑")).toBe("earth");
30
+ expect(getBranchElement("寅")).toBe("wood");
31
+ expect(getBranchElement("卯")).toBe("wood");
32
+ expect(getBranchElement("午")).toBe("fire");
33
+ expect(getBranchElement("酉")).toBe("metal");
34
+ });
35
+ });
36
+ describe("getHiddenStems", () => {
37
+ it("returns correct hidden stems for each branch", () => {
38
+ expect(getHiddenStems("子")).toEqual(["癸"]);
39
+ expect(getHiddenStems("丑")).toEqual(["己", "癸", "辛"]);
40
+ expect(getHiddenStems("寅")).toEqual(["甲", "丙", "戊"]);
41
+ expect(getHiddenStems("卯")).toEqual(["乙"]);
42
+ expect(getHiddenStems("午")).toEqual(["丁", "己"]);
43
+ });
44
+ });
45
+ describe("getTenGod", () => {
46
+ it("correctly identifies 비견 (same element, same polarity)", () => {
47
+ expect(getTenGod("甲", "甲")).toBe("비견");
48
+ expect(getTenGod("乙", "乙")).toBe("비견");
49
+ });
50
+ it("correctly identifies 겁재 (same element, different polarity)", () => {
51
+ expect(getTenGod("甲", "乙")).toBe("겁재");
52
+ expect(getTenGod("乙", "甲")).toBe("겁재");
53
+ });
54
+ it("correctly identifies 식신 (I generate, same polarity)", () => {
55
+ expect(getTenGod("甲", "丙")).toBe("식신");
56
+ expect(getTenGod("丙", "戊")).toBe("식신");
57
+ });
58
+ it("correctly identifies 상관 (I generate, different polarity)", () => {
59
+ expect(getTenGod("甲", "丁")).toBe("상관");
60
+ expect(getTenGod("丙", "己")).toBe("상관");
61
+ });
62
+ it("correctly identifies 편재 (I control, same polarity)", () => {
63
+ expect(getTenGod("甲", "戊")).toBe("편재");
64
+ expect(getTenGod("丙", "庚")).toBe("편재");
65
+ });
66
+ it("correctly identifies 정재 (I control, different polarity)", () => {
67
+ expect(getTenGod("甲", "己")).toBe("정재");
68
+ expect(getTenGod("丙", "辛")).toBe("정재");
69
+ });
70
+ it("correctly identifies 편관 (controls me, same polarity)", () => {
71
+ expect(getTenGod("甲", "庚")).toBe("편관");
72
+ expect(getTenGod("丙", "壬")).toBe("편관");
73
+ });
74
+ it("correctly identifies 정관 (controls me, different polarity)", () => {
75
+ expect(getTenGod("甲", "辛")).toBe("정관");
76
+ expect(getTenGod("丙", "癸")).toBe("정관");
77
+ });
78
+ it("correctly identifies 편인 (generates me, same polarity)", () => {
79
+ expect(getTenGod("甲", "壬")).toBe("편인");
80
+ expect(getTenGod("丙", "甲")).toBe("편인");
81
+ });
82
+ it("correctly identifies 정인 (generates me, different polarity)", () => {
83
+ expect(getTenGod("甲", "癸")).toBe("정인");
84
+ expect(getTenGod("丙", "乙")).toBe("정인");
85
+ });
86
+ });
87
+ describe("analyzeTenGods", () => {
88
+ it("analyzes four pillars correctly", () => {
89
+ const result = analyzeTenGods("甲子", "丙寅", "甲辰", "乙亥");
90
+ expect(result.dayMaster).toBe("甲");
91
+ expect(result.year.stem.tenGod).toBe("비견");
92
+ expect(result.month.stem.tenGod).toBe("식신");
93
+ expect(result.day.stem.tenGod).toBe("일간");
94
+ expect(result.hour.stem.tenGod).toBe("겁재");
95
+ });
96
+ it("includes hidden stems analysis", () => {
97
+ const result = analyzeTenGods("甲子", "丙寅", "甲辰", "乙亥");
98
+ expect(result.year.branch.hiddenStems).toHaveLength(1);
99
+ expect(result.year.branch.hiddenStems[0].stem).toBe("癸");
100
+ expect(result.month.branch.hiddenStems).toHaveLength(3);
101
+ });
102
+ });
103
+ describe("countTenGods", () => {
104
+ it("counts ten gods correctly", () => {
105
+ const analysis = analyzeTenGods("甲子", "丙寅", "甲辰", "乙亥");
106
+ const counts = countTenGods(analysis);
107
+ expect(counts["비견"]).toBeGreaterThanOrEqual(1);
108
+ expect(counts["식신"]).toBeGreaterThanOrEqual(1);
109
+ });
110
+ });
111
+ describe("countElements", () => {
112
+ it("counts elements correctly", () => {
113
+ const analysis = analyzeTenGods("甲子", "丙寅", "甲辰", "乙亥");
114
+ const counts = countElements(analysis);
115
+ expect(counts.wood).toBeGreaterThanOrEqual(3);
116
+ expect(counts.fire).toBeGreaterThanOrEqual(1);
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=yongshen.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yongshen.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/yongshen.test.ts"],"names":[],"mappings":""}