@gracefullight/saju 1.3.0 → 1.3.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.
package/README.ko.md CHANGED
@@ -151,16 +151,20 @@ import { getFourPillars, STANDARD_PRESET } from "@gracefullight/saju";
151
151
 
152
152
  const adapter = await createDateFnsAdapter();
153
153
 
154
- const dt = {
155
- date: new Date(1985, 4, 15, 14, 30), // 주의: 월은 0부터 시작
156
- timeZone: "Asia/Seoul",
157
- };
154
+ // 일반 Date를 바로 사용할 수 있습니다
155
+ const dt = new Date(1985, 4, 15, 14, 30); // 주의: 월은 0부터 시작
158
156
 
159
157
  const result = getFourPillars(dt, {
160
158
  adapter,
161
159
  longitudeDeg: 126.9778,
162
160
  preset: STANDARD_PRESET,
163
161
  });
162
+
163
+ // 명시적인 타임존 메타데이터가 필요하면 래퍼 객체도 계속 사용할 수 있습니다
164
+ const zonedDt = {
165
+ date: new Date(1985, 4, 15, 14, 30),
166
+ timeZone: "Asia/Seoul",
167
+ };
164
168
  ```
165
169
 
166
170
  ### 커스텀 날짜 어댑터
package/README.md CHANGED
@@ -151,16 +151,20 @@ import { getFourPillars, STANDARD_PRESET } from "@gracefullight/saju";
151
151
 
152
152
  const adapter = await createDateFnsAdapter();
153
153
 
154
- const dt = {
155
- date: new Date(1985, 4, 15, 14, 30), // Note: month is 0-indexed
156
- timeZone: "Asia/Seoul",
157
- };
154
+ // Plain Date works directly
155
+ const dt = new Date(1985, 4, 15, 14, 30); // Note: month is 0-indexed
158
156
 
159
157
  const result = getFourPillars(dt, {
160
158
  adapter,
161
159
  longitudeDeg: 126.9778,
162
160
  preset: STANDARD_PRESET,
163
161
  });
162
+
163
+ // If you need explicit timezone metadata, you can still pass a wrapper object
164
+ const zonedDt = {
165
+ date: new Date(1985, 4, 15, 14, 30),
166
+ timeZone: "Asia/Seoul",
167
+ };
164
168
  ```
165
169
 
166
170
  ### Custom Date Adapter
@@ -6,6 +6,13 @@ describe("date-fns Adapter", () => {
6
6
  adapter = await createDateFnsAdapter();
7
7
  });
