Skip to content

Commit f802017

Browse files
committed
fix(saju): support plain Date in date-fns adapter
1 parent cfdc866 commit f802017

5 files changed

Lines changed: 113 additions & 53 deletions

File tree

packages/saju/README.ko.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,20 @@ import { getFourPillars, STANDARD_PRESET } from "@gracefullight/saju";
151151

152152
const adapter = await createDateFnsAdapter();
153153

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부터 시작
158156

159157
const result = getFourPillars(dt, {
160158
adapter,
161159
longitudeDeg: 126.9778,
162160
preset: STANDARD_PRESET,
163161
});
162+
163+
// 명시적인 타임존 메타데이터가 필요하면 래퍼 객체도 계속 사용할 수 있습니다
164+
const zonedDt = {
165+
date: new Date(1985, 4, 15, 14, 30),
166+
timeZone: "Asia/Seoul",
167+
};
164168
```
165169

166170
### 커스텀 날짜 어댑터

packages/saju/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,20 @@ import { getFourPillars, STANDARD_PRESET } from "@gracefullight/saju";
151151

152152
const adapter = await createDateFnsAdapter();
153153

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
158156

159157
const result = getFourPillars(dt, {
160158
adapter,
161159
longitudeDeg: 126.9778,
162160
preset: STANDARD_PRESET,
163161
});
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+
};
164168
```
165169

166170
### Custom Date Adapter

packages/saju/src/__tests__/date-fns-adapter.test.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import { beforeAll, describe, expect, it } from "vitest";
22
import type { DateAdapter } from "@/adapters/date-adapter";
3-
import { createDateFnsAdapter } from "@/adapters/date-fns";
4-
5-
interface DateFnsDate {
6-
date: Date;
7-
timeZone: string;
8-
}
3+
import { createDateFnsAdapter, type DateFnsDate } from "@/adapters/date-fns";
94

