@gracefullight/saju 0.2.0 → 0.4.1

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 (81) hide show
  1. package/README.en.md +250 -32
  2. package/README.md +250 -32
  3. package/dist/__tests__/date-fns-adapter.test.js +1 -1
  4. package/dist/__tests__/four-pillars.test.js +10 -9
  5. package/dist/__tests__/luck.test.d.ts +2 -0
  6. package/dist/__tests__/luck.test.d.ts.map +1 -0
  7. package/dist/__tests__/luck.test.js +33 -0
  8. package/dist/__tests__/lunar.test.js +1 -1
  9. package/dist/__tests__/luxon-adapter.test.js +1 -1
  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__/sinsals.test.d.ts +2 -0
  17. package/dist/__tests__/sinsals.test.d.ts.map +1 -0
  18. package/dist/__tests__/sinsals.test.js +64 -0
  19. package/dist/__tests__/solar-terms.test.d.ts +2 -0
  20. package/dist/__tests__/solar-terms.test.d.ts.map +1 -0
  21. package/dist/__tests__/solar-terms.test.js +121 -0
  22. package/dist/__tests__/strength.test.d.ts +2 -0
  23. package/dist/__tests__/strength.test.d.ts.map +1 -0
  24. package/dist/__tests__/strength.test.js +44 -0
  25. package/dist/__tests__/ten-gods.test.d.ts +2 -0
  26. package/dist/__tests__/ten-gods.test.d.ts.map +1 -0
  27. package/dist/__tests__/ten-gods.test.js +119 -0
  28. package/dist/__tests__/twelve-stages.test.d.ts +2 -0
  29. package/dist/__tests__/twelve-stages.test.d.ts.map +1 -0
  30. package/dist/__tests__/twelve-stages.test.js +86 -0
  31. package/dist/__tests__/utils.test.d.ts +2 -0
  32. package/dist/__tests__/utils.test.d.ts.map +1 -0
  33. package/dist/__tests__/utils.test.js +130 -0
  34. package/dist/__tests__/yongshen.test.d.ts +2 -0
  35. package/dist/__tests__/yongshen.test.d.ts.map +1 -0
  36. package/dist/__tests__/yongshen.test.js +62 -0
  37. package/dist/adapters/date-fns.d.ts +1 -1
  38. package/dist/adapters/luxon.d.ts +1 -1
  39. package/dist/core/four-pillars.d.ts +6 -6
  40. package/dist/core/four-pillars.d.ts.map +1 -1
  41. package/dist/core/four-pillars.js +10 -26
  42. package/dist/core/luck.d.ts +60 -0
  43. package/dist/core/luck.d.ts.map +1 -0
  44. package/dist/core/luck.js +137 -0
  45. package/dist/core/relations.d.ts +94 -0
  46. package/dist/core/relations.d.ts.map +1 -0
  47. package/dist/core/relations.js +309 -0
  48. package/dist/core/sinsals.d.ts +19 -0
  49. package/dist/core/sinsals.d.ts.map +1 -0
  50. package/dist/core/sinsals.js +339 -0
  51. package/dist/core/solar-terms.d.ts +155 -0
  52. package/dist/core/solar-terms.d.ts.map +1 -0
  53. package/dist/core/solar-terms.js +266 -0
  54. package/dist/core/strength.d.ts +18 -0
  55. package/dist/core/strength.d.ts.map +1 -0
  56. package/dist/core/strength.js +255 -0
  57. package/dist/core/ten-gods.d.ts +127 -0
  58. package/dist/core/ten-gods.d.ts.map +1 -0
  59. package/dist/core/ten-gods.js +331 -0
  60. package/dist/core/twelve-stages.d.ts +17 -0
  61. package/dist/core/twelve-stages.d.ts.map +1 -0
  62. package/dist/core/twelve-stages.js +77 -0
  63. package/dist/core/yongshen.d.ts +20 -0
  64. package/dist/core/yongshen.d.ts.map +1 -0
  65. package/dist/core/yongshen.js +216 -0
  66. package/dist/index.d.ts +63 -3
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +57 -2
  69. package/dist/types/common.d.ts +12 -0
  70. package/dist/types/common.d.ts.map +1 -0
  71. package/dist/types/common.js +1 -0
  72. package/dist/types/index.d.ts +2 -0
  73. package/dist/types/index.d.ts.map +1 -0
  74. package/dist/types/index.js +1 -0
  75. package/dist/utils/constants.d.ts +13 -0
  76. package/dist/utils/constants.d.ts.map +1 -0
  77. package/dist/utils/constants.js +59 -0
  78. package/dist/utils/index.d.ts +2 -0
  79. package/dist/utils/index.d.ts.map +1 -0
  80. package/dist/utils/index.js +1 -0
  81. package/package.json +13 -12