8
8
  describe("Basic date getters", () => {
9
+ it("should accept plain Date values", () => {
10
+ const dt = new Date(2000, 0, 1, 18, 0);
11
+ expect(adapter.getYear(dt)).toBe(2000);
12
+ expect(adapter.getMonth(dt)).toBe(1);
13
+ expect(adapter.getDay(dt)).toBe(1);
14
+ expect(adapter.getHour(dt)).toBe(18);
15
+ });
9
16
  it("should get year correctly", () => {
10
17
  const dt = { date: new Date(2000, 0, 1), timeZone: "Asia/Seoul" };
11
18
  expect(adapter.getYear(dt)).toBe(2000);
@@ -34,6 +41,10 @@ describe("date-fns Adapter", () => {
34
41
  const dt = { date: new Date(2000, 0, 1), timeZone: "Asia/Seoul" };
35
42
  expect(adapter.getZoneName(dt)).toBe("Asia/Seoul");
36
43
  });
44
+ it("should fall back to system timezone for plain Date values", () => {
45
+ const dt = new Date(2000, 0, 1);
46
+ expect(adapter.getZoneName(dt)).toBe(Intl.DateTimeFormat().resolvedOptions().timeZone);
47
+ });
37
48
  });
38
49
  describe("Date arithmetic", () => {
39
50
  it("should add minutes correctly", () => {
@@ -1,5 +1,6 @@
1
1
  import { DateTime } from "luxon";
2
2
  import { describe, expect, it } from "vitest";
3
+ import { createDateFnsAdapter } from "../adapters/date-fns";
3
4
  import { createLuxonAdapter } from "../adapters/luxon";
4
5
  import { getSaju, STANDARD_PRESET } from "../index";
5
6
  describe("getSaju integration", () => {
@@ -140,4 +141,25 @@ describe("getSaju integration", () => {
140
141
  expect(result.solarTerms.prevJieDate).toBeDefined();
141
142
  expect(result.solarTerms.nextJieDate).toBeDefined();
142
143
  });
144
+ it("accepts plain Date with the date-fns adapter (Issue #64 regression)", async () => {
145
+ const adapter = await createDateFnsAdapter();
146
+ const dt = new Date(1990, 1, 1, 12, 10);
147
+ expect(() => getSaju(dt, {
148
+ adapter,
149
+ longitudeDeg: 126.9778,
150
+ gender: "male",
151
+ preset: STANDARD_PRESET,
152
+ })).not.toThrow();
153
+ const result = getSaju(dt, {
154
+ adapter,
155
+ longitudeDeg: 126.9778,
156
+ gender: "male",
157
+ preset: STANDARD_PRESET,
158
+ });
159
+ expect(result.pillars.year).toBeDefined();
160
+ expect(result.pillars.month).toBeDefined();
161
+ expect(result.pillars.day).toBeDefined();
162
+ expect(result.pillars.hour).toBeDefined();
163
+ expect(result.lunar.lunarYear).toBeGreaterThan(0);
164
+ });
143
165
  });
@@ -1,8 +1,8 @@
1
1
  import type { DateAdapter } from "../adapters/date-adapter";
2
- interface DateFnsDate {
2
+ export interface ZonedDateFnsDate {
3
3
  date: Date;
4
4
  timeZone: string;
5
5
  }
6
+ export type DateFnsDate = Date | ZonedDateFnsDate;
6
7
  export declare function createDateFnsAdapter(): Promise<DateAdapter<DateFnsDate>>;
7
- export {};
8
8
  //# sourceMappingURL=date-fns.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"date-fns.d.ts","sourceRoot":"","sources":["../../src/adapters/date-fns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,UAAU,WAAW;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CA4E9E"}
1
+ {"version":3,"file":"date-fns.d.ts","sourceRoot":"","sources":["../../src/adapters/date-fns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,gBAAgB,CAAC;AAgClD,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CA4D9E"}
@@ -1,3 +1,25 @@
1
+ function isZonedDate(date) {
2
+ return (typeof date === "object" &&
3
+ date !== null &&
4
+ "date" in date &&
5
+ date.date instanceof Date &&
6
+ typeof date.timeZone === "string");
7
+ }
8
+ function getNativeDate(date) {
9
+ return isZonedDate(date) ? date.date : date;
10
+ }
11
+ function getSystemTimeZone() {
12
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
13
+ }
14
+ function getTimeZone(date) {
15
+ return isZonedDate(date) ? date.timeZone : getSystemTimeZone();
16
+ }
17
+ function cloneWithTimeZone(date, timeZone) {
18
+ return { date, timeZone };
19
+ }
20
+ function preserveInputShape(input, date) {
21
+ return isZonedDate(input) ? cloneWithTimeZone(date, input.timeZone) : date;
22
+ }
1
23
  export async function createDateFnsAdapter() {
2
24
  let addMinutes;
3
25
  let addDays;
@@ -31,43 +53,22 @@ export async function createDateFnsAdapter() {
31
53
  throw new Error("date-fns or date-fns-tz is not installed. Install with: npm install date-fns date-fns-tz");
32
54
  }
33
55
  return {
34
- getYear: (dateFns) => getYear(dateFns.date),
35
- getMonth: (dateFns) => getMonth(dateFns.date) + 1,
36
- getDay: (dateFns) => getDate(dateFns.date),
37
- getHour: (dateFns) => getHours(dateFns.date),
38
- getMinute: (dateFns) => getMinutes(dateFns.date),
39
- getSecond: (dateFns) => getSeconds(dateFns.date),
40
- getZoneName: (dateFns) => dateFns.timeZone,
41
- plusMinutes: (dateFns, minutes) => ({
42
- date: addMinutes(dateFns.date, minutes),
43
- timeZone: dateFns.timeZone,
44
- }),
45
- plusDays: (dateFns, days) => ({
46
- date: addDays(dateFns.date, days),
47
- timeZone: dateFns.timeZone,
48
- }),
49
- minusDays: (dateFns, days) => ({
50
- date: subDays(dateFns.date, days),
51
- timeZone: dateFns.timeZone,
52
- }),
53
- toUTC: (dateFns) => ({
54
- date: fromZonedTime(dateFns.date, dateFns.timeZone),
55
- timeZone: "UTC",
56
- }),
57
- toISO: (dateFns) => formatISO(dateFns.date),
58
- toMillis: (dateFns) => dateFns.date.getTime(),
59
- fromMillis: (millis, zone) => ({
60
- date: new Date(millis),
61
- timeZone: zone,
62
- }),
63
- createUTC: (year, month, day, hour, minute, second) => ({
64
- date: new Date(Date.UTC(year, month - 1, day, hour, minute, second)),
65
- timeZone: "UTC",
66
- }),
67
- setZone: (dateFns, zoneName) => ({
68
- date: toZonedTime(dateFns.date, zoneName),
69
- timeZone: zoneName,
70
- }),
71
- isGreaterThanOrEqual: (date1, date2) => date1.date >= date2.date,
56
+ getYear: (dateFns) => getYear(getNativeDate(dateFns)),
57
+ getMonth: (dateFns) => getMonth(getNativeDate(dateFns)) + 1,
58
+ getDay: (dateFns) => getDate(getNativeDate(dateFns)),
59
+ getHour: (dateFns) => getHours(getNativeDate(dateFns)),
60
+ getMinute: (dateFns) => getMinutes(getNativeDate(dateFns)),
61
+ getSecond: (dateFns) => getSeconds(getNativeDate(dateFns)),
62
+ getZoneName: (dateFns) => getTimeZone(dateFns),
63
+ plusMinutes: (dateFns, minutes) => preserveInputShape(dateFns, addMinutes(getNativeDate(dateFns), minutes)),
64
+ plusDays: (dateFns, days) => preserveInputShape(dateFns, addDays(getNativeDate(dateFns), days)),
65
+ minusDays: (dateFns, days) => preserveInputShape(dateFns, subDays(getNativeDate(dateFns), days)),
66
+ toUTC: (dateFns) => cloneWithTimeZone(fromZonedTime(getNativeDate(dateFns), getTimeZone(dateFns)), "UTC"),
67
+ toISO: (dateFns) => formatISO(getNativeDate(dateFns)),
68
+ toMillis: (dateFns) => getNativeDate(dateFns).getTime(),
69
+ fromMillis: (millis, zone) => cloneWithTimeZone(new Date(millis), zone),
70
+ createUTC: (year, month, day, hour, minute, second) => cloneWithTimeZone(new Date(Date.UTC(year, month - 1, day, hour, minute, second)), "UTC"),
71
+ setZone: (dateFns, zoneName) => cloneWithTimeZone(toZonedTime(getNativeDate(dateFns), zoneName), zoneName),
72
+ isGreaterThanOrEqual: (date1, date2) => getNativeDate(date1) >= getNativeDate(date2),
72
73
  };
73
74
  }
@@ -1 +1 @@
1
- {"version":3,"file":"yongshen.d.ts","sourceRoot":"","sources":["../../src/core/yongshen.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,OAAO,EACZ,KAAK,YAAY,EAIlB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE3F,MAAM,WAAW,mBAAoB,SAAQ,KAAK,CAAC,iBAAiB,CAAC;CAAG;AAUxE,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,iBAAiB,GAAG,mBAAmB,CAGlF;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,YAAY,CAAC;IACtB,SAAS,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,mBAAmB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC,OAAO,EAAE;QAAE,UAAU,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACzE,cAAc,EAAE,YAAY,GAAG,IAAI,CAAC;IACpC,8BAA8B;IAC9B,kBAAkB,CAAC,EAAE;QACnB,OAAO,EAAE,YAAY,CAAC;QACtB,SAAS,EAAE,YAAY,GAAG,IAAI,CAAC;KAChC,CAAC;CACH;AAED,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAoLrC,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,cAAc,CA2FhB;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,cAAc,GAAG;IACnE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CA0BA"}
1
+ {"version":3,"file":"yongshen.d.ts","sourceRoot":"","sources":["../../src/core/yongshen.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,OAAO,EACZ,KAAK,YAAY,EAIlB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE3F,MAAM,WAAW,mBAAoB,SAAQ,KAAK,CAAC,iBAAiB,CAAC;CAAG;AAUxE,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,iBAAiB,GAAG,mBAAmB,CAGlF;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,YAAY,CAAC;IACtB,SAAS,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,mBAAmB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC,OAAO,EAAE;QAAE,UAAU,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACzE,cAAc,EAAE,YAAY,GAAG,IAAI,CAAC;IACpC,8BAA8B;IAC9B,kBAAkB,CAAC,EAAE;QACnB,OAAO,EAAE,YAAY,CAAC;QACtB,SAAS,EAAE,YAAY,GAAG,IAAI,CAAC;KAChC,CAAC;CACH;AAED,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAwNrC,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,cAAc,CA2FhB;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,cAAc,GAAG;IACnE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CA0BA"}
@@ -102,7 +102,7 @@ function getYokbuYongShen(dayMasterElement, level) {
102
102
  const secondary = dayMasterElement;
103
103
  return { primary, secondary };
104
104
  }
105
- function hasSpecialFormation(dayMasterElement, level, allElements) {
105
+ function countElements(elements) {
106
106
  const elementCounts = {
107
107
  wood: 0,
108
108
  fire: 0,
@@ -110,44 +110,56 @@ function hasSpecialFormation(dayMasterElement, level, allElements) {
110
110
  metal: 0,
111
111
  water: 0,
112
112
  };
113
- for (const elem of allElements) {
113
+ for (const elem of elements) {
114
114
  elementCounts[elem]++;
115
115
  }
116
- if (level.key === "extremelyWeak") {
117
- // Find dominant non-daymaster element
118
- let dominantElement = null;
119
- let maxCount = 0;
120
- for (const [elem, count] of Object.entries(elementCounts)) {
121
- if (count > maxCount && elem !== dayMasterElement) {
122
- maxCount = count;
123
- dominantElement = elem;
124
- }
125
- }
126
- if (dominantElement && maxCount >= 3) {
127
- // Determine specific 종격 type
128
- const dmControls = CONTROLS[dayMasterElement];
129
- const dmControlledBy = CONTROLLED_BY[dayMasterElement];
130
- const dmGenerates = GENERATES[dayMasterElement];
131
- let type = "종격";
132
- if (dominantElement === dmControls) {
133
- type = "종재격"; // Follow Wealth
134
- }
135
- else if (dominantElement === dmControlledBy) {
136
- type = "종살격"; // Follow Killings
137
- }
138
- else if (dominantElement === dmGenerates) {
139
- type = "종아격"; // Follow Children/Output
140
- }
141
- return { isSpecial: true, type, followElement: dominantElement };
116
+ return elementCounts;
117
+ }
118
+ function getDominantNonDayMasterElement(dayMasterElement, elementCounts) {
119
+ let dominantElement = null;
120
+ let maxCount = 0;
121
+ for (const [elem, count] of Object.entries(elementCounts)) {
122
+ if (count > maxCount && elem !== dayMasterElement) {
123
+ maxCount = count;
124
+ dominantElement = elem;
142
125
  }
143
126
  }
127
+ return { dominantElement, maxCount };
128
+ }
129
+ function getFollowFormationType(dayMasterElement, dominantElement) {
130
+ const followTypeByElement = {
131
+ [CONTROLS[dayMasterElement]]: "종재격",
132
+ [CONTROLLED_BY[dayMasterElement]]: "종살격",
133
+ [GENERATES[dayMasterElement]]: "종아격",
134
+ };
135
+ return followTypeByElement[dominantElement] ?? "종격";
136
+ }
137
+ function getExtremelyWeakSpecialFormation(dayMasterElement, elementCounts) {
138
+ const { dominantElement, maxCount } = getDominantNonDayMasterElement(dayMasterElement, elementCounts);
139
+ if (!dominantElement || maxCount < 3) {
140
+ return { isSpecial: false, type: null, followElement: null };
141
+ }
142
+ return {
143
+ isSpecial: true,
144
+ type: getFollowFormationType(dayMasterElement, dominantElement),
145
+ followElement: dominantElement,
146
+ };
147
+ }
148
+ function getExtremelyStrongSpecialFormation(dayMasterElement, elementCounts) {
149
+ const controllerElement = CONTROLLED_BY[dayMasterElement];
150
+ const hasController = elementCounts[controllerElement] > 0;
151
+ if (hasController) {
152
+ return { isSpecial: false, type: null, followElement: null };
153
+ }
154
+ return { isSpecial: true, type: "종강격", followElement: dayMasterElement };
155
+ }
156
+ function hasSpecialFormation(dayMasterElement, level, allElements) {
157
+ const elementCounts = countElements(allElements);
158
+ if (level.key === "extremelyWeak") {
159
+ return getExtremelyWeakSpecialFormation(dayMasterElement, elementCounts);
160
+ }
144
161
  if (level.key === "extremelyStrong") {
145
- // 종강격: extremely strong with no controllers present
146
- const controllerElement = CONTROLLED_BY[dayMasterElement];
147
- const hasController = elementCounts[controllerElement] > 0;
148
- if (!hasController) {
149
- return { isSpecial: true, type: "종강격", followElement: dayMasterElement };
150
- }
162
+ return getExtremelyStrongSpecialFormation(dayMasterElement, elementCounts);
151
163
  }
152
164
  return { isSpecial: false, type: null, followElement: null };
153
165
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gracefullight/saju",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Four Pillars (四柱) calculator with flexible date adapter support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",