ko-chrono 0.1.0 → 0.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.
- package/README.md +211 -66
- package/dist/cjs/event.d.ts +18 -0
- package/dist/cjs/event.js +31 -0
- package/dist/cjs/index.d.ts +55 -0
- package/dist/cjs/index.js +189 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/parsers/absoluteDate.d.ts +3 -0
- package/dist/cjs/parsers/absoluteDate.js +95 -0
- package/dist/cjs/parsers/duration.d.ts +3 -0
- package/dist/cjs/parsers/duration.js +45 -0
- package/dist/cjs/parsers/holiday.d.ts +3 -0
- package/dist/cjs/parsers/holiday.js +64 -0
- package/dist/cjs/parsers/offset.d.ts +3 -0
- package/dist/cjs/parsers/offset.js +64 -0
- package/dist/cjs/parsers/recurrence.d.ts +5 -0
- package/dist/cjs/parsers/recurrence.js +117 -0
- package/dist/cjs/parsers/relativeDay.d.ts +3 -0
- package/dist/cjs/parsers/relativeDay.js +26 -0
- package/dist/cjs/parsers/relativeMonth.d.ts +3 -0
- package/dist/cjs/parsers/relativeMonth.js +30 -0
- package/dist/cjs/parsers/relativeYear.d.ts +3 -0
- package/dist/cjs/parsers/relativeYear.js +30 -0
- package/dist/cjs/parsers/time.d.ts +3 -0
- package/dist/cjs/parsers/time.js +149 -0
- package/dist/cjs/parsers/weekday.d.ts +3 -0
- package/dist/cjs/parsers/weekday.js +109 -0
- package/dist/cjs/resolve.d.ts +21 -0
- package/dist/cjs/resolve.js +414 -0
- package/dist/cjs/types.d.ts +105 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/util/lunarHolidays.d.ts +3 -0
- package/dist/cjs/util/lunarHolidays.js +16 -0
- package/dist/cjs/util/number.d.ts +5 -0
- package/dist/cjs/util/number.js +42 -0
- package/dist/cjs/util/timezone.d.ts +4 -0
- package/dist/cjs/util/timezone.js +55 -0
- package/dist/event.d.ts +18 -0
- package/dist/event.js +28 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.js +51 -6
- package/dist/parsers/absoluteDate.js +6 -2
- package/dist/parsers/duration.js +4 -2
- package/dist/parsers/holiday.d.ts +3 -0
- package/dist/parsers/holiday.js +60 -0
- package/dist/parsers/offset.js +6 -3
- package/dist/parsers/recurrence.d.ts +5 -0
- package/dist/parsers/recurrence.js +112 -0
- package/dist/parsers/time.js +19 -2
- package/dist/parsers/weekday.js +10 -0
- package/dist/resolve.d.ts +2 -2
- package/dist/resolve.js +132 -13
- package/dist/types.d.ts +33 -0
- package/dist/util/lunarHolidays.d.ts +3 -0
- package/dist/util/lunarHolidays.js +13 -0
- package/dist/util/timezone.d.ts +4 -0
- package/dist/util/timezone.js +51 -0
- package/package.json +13 -5
package/dist/resolve.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { toRRule } from './parsers/recurrence.js';
|
|
2
|
+
import { LUNAR_HOLIDAYS } from './util/lunarHolidays.js';
|
|
1
3
|
function startOfWeek(d, weekStart) {
|
|
2
4
|
const r = new Date(d);
|
|
3
5
|
r.setHours(0, 0, 0, 0);
|
|
@@ -9,6 +11,22 @@ function startOfWeek(d, weekStart) {
|
|
|
9
11
|
function lastDayOfMonth(y, m0) {
|
|
10
12
|
return new Date(y, m0 + 1, 0).getDate();
|
|
11
13
|
}
|
|
14
|
+
/** 그 달의 N번째 요일이 며칠인지. nth = -1이면 마지막. 없으면 null ("다섯째 주 토요일") */
|
|
15
|
+
function nthWeekdayOfMonth(y, m0, weekday, nth) {
|
|
16
|
+
const last = lastDayOfMonth(y, m0);
|
|
17
|
+
let day = 1 + ((weekday - new Date(y, m0, 1).getDay() + 7) % 7);
|
|
18
|
+
if (nth > 0)
|
|
19
|
+
day += (nth - 1) * 7;
|
|
20
|
+
else
|
|
21
|
+
while (day + 7 <= last)
|
|
22
|
+
day += 7;
|
|
23
|
+
return day > last ? null : day;
|
|
24
|
+
}
|
|
25
|
+
/** 규칙 20: 오전/오후 없는 1~6시는 오후 ("3시에 보자" → 15시). 7~11시는 그대로 */
|
|
26
|
+
function clockHour(c) {
|
|
27
|
+
const h = c.hour;
|
|
28
|
+
return !c.meridiem && h >= 1 && h <= 6 ? h + 12 : h;
|
|
29
|
+
}
|
|
12
30
|
/** 기준일의 "일"을 유지하되 그 달에 없으면 말일로 (2월 29일 + 1년 → 2월 28일) */
|
|
13
31
|
function sameDayIn(y, m0, day) {
|
|
14
32
|
return new Date(y, m0, Math.min(day, lastDayOfMonth(y, m0)));
|
|
@@ -96,13 +114,45 @@ function hasConflict(a, b) {
|
|
|
96
114
|
export function resolve(group, opts) {
|
|
97
115
|
const c = merge(group.map((f) => f.components));
|
|
98
116
|
const ref = opts.referenceDate;
|
|
117
|
+
// "내일 이 시간" → 기준 시각의 시:분. "다음 주 이 시간"은 7일 뒤 (그 주 월요일이 아니라)
|
|
118
|
+
if (c.sameTime && c.hour === undefined) {
|
|
119
|
+
c.hour = ref.getHours();
|
|
120
|
+
c.minute = ref.getMinutes();
|
|
121
|
+
c.meridiem = true;
|
|
122
|
+
if (c.weekOffset !== undefined && c.weekday === undefined)
|
|
123
|
+
c.sameDay = true;
|
|
124
|
+
}
|
|
125
|
+
if (c.recur)
|
|
126
|
+
return resolveRecurrence(group, c, opts);
|
|
99
127
|
let d = new Date(ref);
|
|
100
128
|
d.setSeconds(0, 0);
|
|
101
129
|
let dateSet = false;
|
|
102
130
|
const hasTime = c.hour !== undefined || c.hourOffset !== undefined || c.minuteOffset !== undefined;
|
|
103
|
-
//
|
|
131
|
+
// 0. 음력 명절 (규칙 29): 연도를 말했으면 그 해, 아니면 올해 것이 (연휴까지) 지났을 때 내년.
|
|
132
|
+
// 공식 자료가 없는 연도는 null
|
|
104
133
|
const hasYear = c.year !== undefined || c.yearOffset !== undefined;
|
|
105
|
-
if (
|
|
134
|
+
if (c.lunar) {
|
|
135
|
+
const said = c.year ?? (c.yearOffset !== undefined ? ref.getFullYear() + c.yearOffset : undefined);
|
|
136
|
+
const tail = (c.dayOffset ?? 0) + (c.durationDays ? c.durationDays - 1 : 0);
|
|
137
|
+
const years = said !== undefined ? [said] : [ref.getFullYear(), ref.getFullYear() + 1];
|
|
138
|
+
let found = null;
|
|
139
|
+
for (const y of years) {
|
|
140
|
+
const md = LUNAR_HOLIDAYS[y]?.[c.lunar];
|
|
141
|
+
if (!md)
|
|
142
|
+
break;
|
|
143
|
+
const lastMoment = new Date(y, md[0] - 1, md[1] + tail, 23, 59);
|
|
144
|
+
if (said !== undefined || lastMoment >= ref) {
|
|
145
|
+
found = new Date(y, md[0] - 1, md[1]);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!found)
|
|
150
|
+
return null;
|
|
151
|
+
d = found;
|
|
152
|
+
dateSet = true;
|
|
153
|
+
}
|
|
154
|
+
// 1. 절대 날짜
|
|
155
|
+
if (!c.lunar && (hasYear || c.month !== undefined || c.day !== undefined)) {
|
|
106
156
|
let y = c.year ?? ref.getFullYear() + (c.yearOffset ?? 0);
|
|
107
157
|
// "작년", "내년"처럼 연도만 있으면 1월 1일. "내년 이맘때", "1년 뒤"는 오늘 날짜 유지
|
|
108
158
|
const m0 = c.month !== undefined ? c.month - 1 : hasYear && !c.sameDay ? 0 : ref.getMonth();
|
|
@@ -140,15 +190,9 @@ export function resolve(group, opts) {
|
|
|
140
190
|
const y = dateSet ? d.getFullYear() : ref.getFullYear();
|
|
141
191
|
const m0 = dateSet ? d.getMonth() : ref.getMonth();
|
|
142
192
|
const target = c.weekday ?? (opts.weekStart === 'monday' ? 1 : 0);
|
|
143
|
-
const
|
|
144
|
-
let day = 1 + ((target - new Date(y, m0, 1).getDay() + 7) % 7);
|
|
145
|
-
if (c.nthWeek > 0)
|
|
146
|
-
day += (c.nthWeek - 1) * 7;
|
|
147
|
-
else
|
|
148
|
-
while (day + 7 <= last)
|
|
149
|
-
day += 7;
|
|
193
|
+
const day = nthWeekdayOfMonth(y, m0, target, c.nthWeek);
|
|
150
194
|
// "11월 다섯째 주 토요일"처럼 없는 날짜는 null
|
|
151
|
-
if (day
|
|
195
|
+
if (day === null)
|
|
152
196
|
return null;
|
|
153
197
|
d = new Date(y, m0, day);
|
|
154
198
|
dateSet = true;
|
|
@@ -202,8 +246,7 @@ export function resolve(group, opts) {
|
|
|
202
246
|
}
|
|
203
247
|
// 6. 시각
|
|
204
248
|
if (c.hour !== undefined) {
|
|
205
|
-
|
|
206
|
-
const hour = !c.meridiem && c.hour >= 1 && c.hour <= 6 ? c.hour + 12 : c.hour;
|
|
249
|
+
const hour = clockHour(c);
|
|
207
250
|
if (!dateSet) {
|
|
208
251
|
d = new Date(ref);
|
|
209
252
|
// 시간만 있고 날짜가 없으면: 오늘, 이미 지났으면 내일
|
|
@@ -232,6 +275,9 @@ export function resolve(group, opts) {
|
|
|
232
275
|
else {
|
|
233
276
|
return null;
|
|
234
277
|
}
|
|
278
|
+
// 안전장치: 어떤 경로로든 계산이 깨진 날짜는 내보내지 않는다
|
|
279
|
+
if (Number.isNaN(d.getTime()))
|
|
280
|
+
return null;
|
|
235
281
|
const first = group[0];
|
|
236
282
|
const last = group[group.length - 1];
|
|
237
283
|
// text는 parseAll()에서 원문 slice로 채운다. _end는 그때 쓰는 내부 값.
|
|
@@ -260,8 +306,9 @@ export function resolve(group, opts) {
|
|
|
260
306
|
// "~까지", "~이내": 시작은 기준 시각, 끝은 해당 시점(시간 없으면 그날 23:59)
|
|
261
307
|
// "~내내": 시작은 그 기간의 시작, 끝은 기간의 끝
|
|
262
308
|
result.end = hasTime ? new Date(d) : periodEnd(d, c);
|
|
309
|
+
// 규칙 30: "어제까지"처럼 이미 지난 마감은 구간이 아니라 그 시점 (date = end)
|
|
263
310
|
if (c.isEnd)
|
|
264
|
-
result.date = new Date(ref);
|
|
311
|
+
result.date = result.end < ref ? new Date(result.end) : new Date(ref);
|
|
265
312
|
}
|
|
266
313
|
return result;
|
|
267
314
|
}
|
|
@@ -277,6 +324,78 @@ function periodEnd(d, c) {
|
|
|
277
324
|
end.setHours(23, 59, 0, 0);
|
|
278
325
|
return end;
|
|
279
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* 반복 일정 (규칙 27): date = 기준 시각 이후 첫 발생.
|
|
329
|
+
* 시각이 있으면 지난 시각은 건너뛰고, 날짜만 있으면 오늘도 포함한다.
|
|
330
|
+
* "매달 31일"은 31일이 없는 달을 건너뛴다 (RRULE과 같은 동작).
|
|
331
|
+
*/
|
|
332
|
+
function resolveRecurrence(group, c, opts) {
|
|
333
|
+
const r = c.recur;
|
|
334
|
+
const ref = opts.referenceDate;
|
|
335
|
+
const hasTime = c.hour !== undefined;
|
|
336
|
+
const today = new Date(ref);
|
|
337
|
+
today.setHours(0, 0, 0, 0);
|
|
338
|
+
const at = (day) => {
|
|
339
|
+
const x = new Date(day);
|
|
340
|
+
x.setHours(hasTime ? clockHour(c) : opts.defaultHour, hasTime ? c.minute ?? 0 : 0, 0, 0);
|
|
341
|
+
return x;
|
|
342
|
+
};
|
|
343
|
+
const ok = (x) => (hasTime ? x >= ref : x >= today);
|
|
344
|
+
let d = null;
|
|
345
|
+
if (r.freq === 'MINUTELY' || r.freq === 'HOURLY') {
|
|
346
|
+
d = new Date(ref);
|
|
347
|
+
d.setSeconds(0, 0);
|
|
348
|
+
d.setMinutes(d.getMinutes() + r.interval * (r.freq === 'HOURLY' ? 60 : 1));
|
|
349
|
+
}
|
|
350
|
+
else if (r.freq === 'DAILY') {
|
|
351
|
+
d = at(today);
|
|
352
|
+
if (!ok(d))
|
|
353
|
+
d.setDate(d.getDate() + 1);
|
|
354
|
+
}
|
|
355
|
+
else if (r.freq === 'WEEKLY') {
|
|
356
|
+
for (let i = 0; i < 14 && !d; i++) {
|
|
357
|
+
const day = new Date(today.getFullYear(), today.getMonth(), today.getDate() + i);
|
|
358
|
+
if (r.byWeekday?.includes(day.getDay()) && ok(at(day)))
|
|
359
|
+
d = at(day);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
else if (r.freq === 'MONTHLY') {
|
|
363
|
+
for (let k = 0; k < 24 && !d; k++) {
|
|
364
|
+
const y = today.getFullYear(), m0 = today.getMonth() + k;
|
|
365
|
+
const dom = r.byWeekdayNth !== undefined
|
|
366
|
+
? nthWeekdayOfMonth(y, m0, r.byWeekday[0], r.byWeekdayNth)
|
|
367
|
+
: r.byMonthDay === -1 ? lastDayOfMonth(y, m0) : r.byMonthDay;
|
|
368
|
+
if (dom === null || dom > lastDayOfMonth(y, m0))
|
|
369
|
+
continue;
|
|
370
|
+
const x = at(new Date(y, m0, dom));
|
|
371
|
+
if (ok(x))
|
|
372
|
+
d = x;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
// YEARLY: 2월 29일은 다음 윤년까지
|
|
377
|
+
for (let k = 0; k < 9 && !d; k++) {
|
|
378
|
+
const y = today.getFullYear() + k, m0 = r.byMonth - 1;
|
|
379
|
+
if (r.byMonthDay > lastDayOfMonth(y, m0))
|
|
380
|
+
continue;
|
|
381
|
+
const x = at(new Date(y, m0, r.byMonthDay));
|
|
382
|
+
if (ok(x))
|
|
383
|
+
d = x;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!d)
|
|
387
|
+
return null;
|
|
388
|
+
const last = group[group.length - 1];
|
|
389
|
+
const result = {
|
|
390
|
+
date: d,
|
|
391
|
+
text: '',
|
|
392
|
+
index: group[0].index,
|
|
393
|
+
hasTime: hasTime || r.freq === 'HOURLY' || r.freq === 'MINUTELY',
|
|
394
|
+
recurrence: { ...r, rrule: toRRule(r) },
|
|
395
|
+
};
|
|
396
|
+
result._end = last.index + last.text.length;
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
280
399
|
function partDay(y, m0, part) {
|
|
281
400
|
if (part === 'start')
|
|
282
401
|
return 1;
|
package/dist/types.d.ts
CHANGED
|
@@ -6,7 +6,15 @@ export interface ParseOptions {
|
|
|
6
6
|
weekStart?: WeekStart;
|
|
7
7
|
/** 시간이 명시되지 않았을 때 넣을 시(hour). 기본값: 0 (00:00) */
|
|
8
8
|
defaultHour?: number;
|
|
9
|
+
/**
|
|
10
|
+
* "오후 3시"를 어느 시간대의 3시로 볼지. IANA 이름 ('Asia/Seoul', 'UTC', 'America/New_York').
|
|
11
|
+
* 기본값: 실행 환경의 시간대. 서버(UTC)에서 한국 사용자 문장을 처리할 땐 'Asia/Seoul'을 넘긴다.
|
|
12
|
+
* 없는 이름이면 RangeError.
|
|
13
|
+
*/
|
|
14
|
+
timezone?: string;
|
|
9
15
|
}
|
|
16
|
+
/** resolve()가 쓰는 옵션. timezone은 parseAll()이 앞뒤에서 처리하므로 빠진다. */
|
|
17
|
+
export type ResolveOptions = Required<Omit<ParseOptions, 'timezone'>>;
|
|
10
18
|
/** 파서들이 추출하는 중간 결과. 날짜/시간 중 아는 것만 채운다. */
|
|
11
19
|
export interface Components {
|
|
12
20
|
year?: number;
|
|
@@ -26,6 +34,12 @@ export interface Components {
|
|
|
26
34
|
weekdayDir?: 'past' | 'next';
|
|
27
35
|
/** "1년 뒤", "내년 이맘때": 연/월/주 이동 시 기준일의 날짜를 유지 */
|
|
28
36
|
sameDay?: boolean;
|
|
37
|
+
/** "내일 이 시간": 기준 시각의 시:분을 쓴다 */
|
|
38
|
+
sameTime?: boolean;
|
|
39
|
+
/** 음력 명절 (설날·추석·부처님 오신 날). 양력 날짜는 resolve가 util/lunarHolidays 표에서 찾는다 */
|
|
40
|
+
lunar?: 'seollal' | 'chuseok' | 'buddha';
|
|
41
|
+
/** "매주 월요일", "매달 15일": 반복 규칙 (rrule은 resolve가 만든다) */
|
|
42
|
+
recur?: Omit<Recurrence, 'rrule'>;
|
|
29
43
|
/** 연 단위 이동 (내년=+1). 지정되면 "지난 월일 → 내년" 규칙을 적용하지 않는다 */
|
|
30
44
|
yearOffset?: number;
|
|
31
45
|
/** 월 단위 이동 (다음달=+1) */
|
|
@@ -52,6 +66,23 @@ export interface Components {
|
|
|
52
66
|
/** 이 조각이 "~부터" 시작점인지 */
|
|
53
67
|
isStart?: boolean;
|
|
54
68
|
}
|
|
69
|
+
export type Frequency = 'MINUTELY' | 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
|
|
70
|
+
/** 반복 일정. 필드 이름과 값은 RFC 5545 RRULE을 따른다 */
|
|
71
|
+
export interface Recurrence {
|
|
72
|
+
freq: Frequency;
|
|
73
|
+
/** 몇 번째마다 (격주 = 2) */
|
|
74
|
+
interval: number;
|
|
75
|
+
/** 요일들. 0=일 … 6=토 */
|
|
76
|
+
byWeekday?: number[];
|
|
77
|
+
/** 매달 N번째 요일 ("마지막 주 금요일" = -1). byWeekday와 함께 쓴다 */
|
|
78
|
+
byWeekdayNth?: number;
|
|
79
|
+
/** 매달 며칠 (말일 = -1) */
|
|
80
|
+
byMonthDay?: number;
|
|
81
|
+
/** 매년 몇 월 */
|
|
82
|
+
byMonth?: number;
|
|
83
|
+
/** 'FREQ=WEEKLY;BYDAY=MO,WE,FR'. date를 DTSTART로 함께 쓰면 캘린더에 그대로 넣을 수 있다 */
|
|
84
|
+
rrule: string;
|
|
85
|
+
}
|
|
55
86
|
export interface Fragment {
|
|
56
87
|
text: string;
|
|
57
88
|
index: number;
|
|
@@ -68,5 +99,7 @@ export interface ParseResult {
|
|
|
68
99
|
index: number;
|
|
69
100
|
/** 시간이 명시되었는지 */
|
|
70
101
|
hasTime: boolean;
|
|
102
|
+
/** 반복 일정이면 그 규칙. date는 기준 시각 이후 첫 발생 */
|
|
103
|
+
recurrence?: Recurrence;
|
|
71
104
|
}
|
|
72
105
|
export type Parser = (text: string) => Fragment[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// 음력 명절의 양력 날짜. 런타임 의존성 없이 음력 변환을 하려면 표가 필요하다.
|
|
2
|
+
//
|
|
3
|
+
// 출처: 한국천문연구원 천문우주지식정보 "월별 양음력 대조" (astro.kasi.re.kr/life/post/calendarData)
|
|
4
|
+
// 2025년은 관공서 공휴일 목록. 2026년 부처님 오신 날·추석은 공휴일 목록으로 교차 확인.
|
|
5
|
+
// KASI는 2~3년 앞까지만 공식 자료를 낸다 (2026-10 기준 2028년까지). 새 연도가 공개되면 한 줄 추가할 것.
|
|
6
|
+
// 표에 없는 연도는 추측하지 않고 null (규칙 10). 한국 설날은 한국 표준시 기준이라 중국 춘절과 하루 다를 수 있다.
|
|
7
|
+
/** 연도 → [월, 일] (양력) */
|
|
8
|
+
export const LUNAR_HOLIDAYS = {
|
|
9
|
+
2025: { seollal: [1, 29], buddha: [5, 5], chuseok: [10, 6] },
|
|
10
|
+
2026: { seollal: [2, 17], buddha: [5, 24], chuseok: [9, 25] },
|
|
11
|
+
2027: { seollal: [2, 7], buddha: [5, 13], chuseok: [9, 15] },
|
|
12
|
+
2028: { seollal: [1, 27], buddha: [5, 2], chuseok: [10, 3] },
|
|
13
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// IANA 시간대 변환. 런타임 의존성 없이 Intl만 쓴다.
|
|
2
|
+
//
|
|
3
|
+
// resolve.ts는 Date의 로컬 getter/setter로 계산한다. timezone 옵션이 있으면
|
|
4
|
+
// 기준 시각을 그 시간대의 벽시계 시각(로컬 필드가 같은 Date)으로 바꿔 계산하고,
|
|
5
|
+
// 결과의 벽시계 시각을 다시 실제 시점으로 되돌린다.
|
|
6
|
+
const formatters = new Map();
|
|
7
|
+
function formatter(timeZone) {
|
|
8
|
+
let f = formatters.get(timeZone);
|
|
9
|
+
if (!f) {
|
|
10
|
+
// 없는 시간대면 여기서 RangeError
|
|
11
|
+
f = new Intl.DateTimeFormat('en-US', {
|
|
12
|
+
timeZone,
|
|
13
|
+
hourCycle: 'h23',
|
|
14
|
+
year: 'numeric',
|
|
15
|
+
month: 'numeric',
|
|
16
|
+
day: 'numeric',
|
|
17
|
+
hour: 'numeric',
|
|
18
|
+
minute: 'numeric',
|
|
19
|
+
second: 'numeric',
|
|
20
|
+
});
|
|
21
|
+
formatters.set(timeZone, f);
|
|
22
|
+
}
|
|
23
|
+
return f;
|
|
24
|
+
}
|
|
25
|
+
/** 시점 t에서 그 시간대의 벽시계 필드 */
|
|
26
|
+
function fieldsAt(t, timeZone) {
|
|
27
|
+
const parts = {};
|
|
28
|
+
for (const p of formatter(timeZone).formatToParts(new Date(t))) {
|
|
29
|
+
if (p.type !== 'literal')
|
|
30
|
+
parts[p.type] = +p.value;
|
|
31
|
+
}
|
|
32
|
+
return [parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second];
|
|
33
|
+
}
|
|
34
|
+
/** 시점 t에서 그 시간대의 UTC 오프셋(ms). 서울 = +9h */
|
|
35
|
+
function offsetAt(t, timeZone) {
|
|
36
|
+
const [y, m0, d, h, mi, s] = fieldsAt(t, timeZone);
|
|
37
|
+
return Date.UTC(y, m0, d, h, mi, s) - (t - (t % 1000));
|
|
38
|
+
}
|
|
39
|
+
/** 실제 시점 → 그 시간대의 벽시계 시각을 로컬 필드로 갖는 Date */
|
|
40
|
+
export function toWallClock(instant, timeZone) {
|
|
41
|
+
const [y, m0, d, h, mi, s] = fieldsAt(instant.getTime(), timeZone);
|
|
42
|
+
return new Date(y, m0, d, h, mi, s, instant.getMilliseconds());
|
|
43
|
+
}
|
|
44
|
+
/** 로컬 필드를 그 시간대의 벽시계 시각으로 보고 → 실제 시점 */
|
|
45
|
+
export function fromWallClock(wall, timeZone) {
|
|
46
|
+
const asUtc = Date.UTC(wall.getFullYear(), wall.getMonth(), wall.getDate(), wall.getHours(), wall.getMinutes(), wall.getSeconds(), wall.getMilliseconds());
|
|
47
|
+
// 오프셋은 시점에 따라 달라지므로(서머타임) 한 번 더 맞춘다
|
|
48
|
+
const first = offsetAt(asUtc, timeZone);
|
|
49
|
+
const second = offsetAt(asUtc - first, timeZone);
|
|
50
|
+
return new Date(asUtc - second);
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ko-chrono",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "한국어 자연어 날짜/시간 파서 — '다음주 화요일 오후 3시' → Date",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
6
|
+
"main": "./dist/cjs/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
7
8
|
"types": "./dist/index.d.ts",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": {
|
|
10
|
-
"
|
|
11
|
-
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/cjs/index.d.ts",
|
|
17
|
+
"default": "./dist/cjs/index.js"
|
|
18
|
+
}
|
|
12
19
|
}
|
|
13
20
|
},
|
|
21
|
+
"sideEffects": false,
|
|
14
22
|
"files": ["dist", "README.md", "LICENSE"],
|
|
15
23
|
"scripts": {
|
|
16
|
-
"build": "tsc -p tsconfig.build.json",
|
|
24
|
+
"build": "tsc -p tsconfig.build.json && tsc -p tsconfig.cjs.json && node scripts/mark-cjs.mjs",
|
|
17
25
|
"test": "vitest run",
|
|
18
26
|
"test:watch": "vitest",
|
|
19
27
|
"typecheck": "tsc --noEmit",
|