@@ -1,7 +1,7 @@
1
1
  import { DateTime } from "luxon";
2
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";
3
+ import { createLuxonAdapter } from "../adapters/luxon";
4
+ import { BRANCHES, dayPillarFromDate, effectiveDayDate, getFourPillars, hourPillar, monthPillar, presetA, presetB, STEMS, yearPillar, } from "../core/four-pillars";
5
5
  describe("Four Pillars Core", () => {
6
6
  let adapter;
7
7
  beforeAll(async () => {
@@ -240,14 +240,15 @@ describe("Four Pillars Core", () => {
240
240
  });
241
241
  expect(resultA.meta.effectiveDayDate.day).toBe(1);
242
242
  });
243
- it("should require longitudeDeg parameter", () => {
243
+ it("should use default longitude from timezone offset when longitudeDeg is omitted", () => {
244
244
  const dt = DateTime.fromObject({ year: 2000, month: 1, day: 1, hour: 18, minute: 0 }, { zone: "Asia/Seoul" });
245
- expect(() => {
246
- getFourPillars(adapter, dt, {
247
- longitudeDeg: undefined,
248
- preset: presetA,
249
- });
250
- }).toThrow("longitudeDeg is required");
245
+ const result = getFourPillars(adapter, dt, {
246
+ preset: presetA,
247
+ });
248
+ expect(result.year).toBeDefined();
249
+ expect(result.month).toBeDefined();
250
+ expect(result.day).toBeDefined();
251
+ expect(result.hour).toBeDefined();
251
252
  });
252
253
  it("should handle leap year dates", () => {
253
254
  const dt = DateTime.fromObject({ year: 2000, month: 2, day: 29 }, { zone: "UTC" });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=luck.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"luck.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/luck.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { calculateYearlyLuck, getYearPillar } from "../core/luck";
3
+ describe("luck", () => {
4
+ describe("getYearPillar", () => {
5
+ it("returns correct pillar for 1984 (甲子)", () => {
6
+ expect(getYearPillar(1984)).toBe("甲子");
7
+ });
8
+ it("returns correct pillar for 2024 (甲辰)", () => {
9
+ expect(getYearPillar(2024)).toBe("甲辰");
10
+ });
11
+ it("returns correct pillar for 2000 (庚辰)", () => {
12
+ expect(getYearPillar(2000)).toBe("庚辰");
13
+ });
14
+ });
15
+ describe("calculateYearlyLuck", () => {
16
+ it("calculates yearly luck for a range", () => {
17
+ const result = calculateYearlyLuck(1990, 2020, 2025);
18
+ expect(result).toHaveLength(6);
19
+ expect(result[0].year).toBe(2020);
20
+ expect(result[5].year).toBe(2025);
21
+ });
22
+ it("includes correct age calculation", () => {
23
+ const result = calculateYearlyLuck(1990, 2020, 2020);
24
+ expect(result[0].age).toBe(31);
25
+ });
26
+ it("returns pillar, stem, and branch for each year", () => {
27
+ const result = calculateYearlyLuck(1990, 2024, 2024);
28
+ expect(result[0].pillar).toBe("甲辰");
29
+ expect(result[0].stem).toBe("甲");
30
+ expect(result[0].branch).toBe("辰");
31
+ });
32
+ });
33
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { getLunarDate, getSolarDate } from "@/core/lunar";
2
+ import { getLunarDate, getSolarDate } from "../core/lunar";
3
3
  describe("Lunar Calendar", () => {
4
4
  describe("getLunarDate", () => {
5
5
  it("should convert solar date to lunar date (2000-01-01)", () => {
@@ -1,6 +1,6 @@
1
1
  import { DateTime } from "luxon";
2
2
  import { beforeAll, describe, expect, it } from "vitest";
3
- import { createLuxonAdapter } from "@/adapters/luxon";
3
+ import { createLuxonAdapter } from "../adapters/luxon";
4
4
  describe("Luxon Adapter", () => {
5
5
  let adapter;
6
6
  beforeAll(async () => {
@@ -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, expect, it } from "vitest";
2
+ import { analyzeRelations, findBranchClash, findBranchSixCombination, findStemCombination, } 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 { DateTime } from "luxon";
2
+ import { describe, expect, it } from "vitest";
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=sinsals.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sinsals.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sinsals.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { analyzeSinsals, SINSAL_INFO, SINSALS } from "../core/sinsals";
3
+ describe("sinsals", () => {
4
+ describe("analyzeSinsals", () => {
5
+ it("should detect peach blossom (도화살)", () => {
6
+ const result = analyzeSinsals("甲寅", "丙寅", "戊卯", "庚午");
7
+ const peachBlossoms = result.matches.filter((m) => m.sinsal === "peachBlossom");
8
+ expect(peachBlossoms.length).toBeGreaterThan(0);
9
+ });
10
+ it("should detect sky horse (역마살)", () => {
11
+ const result = analyzeSinsals("甲寅", "丙寅", "戊申", "庚午");
12
+ const skyHorses = result.matches.filter((m) => m.sinsal === "skyHorse");
13
+ expect(skyHorses.length).toBeGreaterThan(0);
14
+ });
15
+ it("should detect flowery canopy (화개살)", () => {
16
+ const result = analyzeSinsals("甲寅", "丙戌", "戊子", "庚午");
17
+ const floweryCanopies = result.matches.filter((m) => m.sinsal === "floweryCanopy");
18
+ expect(floweryCanopies.length).toBeGreaterThan(0);
19
+ });
20
+ it("should return summary grouped by sinsal", () => {
21
+ const result = analyzeSinsals("甲子", "丙寅", "戊辰", "庚午");
22
+ expect(result.summary).toBeDefined();
23
+ for (const [sinsal, positions] of Object.entries(result.summary)) {
24
+ expect(SINSALS).toContain(sinsal);
25
+ expect(Array.isArray(positions)).toBe(true);
26
+ }
27
+ });
28
+ it("should not have duplicate matches", () => {
29
+ const result = analyzeSinsals("甲子", "丙寅", "戊辰", "庚午");
30
+ const seen = new Set();
31
+ for (const match of result.matches) {
32
+ const key = `${match.sinsal}-${match.position}`;
33
+ expect(seen.has(key)).toBe(false);
34
+ seen.add(key);
35
+ }
36
+ });
37
+ it("should detect sky noble (천을귀인)", () => {
38
+ const result = analyzeSinsals("甲丑", "丙寅", "甲子", "庚午");
39
+ const skyNobles = result.matches.filter((m) => m.sinsal === "skyNoble");
40
+ expect(skyNobles.length).toBeGreaterThan(0);
41
+ });
42
+ });
43
+ describe("SINSAL_INFO", () => {
44
+ it("should have info for all sinsals", () => {
45
+ for (const sinsal of SINSALS) {
46
+ expect(SINSAL_INFO[sinsal]).toBeDefined();
47
+ expect(SINSAL_INFO[sinsal].korean).toBeDefined();
48
+ expect(SINSAL_INFO[sinsal].hanja).toBeDefined();
49
+ expect(SINSAL_INFO[sinsal].meaning).toBeDefined();
50
+ expect(["auspicious", "inauspicious", "neutral"]).toContain(SINSAL_INFO[sinsal].type);
51
+ }
52
+ });
53
+ it("should have correct korean names", () => {
54
+ expect(SINSAL_INFO.peachBlossom.korean).toBe("도화살");
55
+ expect(SINSAL_INFO.skyHorse.korean).toBe("역마살");
56
+ expect(SINSAL_INFO.skyNoble.korean).toBe("천을귀인");
57
+ });
58
+ it("should have correct type classifications", () => {
59
+ expect(SINSAL_INFO.peachBlossom.type).toBe("neutral");
60
+ expect(SINSAL_INFO.skyNoble.type).toBe("auspicious");
61
+ expect(SINSAL_INFO.ghostGate.type).toBe("inauspicious");
62
+ });
63
+ });
64
+ });
@@ -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, expect, it } 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":""}