105
describe("date-fns Adapter", () => {
116
let adapter: DateAdapter<DateFnsDate>;
@@ -15,6 +10,14 @@ describe("date-fns Adapter", () => {
1510
});
1611

1712
describe("Basic date getters", () => {
13+
it("should accept plain Date values", () => {
14+
const dt = new Date(2000, 0, 1, 18, 0);
15+
expect(adapter.getYear(dt)).toBe(2000);
16+
expect(adapter.getMonth(dt)).toBe(1);
17+
expect(adapter.getDay(dt)).toBe(1);
18+
expect(adapter.getHour(dt)).toBe(18);
19+
});
20+
1821
it("should get year correctly", () => {
1922
const dt = { date: new Date(2000, 0, 1), timeZone: "Asia/Seoul" };
2023
expect(adapter.getYear(dt)).toBe(2000);
@@ -49,6 +52,11 @@ describe("date-fns Adapter", () => {
4952
const dt = { date: new Date(2000, 0, 1), timeZone: "Asia/Seoul" };
5053
expect(adapter.getZoneName(dt)).toBe("Asia/Seoul");
5154
});
55+
56+
it("should fall back to system timezone for plain Date values", () => {
57+
const dt = new Date(2000, 0, 1);
58+
expect(adapter.getZoneName(dt)).toBe(Intl.DateTimeFormat().resolvedOptions().timeZone);
59+
});
5260
});
5361

5462
describe("Date arithmetic", () => {

packages/saju/src/__tests__/saju.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { DateTime } from "luxon";
22
import { describe, expect, it } from "vitest";
3+
import { createDateFnsAdapter } from "@/adapters/date-fns";
34
import { createLuxonAdapter } from "@/adapters/luxon";
45
import { getSaju, STANDARD_PRESET } from "@/index";
56

@@ -194,4 +195,31 @@ describe("getSaju integration", () => {
194195
expect(result.solarTerms.prevJieDate).toBeDefined();
195196
expect(result.solarTerms.nextJieDate).toBeDefined();
196197
});
198+
199+
it("accepts plain Date with the date-fns adapter (Issue #64 regression)", async () => {
200+
const adapter = await createDateFnsAdapter();
201+
const dt = new Date(1990, 1, 1, 12, 10);
202+
203+
expect(() =>
204+
getSaju(dt, {
205+
adapter,
206+
longitudeDeg: 126.9778,
207+
gender: "male",
208+
preset: STANDARD_PRESET,
209+
}),
210+
).not.toThrow();
211+
212+
const result = getSaju(dt, {
213+
adapter,
214+
longitudeDeg: 126.9778,
215+
gender: "male",
216+
preset: STANDARD_PRESET,
217+
});
218+
219+
expect(result.pillars.year).toBeDefined();
220+
expect(result.pillars.month).toBeDefined();
221+
expect(result.pillars.day).toBeDefined();
222+
expect(result.pillars.hour).toBeDefined();
223+
expect(result.lunar.lunarYear).toBeGreaterThan(0);
224+
});
197225
});
Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,42 @@
11
import type { DateAdapter } from "@/adapters/date-adapter";
22

3-
interface DateFnsDate {
3+
export interface ZonedDateFnsDate {
44
date: Date;
55
timeZone: string;
66
}
77

8+
export type DateFnsDate = Date | ZonedDateFnsDate;
9+
10+
function isZonedDate(date: DateFnsDate): date is ZonedDateFnsDate {
11+
return (
12+
typeof date === "object" &&
13+
date !== null &&
14+
"date" in date &&
15+
date.date instanceof Date &&
16+
typeof date.timeZone === "string"
17+
);
18+
}
19+
20+
function getNativeDate(date: DateFnsDate): Date {
21+
return isZonedDate(date) ? date.date : date;
22+
}
23+
24+
function getSystemTimeZone(): string {
25+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
26+
}
27+
28+
function getTimeZone(date: DateFnsDate): string {
29+
return isZonedDate(date) ? date.timeZone : getSystemTimeZone();
30+
}
31+
32+
function cloneWithTimeZone(date: Date, timeZone: string): ZonedDateFnsDate {
33+
return { date, timeZone };
34+
}
35+
36+
function preserveInputShape(input: DateFnsDate, date: Date): DateFnsDate {
37+
return isZonedDate(input) ? cloneWithTimeZone(date, input.timeZone) : date;
38+
}
39+
840
export async function createDateFnsAdapter(): Promise<DateAdapter<DateFnsDate>> {
941
let addMinutes: typeof import("date-fns").addMinutes;
1042
let addDays: typeof import("date-fns").addDays;
@@ -42,43 +74,27 @@ export async function createDateFnsAdapter(): Promise<DateAdapter<DateFnsDate>>
4274
}
4375

4476
return {
45-
getYear: (dateFns) => getYear(dateFns.date),
46-
getMonth: (dateFns) => getMonth(dateFns.date) + 1,
47-
getDay: (dateFns) => getDate(dateFns.date),
48-
getHour: (dateFns) => getHours(dateFns.date),
49-
getMinute: (dateFns) => getMinutes(dateFns.date),
50-
getSecond: (dateFns) => getSeconds(dateFns.date),
51-
getZoneName: (dateFns) => dateFns.timeZone,
52-
plusMinutes: (dateFns, minutes) => ({
53-
date: addMinutes(dateFns.date, minutes),
54-
timeZone: dateFns.timeZone,
55-
}),
56-
plusDays: (dateFns, days) => ({
57-
date: addDays(dateFns.date, days),
58-
timeZone: dateFns.timeZone,
59-
}),
60-
minusDays: (dateFns, days) => ({
61-
date: subDays(dateFns.date, days),
62-
timeZone: dateFns.timeZone,
63-
}),
64-
toUTC: (dateFns) => ({
65-
date: fromZonedTime(dateFns.date, dateFns.timeZone),
66-
timeZone: "UTC",
67-
}),
68-
toISO: (dateFns) => formatISO(dateFns.date),
69-
toMillis: (dateFns) => dateFns.date.getTime(),
70-
fromMillis: (millis, zone) => ({
71-
date: new Date(millis),
72-
timeZone: zone,
73-
}),
74-
createUTC: (year, month, day, hour, minute, second) => ({
75-
date: new Date(Date.UTC(year, month - 1, day, hour, minute, second)),
76-
timeZone: "UTC",
77-
}),
78-
setZone: (dateFns, zoneName) => ({
79-
date: toZonedTime(dateFns.date, zoneName),
80-
timeZone: zoneName,
81-
}),
82-
isGreaterThanOrEqual: (date1, date2) => date1.date >= date2.date,
77+
getYear: (dateFns) => getYear(getNativeDate(dateFns)),
78+
getMonth: (dateFns) => getMonth(getNativeDate(dateFns)) + 1,
79+
getDay: (dateFns) => getDate(getNativeDate(dateFns)),
80+
getHour: (dateFns) => getHours(getNativeDate(dateFns)),
81+
getMinute: (dateFns) => getMinutes(getNativeDate(dateFns)),
82+
getSecond: (dateFns) => getSeconds(getNativeDate(dateFns)),
83+
getZoneName: (dateFns) => getTimeZone(dateFns),
84+
plusMinutes: (dateFns, minutes) =>
85+
preserveInputShape(dateFns, addMinutes(getNativeDate(dateFns), minutes)),
86+
plusDays: (dateFns, days) => preserveInputShape(dateFns, addDays(getNativeDate(dateFns), days)),
87+
minusDays: (dateFns, days) =>
88+
preserveInputShape(dateFns, subDays(getNativeDate(dateFns), days)),
89+
toUTC: (dateFns) =>
90+
cloneWithTimeZone(fromZonedTime(getNativeDate(dateFns), getTimeZone(dateFns)), "UTC"),
91+
toISO: (dateFns) => formatISO(getNativeDate(dateFns)),
92+
toMillis: (dateFns) => getNativeDate(dateFns).getTime(),
93+
fromMillis: (millis, zone) => cloneWithTimeZone(new Date(millis), zone),
94+
createUTC: (year, month, day, hour, minute, second) =>
95+
cloneWithTimeZone(new Date(Date.UTC(year, month - 1, day, hour, minute, second)), "UTC"),
96+
setZone: (dateFns, zoneName) =>
97+
cloneWithTimeZone(toZonedTime(getNativeDate(dateFns), zoneName), zoneName),
98+
isGreaterThanOrEqual: (date1, date2) => getNativeDate(date1) >= getNativeDate(date2),
8399
};
84100
}

0 commit comments

Comments
 (0)