saju8z 1.0.0 → 1.2.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 (45) hide show
  1. package/README.md +405 -108
  2. package/bin/saju8z.js +160 -0
  3. package/dist/core.cjs +168 -37
  4. package/dist/core.js +168 -37
  5. package/dist/format.cjs +234 -55
  6. package/dist/format.js +234 -55
  7. package/dist/index.cjs +579 -328
  8. package/dist/index.js +579 -328
  9. package/dist/lunar.cjs +4 -4
  10. package/dist/lunar.js +4 -4
  11. package/dist/types/application/calculateSajuChart.d.ts +7 -1
  12. package/dist/types/application/normalizeDateTime.d.ts +7 -0
  13. package/dist/types/calendar/dateMath.d.ts +2 -1
  14. package/dist/types/calendar/solarTerms.d.ts +1 -0
  15. package/dist/types/config/saju8z.d.ts +1 -1
  16. package/dist/types/contracts/sajuChart.d.ts +21 -4
  17. package/dist/types/format.d.ts +1 -1
  18. package/dist/types/index.d.ts +1 -1
  19. package/dist/types/input/parseSajuChartInput.d.ts +10 -0
  20. package/dist/types/output/chartPresentation.d.ts +14 -0
  21. package/dist/types/output/createSajuPrompt.d.ts +1 -0
  22. package/dist/types/output/formatSajuChart.d.ts +1 -1
  23. package/dist/types/output/formatSajuText.d.ts +2 -0
  24. package/dist/types/saju/calculateDayPillar.d.ts +1 -0
  25. package/dist/types/saju/calculateMonthPillar.d.ts +1 -0
  26. package/dist/types/saju/calculateTenGods.d.ts +1 -0
  27. package/dist/types/saju/calculateThreePillars.d.ts +3 -0
  28. package/dist/types-cjs/application/calculateSajuChart.d.ts +7 -1
  29. package/dist/types-cjs/application/normalizeDateTime.d.ts +7 -0
  30. package/dist/types-cjs/calendar/dateMath.d.ts +2 -1
  31. package/dist/types-cjs/calendar/solarTerms.d.ts +1 -0
  32. package/dist/types-cjs/config/saju8z.d.ts +1 -1
  33. package/dist/types-cjs/contracts/sajuChart.d.ts +21 -4
  34. package/dist/types-cjs/format.d.ts +1 -1
  35. package/dist/types-cjs/index.d.ts +1 -1
  36. package/dist/types-cjs/input/parseSajuChartInput.d.ts +10 -0
  37. package/dist/types-cjs/output/chartPresentation.d.ts +14 -0
  38. package/dist/types-cjs/output/createSajuPrompt.d.ts +1 -0
  39. package/dist/types-cjs/output/formatSajuChart.d.ts +1 -1
  40. package/dist/types-cjs/output/formatSajuText.d.ts +2 -0
  41. package/dist/types-cjs/saju/calculateDayPillar.d.ts +1 -0
  42. package/dist/types-cjs/saju/calculateMonthPillar.d.ts +1 -0
  43. package/dist/types-cjs/saju/calculateTenGods.d.ts +1 -0
  44. package/dist/types-cjs/saju/calculateThreePillars.d.ts +3 -0
  45. package/package.json +5 -1
package/bin/saju8z.js ADDED
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ import process from "node:process";
3
+ import { createInterface } from "node:readline";
4
+ import { calculateSajuChart, formatSajuChart, UserInputError } from "../dist/index.js";
5
+
6
+ const HELP_TEXT = `사용법: saju8z
7
+ saju8z --date YYYY-MM-DD [--time HH:mm] [--gmt ±HH:mm] [옵션]
8
+ saju8z --dt YYYYMMDD[HHmm] [--gmt ±HH:mm] [옵션]
9
+
10
+ 인자 없이 실행하면 날짜와 시각을 차례로 입력받습니다.
11
+ 날짜 예: 1998-03-05 / 시각 예: 11:00 (24시간제, 모르면 비워두고 Enter)
12
+ 대화형 입력은 양력·현재 GMT 시차·text 출력을 사용합니다.
13
+
14
+ 옵션으로 실행할 때 필수 입력 (둘 중 하나):
15
+ --dt 생년월일 8자리 (YYYYMMDD) 또는 생년월일시 12자리 (YYYYMMDDHHmm)
16
+ --date 생년월일 (YYYY-MM-DD)
17
+ --time 출생 시각 (선택, HH:mm, 00:00~23:59). 생략하면 삼주만 계산
18
+ --date 또는 --dt는 필수이며, --dt와 --date·--time은 혼용할 수 없습니다.
19
+ 시간이 없으면 입력 날짜 기준으로 삼주를 계산하고 GMT 환산·-30분 보정은 생략합니다.
20
+
21
+ 옵션:
22
+ --gmt 고정 GMT 시차 (기본: 실행 환경의 현재 시차, -12:00~+14:00)
23
+ --calendar solar (기본) 또는 lunar (한국 음력)
24
+ --leap 음력 윤달 지정 (--calendar lunar와 함께 사용)
25
+ --format text (기본 만세력 텍스트), json 또는 prompt (AI 전달용)
26
+ --help, -h 도움말
27
+
28
+ 예제:
29
+ saju8z
30
+ saju8z --dt 19980305
31
+ saju8z --dt 199803051100
32
+ saju8z --date 2000-02-29 --time 00:10
33
+ saju8z --date 2000-02-29 --time 00:10 --gmt +09:00
34
+ saju8z --date 2000-02-29 --time 00:10 --gmt=-05:00 --format json
35
+ saju8z --date 2000-02-29 --time 00:10 --format prompt
36
+ saju8z --date 2017-05-01 --time 12:00 --gmt +09:00 --calendar lunar --leap
37
+ `;
38
+
39
+ function getCurrentGmtOffset() {
40
+ const minutes = -new Date().getTimezoneOffset();
41
+ const sign = minutes < 0 ? "-" : "+";
42
+ const absolute = Math.abs(minutes);
43
+ const hours = String(Math.floor(absolute / 60)).padStart(2, "0");
44
+ const remainder = String(absolute % 60).padStart(2, "0");
45
+ return `${sign}${hours}:${remainder}`;
46
+ }
47
+
48
+ async function readInteractiveArguments() {
49
+ const reader = createInterface({
50
+ input: process.stdin,
51
+ output: process.stderr,
52
+ terminal: Boolean(process.stdin.isTTY && process.stderr.isTTY),
53
+ crlfDelay: Infinity
54
+ });
55
+ const answers = reader[Symbol.asyncIterator]();
56
+ /** @param {string} prompt */
57
+ async function ask(prompt) {
58
+ process.stderr.write(prompt);
59
+ const answer = await answers.next();
60
+ if (answer.done) {
61
+ throw new UserInputError("입력이 종료되었습니다. --date 또는 --dt를 사용하세요.");
62
+ }
63
+ return answer.value.trim();
64
+ }
65
+
66
+ try {
67
+ process.stderr.write("생년월일과 출생 시각을 입력하세요. 양력·현재 GMT 시차로 계산합니다.\n");
68
+ const date = await ask("생년월일 (YYYY-MM-DD, 예: 1998-03-05): ");
69
+ const time = await ask("출생 시각 (HH:mm, 예: 11:00 / 시간을 모르면 비워두고 Enter): ");
70
+ return time ? ["--date", date, "--time", time] : ["--date", date];
71
+ } finally {
72
+ reader.close();
73
+ }
74
+ }
75
+
76
+ const VALUE_OPTIONS = new Set(["--dt", "--date", "--time", "--gmt", "--calendar", "--format"]);
77
+
78
+ /** @param {string[]} args */
79
+ function parseOptions(args) {
80
+ /** @type {Map<string, string>} */
81
+ const values = new Map();
82
+ let leapMonth = false;
83
+ for (let index = 0; index < args.length; index++) {
84
+ const argument = args[index];
85
+ const separator = argument.indexOf("=");
86
+ const option = separator < 0 ? argument : argument.slice(0, separator);
87
+ if (option === "--leap" && separator < 0) {
88
+ if (leapMonth) throw new UserInputError("중복된 옵션입니다: --leap");
89
+ leapMonth = true;
90
+ continue;
91
+ }
92
+ if (!VALUE_OPTIONS.has(option)) throw new UserInputError(`지원하지 않는 옵션입니다: ${option}`);
93
+ if (values.has(option)) throw new UserInputError(`중복된 옵션입니다: ${option}`);
94
+ const value = separator < 0 ? args[++index] : argument.slice(separator + 1);
95
+ if (!value || value.startsWith("--")) throw new UserInputError(`${option} 값을 입력하세요.`);
96
+ values.set(option, value);
97
+ }
98
+ return { values, leapMonth };
99
+ }
100
+
101
+ /** @param {Map<string, string>} values */
102
+ function resolveDateTime(values) {
103
+ const dt = values.get("--dt");
104
+ if (dt !== undefined) {
105
+ if (values.has("--date") || values.has("--time")) {
106
+ throw new UserInputError("--dt는 --date 또는 --time과 함께 사용할 수 없습니다.");
107
+ }
108
+ if (![8, 12].includes(dt.length) || !/^\d+$/.test(dt)) {
109
+ throw new UserInputError(
110
+ "--dt는 YYYYMMDD 형식의 8자리 또는 YYYYMMDDHHmm 형식의 12자리 숫자여야 합니다."
111
+ );
112
+ }
113
+ return {
114
+ date: `${dt.slice(0, 4)}-${dt.slice(4, 6)}-${dt.slice(6, 8)}`,
115
+ ...(dt.length === 12 ? { time: `${dt.slice(8, 10)}:${dt.slice(10, 12)}` } : {})
116
+ };
117
+ }
118
+ const date = values.get("--date");
119
+ const time = values.get("--time");
120
+ if (!date) {
121
+ throw new UserInputError("--date 또는 --dt를 입력하세요.");
122
+ }
123
+ return { date, ...(time === undefined ? {} : { time }) };
124
+ }
125
+
126
+ /** @param {string[]} args */
127
+ function parseArguments(args) {
128
+ const { values, leapMonth } = parseOptions(args);
129
+ const { date, time } = resolveDateTime(values);
130
+ const gmt = values.get("--gmt") ?? getCurrentGmtOffset();
131
+ const calendar = values.get("--calendar") ?? "solar";
132
+ if (calendar !== "solar" && calendar !== "lunar")
133
+ throw new UserInputError("달력은 solar 또는 lunar여야 합니다.");
134
+ const format = values.get("--format") ?? "text";
135
+ if (format !== "text" && format !== "json" && format !== "prompt")
136
+ throw new UserInputError("출력 형식은 text, json 또는 prompt이어야 합니다.");
137
+ return {
138
+ input: { date, ...(time === undefined ? {} : { time }), gmt, calendar, leapMonth },
139
+ format
140
+ };
141
+ }
142
+
143
+ try {
144
+ const args = process.argv.slice(2);
145
+ const showHelp = args.length === 1 && (args[0] === "--help" || args[0] === "-h");
146
+ if (showHelp) {
147
+ process.stdout.write(HELP_TEXT);
148
+ } else {
149
+ const inputArgs = args.length === 0 ? await readInteractiveArguments() : args;
150
+ const { input, format } = parseArguments(inputArgs);
151
+ const result = calculateSajuChart(input);
152
+ const leadingNewline = format === "text" ? "\n" : "";
153
+ process.stdout.write(`${leadingNewline}${formatSajuChart(result, format)}\n`);
154
+ }
155
+ } catch (error) {
156
+ process.stderr.write(
157
+ `saju8z: ${error instanceof Error ? error.message : "계산에 실패했습니다."}\n도움말: saju8z --help\n`
158
+ );
159
+ process.exitCode = 1;
160
+ }
package/dist/core.cjs CHANGED
@@ -37,7 +37,9 @@ __export(core_exports, {
37
37
  addMinutes: () => addMinutes,
38
38
  applySolarTimeCorrection: () => applySolarTimeCorrection,
39
39
  branchIndex: () => branchIndex,
40
+ calculateDateTenGods: () => calculateDateTenGods,
40
41
  calculateDayPillar: () => calculateDayPillar,
42
+ calculateDayPillarFromDate: () => calculateDayPillarFromDate,
41
43
  calculateFourPillars: () => calculateFourPillars,
42
44
  calculateHourBranch: () => calculateHourBranch,
43
45
  calculateHourPillar: () => calculateHourPillar,
@@ -45,10 +47,12 @@ __export(core_exports, {
45
47
  calculateTenGod: () => calculateTenGod,
46
48
  calculateTenGods: () => calculateTenGods,
47
49
  calculateYearPillar: () => calculateYearPillar,
50
+ compareLocalDate: () => compareLocalDate,
48
51
  compareLocalDateTime: () => compareLocalDateTime,
49
52
  daysInMonth: () => daysInMonth,
50
53
  formatLocalDateTime: () => formatLocalDateTime,
51
54
  formatPillar: () => formatPillar,
55
+ getMonthBranchBySolarDate: () => getMonthBranchBySolarDate,
52
56
  getMonthBranchBySolarTerm: () => getMonthBranchBySolarTerm,
53
57
  getSolarTermDateTime: () => getSolarTermDateTime,
54
58
  hasLunarLeapMonth: () => hasLunarLeapMonth,
@@ -58,6 +62,7 @@ __export(core_exports, {
58
62
  lunarMonthDays: () => lunarMonthDays,
59
63
  lunarToSolar: () => lunarToSolar,
60
64
  monthBranchesInOrder: () => monthBranchesInOrder,
65
+ monthPillarFromBranch: () => monthPillarFromBranch,
61
66
  pillarFromIndex: () => pillarFromIndex,
62
67
  positiveModulo: () => positiveModulo,
63
68
  sexagenaryIndexOfPillar: () => sexagenaryIndexOfPillar,
@@ -74,7 +79,7 @@ var SUPPORTED_START_YEAR = 1900;
74
79
  var SUPPORTED_END_YEAR = 2100;
75
80
 
76
81
  // src/config/saju8z.ts
77
- var SAJU8Z_VERSION = "1.0.0";
82
+ var SAJU8Z_VERSION = "1.2.0";
78
83
  var SAJU8Z_CALCULATION_PROFILE = "saju8z-fixed-gmt-kst-minus30-v1";
79
84
  var SAJU8Z_CORRECTION_MINUTES = -30;
80
85
 
@@ -116,18 +121,112 @@ var EARTHLY_BRANCHES = [
116
121
  "\uD574"
117
122
  ];
118
123
  var BRANCH_INFO = {
119
- \uC790: { name: "\uC790", element: "\u6C34", mainHiddenStem: "\uACC4", hiddenStems: [{ stem: "\uACC4", role: "main" }] },
120
- \uCD95: { name: "\uCD95", element: "\u571F", mainHiddenStem: "\uAE30", hiddenStems: [{ stem: "\uAE30", role: "main" }, { stem: "\uC2E0", role: "middle" }, { stem: "\uACC4", role: "initial" }] },
121
- \uC778: { name: "\uC778", element: "\u6728", mainHiddenStem: "\uAC11", hiddenStems: [{ stem: "\uAC11", role: "main" }, { stem: "\uBCD1", role: "middle" }, { stem: "\uBB34", role: "initial" }] },
122
- \uBB18: { name: "\uBB18", element: "\u6728", mainHiddenStem: "\uC744", hiddenStems: [{ stem: "\uC744", role: "main" }] },
123
- \uC9C4: { name: "\uC9C4", element: "\u571F", mainHiddenStem: "\uBB34", hiddenStems: [{ stem: "\uBB34", role: "main" }, { stem: "\uC744", role: "middle" }, { stem: "\uACC4", role: "initial" }] },
124
- \uC0AC: { name: "\uC0AC", element: "\u706B", mainHiddenStem: "\uBCD1", hiddenStems: [{ stem: "\uBCD1", role: "main" }, { stem: "\uACBD", role: "middle" }, { stem: "\uBB34", role: "initial" }] },
125
- \uC624: { name: "\uC624", element: "\u706B", mainHiddenStem: "\uC815", hiddenStems: [{ stem: "\uC815", role: "main" }, { stem: "\uAE30", role: "middle" }] },
126
- \uBBF8: { name: "\uBBF8", element: "\u571F", mainHiddenStem: "\uAE30", hiddenStems: [{ stem: "\uAE30", role: "main" }, { stem: "\uC815", role: "middle" }, { stem: "\uC744", role: "initial" }] },
127
- \uC2E0: { name: "\uC2E0", element: "\u91D1", mainHiddenStem: "\uACBD", hiddenStems: [{ stem: "\uACBD", role: "main" }, { stem: "\uC784", role: "middle" }, { stem: "\uBB34", role: "initial" }] },
128
- \uC720: { name: "\uC720", element: "\u91D1", mainHiddenStem: "\uC2E0", hiddenStems: [{ stem: "\uC2E0", role: "main" }] },
129
- \uC220: { name: "\uC220", element: "\u571F", mainHiddenStem: "\uBB34", hiddenStems: [{ stem: "\uBB34", role: "main" }, { stem: "\uC2E0", role: "middle" }, { stem: "\uC815", role: "initial" }] },
130
- \uD574: { name: "\uD574", element: "\u6C34", mainHiddenStem: "\uC784", hiddenStems: [{ stem: "\uC784", role: "main" }, { stem: "\uAC11", role: "middle" }] }
124
+ \uC790: {
125
+ name: "\uC790",
126
+ element: "\u6C34",
127
+ mainHiddenStem: "\uACC4",
128
+ hiddenStems: [{ stem: "\uACC4", role: "main" }]
129
+ },
130
+ \uCD95: {
131
+ name: "\uCD95",
132
+ element: "\u571F",
133
+ mainHiddenStem: "\uAE30",
134
+ hiddenStems: [
135
+ { stem: "\uAE30", role: "main" },
136
+ { stem: "\uC2E0", role: "middle" },
137
+ { stem: "\uACC4", role: "initial" }
138
+ ]
139
+ },
140
+ \uC778: {
141
+ name: "\uC778",
142
+ element: "\u6728",
143
+ mainHiddenStem: "\uAC11",
144
+ hiddenStems: [
145
+ { stem: "\uAC11", role: "main" },
146
+ { stem: "\uBCD1", role: "middle" },
147
+ { stem: "\uBB34", role: "initial" }
148
+ ]
149
+ },
150
+ \uBB18: {
151
+ name: "\uBB18",
152
+ element: "\u6728",
153
+ mainHiddenStem: "\uC744",
154
+ hiddenStems: [{ stem: "\uC744", role: "main" }]
155
+ },
156
+ \uC9C4: {
157
+ name: "\uC9C4",
158
+ element: "\u571F",
159
+ mainHiddenStem: "\uBB34",
160
+ hiddenStems: [
161
+ { stem: "\uBB34", role: "main" },
162
+ { stem: "\uC744", role: "middle" },
163
+ { stem: "\uACC4", role: "initial" }
164
+ ]
165
+ },
166
+ \uC0AC: {
167
+ name: "\uC0AC",
168
+ element: "\u706B",
169
+ mainHiddenStem: "\uBCD1",
170
+ hiddenStems: [
171
+ { stem: "\uBCD1", role: "main" },
172
+ { stem: "\uACBD", role: "middle" },
173
+ { stem: "\uBB34", role: "initial" }
174
+ ]
175
+ },
176
+ \uC624: {
177
+ name: "\uC624",
178
+ element: "\u706B",
179
+ mainHiddenStem: "\uC815",
180
+ hiddenStems: [
181
+ { stem: "\uC815", role: "main" },
182
+ { stem: "\uAE30", role: "middle" }
183
+ ]
184
+ },
185
+ \uBBF8: {
186
+ name: "\uBBF8",
187
+ element: "\u571F",
188
+ mainHiddenStem: "\uAE30",
189
+ hiddenStems: [
190
+ { stem: "\uAE30", role: "main" },
191
+ { stem: "\uC815", role: "middle" },
192
+ { stem: "\uC744", role: "initial" }
193
+ ]
194
+ },
195
+ \uC2E0: {
196
+ name: "\uC2E0",
197
+ element: "\u91D1",
198
+ mainHiddenStem: "\uACBD",
199
+ hiddenStems: [
200
+ { stem: "\uACBD", role: "main" },
201
+ { stem: "\uC784", role: "middle" },
202
+ { stem: "\uBB34", role: "initial" }
203
+ ]
204
+ },
205
+ \uC720: {
206
+ name: "\uC720",
207
+ element: "\u91D1",
208
+ mainHiddenStem: "\uC2E0",
209
+ hiddenStems: [{ stem: "\uC2E0", role: "main" }]
210
+ },
211
+ \uC220: {
212
+ name: "\uC220",
213
+ element: "\u571F",
214
+ mainHiddenStem: "\uBB34",
215
+ hiddenStems: [
216
+ { stem: "\uBB34", role: "main" },
217
+ { stem: "\uC2E0", role: "middle" },
218
+ { stem: "\uC815", role: "initial" }
219
+ ]
220
+ },
221
+ \uD574: {
222
+ name: "\uD574",
223
+ element: "\u6C34",
224
+ mainHiddenStem: "\uC784",
225
+ hiddenStems: [
226
+ { stem: "\uC784", role: "main" },
227
+ { stem: "\uAC11", role: "middle" }
228
+ ]
229
+ }
131
230
  };
132
231
  function branchIndex(branch) {
133
232
  return EARTHLY_BRANCHES.indexOf(branch);
@@ -206,6 +305,8 @@ function stemAt(index) {
206
305
  }
207
306
 
208
307
  // src/calendar/dateMath.ts
308
+ var MINUTES_PER_HOUR = 60;
309
+ var MINUTES_PER_DAY = 24 * MINUTES_PER_HOUR;
209
310
  function isLeapYear(year) {
210
311
  return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
211
312
  }
@@ -216,11 +317,14 @@ function daysInMonth(year, month) {
216
317
  function isValidDate(year, month, day) {
217
318
  return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month);
218
319
  }
219
- function compareLocalDateTime(a, b) {
320
+ function compareLocalDate(left, right) {
321
+ return left.year - right.year || left.month - right.month || left.day - right.day;
322
+ }
323
+ function compareLocalDateTime(left, right) {
220
324
  const keys = ["year", "month", "day", "hour", "minute"];
221
325
  for (const key of keys) {
222
- const diff = a[key] - b[key];
223
- if (diff !== 0) return diff;
326
+ const difference = left[key] - right[key];
327
+ if (difference !== 0) return difference;
224
328
  }
225
329
  return 0;
226
330
  }
@@ -228,9 +332,9 @@ function addMinutes(dateTime, minutes) {
228
332
  let year = dateTime.year;
229
333
  let month = dateTime.month;
230
334
  let day = dateTime.day;
231
- let totalMinutes = dateTime.hour * 60 + dateTime.minute + minutes;
335
+ let totalMinutes = dateTime.hour * MINUTES_PER_HOUR + dateTime.minute + minutes;
232
336
  while (totalMinutes < 0) {
233
- totalMinutes += 1440;
337
+ totalMinutes += MINUTES_PER_DAY;
234
338
  day -= 1;
235
339
  if (day < 1) {
236
340
  month -= 1;
@@ -241,8 +345,8 @@ function addMinutes(dateTime, minutes) {
241
345
  day = daysInMonth(year, month);
242
346
  }
243
347
  }
244
- while (totalMinutes >= 1440) {
245
- totalMinutes -= 1440;
348
+ while (totalMinutes >= MINUTES_PER_DAY) {
349
+ totalMinutes -= MINUTES_PER_DAY;
246
350
  day += 1;
247
351
  if (day > daysInMonth(year, month)) {
248
352
  day = 1;
@@ -257,18 +361,21 @@ function addMinutes(dateTime, minutes) {
257
361
  year,
258
362
  month,
259
363
  day,
260
- hour: Math.floor(totalMinutes / 60),
261
- minute: totalMinutes % 60
364
+ hour: Math.floor(totalMinutes / MINUTES_PER_HOUR),
365
+ minute: totalMinutes % MINUTES_PER_HOUR
262
366
  };
263
367
  }
264
368
  function julianDayNumber(year, month, day) {
265
- const a = Math.floor((14 - month) / 12);
266
- const y = year + 4800 - a;
267
- const m = month + 12 * a - 3;
268
- return day + Math.floor((153 * m + 2) / 5) + 365 * y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400) - 32045;
369
+ const yearAdjustment = Math.floor((14 - month) / 12);
370
+ const adjustedYear = year + 4800 - yearAdjustment;
371
+ const adjustedMonth = month + 12 * yearAdjustment - 3;
372
+ return day + Math.floor((153 * adjustedMonth + 2) / 5) + 365 * adjustedYear + Math.floor(adjustedYear / 4) - Math.floor(adjustedYear / 100) + Math.floor(adjustedYear / 400) - 32045;
269
373
  }
270
374
  function formatLocalDateTime(dateTime) {
271
- return `${dateTime.year.toString().padStart(4, "0")}-${dateTime.month.toString().padStart(2, "0")}-${dateTime.day.toString().padStart(2, "0")}T${dateTime.hour.toString().padStart(2, "0")}:${dateTime.minute.toString().padStart(2, "0")}`;
375
+ const pad = (value) => value.toString().padStart(2, "0");
376
+ const date = `${dateTime.year.toString().padStart(4, "0")}-${pad(dateTime.month)}-${pad(dateTime.day)}`;
377
+ const time = `${pad(dateTime.hour)}:${pad(dateTime.minute)}`;
378
+ return `${date}T${time}`;
272
379
  }
273
380
 
274
381
  // node_modules/korean-lunar-calendar/dist/korean-lunar-calendar.mjs
@@ -1672,16 +1779,16 @@ function lunarMonthDays(year, month, leap) {
1672
1779
  return 29;
1673
1780
  }
1674
1781
  }
1675
- var leapYears = /* @__PURE__ */ new Map();
1782
+ var leapMonthByYear = /* @__PURE__ */ new Map();
1676
1783
  function hasLunarLeapMonth(year) {
1677
1784
  if (!Number.isInteger(year) || year < 1900 || year > 2050) return false;
1678
- const known = leapYears.get(year);
1679
- if (known !== void 0) return known;
1785
+ const cached = leapMonthByYear.get(year);
1786
+ if (cached !== void 0) return cached;
1680
1787
  const calendar = createCalendar();
1681
1788
  const hasLeap = Array.from({ length: 12 }, (_, index) => index + 1).some(
1682
1789
  (month) => calendar.setLunarDate(year, month, 1, true) && calendar.getLunarCalendar().intercalation === true
1683
1790
  );
1684
- leapYears.set(year, hasLeap);
1791
+ leapMonthByYear.set(year, hasLeap);
1685
1792
  return hasLeap;
1686
1793
  }
1687
1794
 
@@ -1705,14 +1812,17 @@ var SOLAR_TERM_RULES = [
1705
1812
  { key: "daeseol", month: 12, day: 7, hour: 5, minute: 0, monthBranch: "\uC790" },
1706
1813
  { key: "sohan", month: 1, day: 6, hour: 5, minute: 0, monthBranch: "\uCD95" }
1707
1814
  ];
1815
+ function solarTermDateTime(year, rule) {
1816
+ return { year, month: rule.month, day: rule.day, hour: rule.hour, minute: rule.minute };
1817
+ }
1708
1818
  function getSolarTermDateTime(year, key) {
1709
1819
  const rule = SOLAR_TERM_RULES.find((item) => item.key === key);
1710
1820
  if (!rule) throw new Error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC808\uAE30\uC785\uB2C8\uB2E4: ${key}`);
1711
- return { year, month: rule.month, day: rule.day, hour: rule.hour, minute: rule.minute };
1821
+ return solarTermDateTime(year, rule);
1712
1822
  }
1713
1823
  function getMonthBranchBySolarTerm(dateTime) {
1714
1824
  const candidates = SOLAR_TERM_RULES.map((rule) => ({
1715
- startsAt: { year: dateTime.year, month: rule.month, day: rule.day, hour: rule.hour, minute: rule.minute },
1825
+ startsAt: solarTermDateTime(dateTime.year, rule),
1716
1826
  branch: rule.monthBranch
1717
1827
  })).sort((a, b) => compareLocalDateTime(a.startsAt, b.startsAt));
1718
1828
  let selected = "\uCD95";
@@ -1721,11 +1831,23 @@ function getMonthBranchBySolarTerm(dateTime) {
1721
1831
  }
1722
1832
  return selected;
1723
1833
  }
1834
+ function getMonthBranchBySolarDate(date) {
1835
+ const rules = [...SOLAR_TERM_RULES].sort((left, right) => left.month - right.month);
1836
+ let selected = "\uCD95";
1837
+ for (const rule of rules) {
1838
+ if (compareLocalDate(date, solarTermDateTime(date.year, rule)) >= 0)
1839
+ selected = rule.monthBranch;
1840
+ }
1841
+ return selected;
1842
+ }
1724
1843
 
1725
1844
  // src/saju/calculateDayPillar.ts
1726
1845
  var JDN_TO_SEXAGENARY_OFFSET = 49;
1727
1846
  function calculateDayPillar(dateTime) {
1728
- const jdn = julianDayNumber(dateTime.year, dateTime.month, dateTime.day);
1847
+ return calculateDayPillarFromDate(dateTime);
1848
+ }
1849
+ function calculateDayPillarFromDate(date) {
1850
+ const jdn = julianDayNumber(date.year, date.month, date.day);
1729
1851
  return pillarFromIndex(positiveModulo(jdn + JDN_TO_SEXAGENARY_OFFSET, 60));
1730
1852
  }
1731
1853
 
@@ -1778,6 +1900,9 @@ var IN_MONTH_START_STEM_BY_YEAR_STEM = {
1778
1900
  };
1779
1901
  function calculateMonthPillar(dateTime, yearStem) {
1780
1902
  const branch = getMonthBranchBySolarTerm(dateTime);
1903
+ return monthPillarFromBranch(branch, yearStem);
1904
+ }
1905
+ function monthPillarFromBranch(branch, yearStem) {
1781
1906
  const offsetFromIn = positiveModulo(branchIndex(branch) - branchIndex("\uC778"), 12);
1782
1907
  const stem = stemAt(IN_MONTH_START_STEM_BY_YEAR_STEM[yearStem] + offsetFromIn);
1783
1908
  return { stem, branch };
@@ -1831,14 +1956,20 @@ function calculateTenGod(dayStem, targetStem) {
1831
1956
 
1832
1957
  // src/saju/calculateTenGods.ts
1833
1958
  function calculateTenGods(fourPillars) {
1959
+ const dayStem = fourPillars.day.stem;
1960
+ return {
1961
+ ...calculateDateTenGods(fourPillars),
1962
+ hourStem: calculateTenGod(dayStem, fourPillars.hour.stem),
1963
+ hourBranch: calculateTenGod(dayStem, BRANCH_INFO[fourPillars.hour.branch].mainHiddenStem)
1964
+ };
1965
+ }
1966
+ function calculateDateTenGods(fourPillars) {
1834
1967
  const dayStem = fourPillars.day.stem;
1835
1968
  return {
1836
1969
  yearStem: calculateTenGod(dayStem, fourPillars.year.stem),
1837
1970
  yearBranch: calculateTenGod(dayStem, BRANCH_INFO[fourPillars.year.branch].mainHiddenStem),
1838
1971
  monthStem: calculateTenGod(dayStem, fourPillars.month.stem),
1839
1972
  monthBranch: calculateTenGod(dayStem, BRANCH_INFO[fourPillars.month.branch].mainHiddenStem),
1840
- dayBranch: calculateTenGod(dayStem, BRANCH_INFO[fourPillars.day.branch].mainHiddenStem),
1841
- hourStem: calculateTenGod(dayStem, fourPillars.hour.stem),
1842
- hourBranch: calculateTenGod(dayStem, BRANCH_INFO[fourPillars.hour.branch].mainHiddenStem)
1973
+ dayBranch: calculateTenGod(dayStem, BRANCH_INFO[fourPillars.day.branch].mainHiddenStem)
1843
1974
  };
1844
1975
  }