ko-chrono 0.3.1 → 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.
- package/README.en.md +146 -0
- package/README.md +112 -151
- package/dist/cjs/ics.d.ts +30 -0
- package/dist/cjs/ics.js +80 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +93 -3
- package/dist/cjs/parsers/absoluteDate.js +2 -2
- package/dist/cjs/parsers/holiday.js +1 -0
- package/dist/cjs/parsers/offset.js +2 -0
- package/dist/cjs/parsers/recurrence.d.ts +6 -2
- package/dist/cjs/parsers/recurrence.js +15 -2
- package/dist/cjs/parsers/time.js +4 -0
- package/dist/cjs/parsers/weekday.js +12 -8
- package/dist/cjs/resolve.js +31 -5
- package/dist/cjs/types.d.ts +9 -1
- package/dist/ics.d.ts +30 -0
- package/dist/ics.js +77 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +92 -4
- package/dist/parsers/absoluteDate.js +2 -2
- package/dist/parsers/holiday.js +1 -0
- package/dist/parsers/offset.js +2 -0
- package/dist/parsers/recurrence.d.ts +6 -2
- package/dist/parsers/recurrence.js +15 -2
- package/dist/parsers/time.js +4 -0
- package/dist/parsers/weekday.js +12 -8
- package/dist/resolve.js +31 -5
- package/dist/types.d.ts +9 -1
- package/package.json +18 -4
package/README.en.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# ko-chrono
|
|
4
|
+
|
|
5
|
+
**Find dates and times in Korean sentences and turn them into `Date`**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/ko-chrono)
|
|
8
|
+
[](https://github.com/jwn7/ko-chrono/actions/workflows/ci.yml)
|
|
9
|
+
[](./package.json)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
|
|
12
|
+
<a href="https://jwn7.github.io/ko-chrono/"><img alt="Playground demo: the date part of a Korean sentence is highlighted, and recurrences, ambiguous times, deadlines and holidays are shown as cards" src="./docs/demo.gif" width="720"></a>
|
|
13
|
+
|
|
14
|
+
**[▶ Try it in your browser](https://jwn7.github.io/ko-chrono/)** · [한국어](./README.md)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<br>
|
|
19
|
+
|
|
20
|
+
`ko-chrono` is a natural-language date parser for **Korean**, in the spirit of [chrono-node](https://github.com/wanasit/chrono) (which supports English, Japanese, Chinese and others, but not Korean).
|
|
21
|
+
It finds expressions like "다음주 화요일 오후 3시" (next Tuesday 3 PM) inside a sentence and resolves them against a reference date.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install ko-chrono
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Zero runtime dependencies. ESM and CommonJS, browser and Node.
|
|
28
|
+
|
|
29
|
+
## Examples
|
|
30
|
+
|
|
31
|
+
> All results below use the reference time **2026-10-30 (Fri) 14:00**.
|
|
32
|
+
|
|
33
|
+
### Find a meeting time in a chat message
|
|
34
|
+
|
|
35
|
+
Surrounding words are skipped. `text` and `index` tell you where the date was, so you can highlight it.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { parse } from 'ko-chrono';
|
|
39
|
+
|
|
40
|
+
const r = parse('그럼 다음주 수요일 저녁 7시에 강남에서 볼까?'); // "Then shall we meet in Gangnam next Wed at 7 PM?"
|
|
41
|
+
|
|
42
|
+
r.date; // 2026-11-04 19:00
|
|
43
|
+
r.text; // '다음주 수요일 저녁 7시'
|
|
44
|
+
r.index; // 3
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Quick-add for a to-do app
|
|
48
|
+
|
|
49
|
+
`parseEvent` splits a line into a **title** and a **time**. Deadlines ("\~까지", "by …") go into `end`.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { parseEvent } from 'ko-chrono';
|
|
53
|
+
|
|
54
|
+
parseEvent('다음주 금요일까지 보고서 제출'); // "Submit the report by next Friday"
|
|
55
|
+
// { title: '보고서 제출', end: 2026-11-06 23:59, hasTime: false, ... }
|
|
56
|
+
|
|
57
|
+
parseEvent('내일 오후 3시에 강남역에서 친구 만나기'); // "Meet a friend at Gangnam station tomorrow 3 PM"
|
|
58
|
+
// { title: '강남역에서 친구 만나기', date: 2026-10-31 15:00, hasTime: true }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Recurring events → calendar
|
|
62
|
+
|
|
63
|
+
Recurrences come out as an [RFC 5545 RRULE](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html), and `toICS` builds a calendar file you can import into Google, Apple or Outlook Calendar.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { parse, parseEvent, toICS } from 'ko-chrono';
|
|
67
|
+
|
|
68
|
+
parse('매주 월수금 오전 7시 운동')?.recurrence?.rrule; // every Mon/Wed/Fri 7 AM → 'FREQ=WEEKLY;BYDAY=MO,WE,FR'
|
|
69
|
+
parse('매달 마지막 주 금요일 회식')?.recurrence?.rrule; // last Friday of every month → 'FREQ=MONTHLY;BYDAY=-1FR'
|
|
70
|
+
parse('다음달까지 매주 월요일 9시')?.recurrence?.rrule; // every Monday 9 AM until next month
|
|
71
|
+
// → 'FREQ=WEEKLY;BYDAY=MO;UNTIL=20261130T235900'
|
|
72
|
+
|
|
73
|
+
toICS(parseEvent('다음달까지 매주 월요일 9시 팀 회의'), { timezone: 'Asia/Seoul' });
|
|
74
|
+
// BEGIN:VCALENDAR … SUMMARY:팀 회의 … DTSTART;TZID=Asia/Seoul:20261102T090000 …
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Tell me when it's ambiguous
|
|
78
|
+
|
|
79
|
+
When a sentence can be read more than one way (e.g. "3시" with no AM/PM), you get the default reading plus the alternatives, so you can ask the user or hand only those sentences to an LLM.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const r = parse('3시에 보자'); // "See you at 3"
|
|
83
|
+
|
|
84
|
+
r.date; // today 15:00 (1–6 o'clock without AM/PM is read as PM)
|
|
85
|
+
r.ambiguity; // { reason: 'meridiem', alternatives: [tomorrow 03:00] }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### On a server
|
|
89
|
+
|
|
90
|
+
Servers usually run in UTC, so "오후 3시" (3 PM) would be 3 PM UTC. Pass `timezone`.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
parse('내일 오후 3시', { timezone: 'Asia/Seoul' }); // 3 PM in Seoul, whatever the server's timezone
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## What it understands
|
|
97
|
+
|
|
98
|
+
| Kind | Examples |
|
|
99
|
+
|---|---|
|
|
100
|
+
| Relative days | 오늘 (today) · 내일 (tomorrow) · 모레 · 글피 · 어제 · 그저께 · 낼 |
|
|
101
|
+
| N later / ago | 3일 뒤 · 2주 전 · 한 시간 반 뒤 · 30분 내로 |
|
|
102
|
+
| Weeks & weekdays | 다음주 화요일 · 담주 월욜 (slang) · 지난 금요일 · 이번 주말 · 11월 첫째 주 월요일 |
|
|
103
|
+
| Dates | 11월 5일 · 2026-11-05 · 11/5 · 26.11.5 · 11/3(화) |
|
|
104
|
+
| Months & years | 이번달 말 · 다음달 중순 · 내년 초 · 연말 · 내년 이맘때 |
|
|
105
|
+
| Times | 오후 3시 · 3시 반 · 15:30 · 3pm · 정오 · 자정 · 점심 · 퇴근 |
|
|
106
|
+
| Deadlines & ranges | \~까지 · \~안에 · 이번 주 안에 · A부터 B까지 · 3\~5시 · 오늘부터 3일간 |
|
|
107
|
+
| Holidays | 크리스마스 · 설 연휴 · 추석 전날 · 부처님 오신 날 · 음력 3월 3일 (lunar) |
|
|
108
|
+
| Business words | 금일 · 익일 · 차주 · 익월 초 · 금월 말까지 |
|
|
109
|
+
| Recurrence | 매일 · 매주 월수금 · 격주 · 매달 15일 · 매달 마지막 주 금요일 · 다음달까지 · 총 8회 |
|
|
110
|
+
|
|
111
|
+
It is deliberately conservative: when in doubt it returns `null` rather than a wrong date ("3일 연속" = "3 days in a row", "주 5일 근무" = "5 days a week", "내가 낼게" = "I'll pay" are not dates).
|
|
112
|
+
Every rule is listed in the [Korean README](./README.md#이런-문장을-읽습니다) and [SPEC.md](./SPEC.md).
|
|
113
|
+
|
|
114
|
+
## API
|
|
115
|
+
|
|
116
|
+
| Function | Returns |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `parse(text, options?)` | The first `ParseResult`, or `null` |
|
|
119
|
+
| `parseAll(text, options?)` | Every `ParseResult` in the sentence |
|
|
120
|
+
| `parseDate(text, options?)` | Shorthand for `parse(...)?.date` |
|
|
121
|
+
| `parseEvent(text, options?)` | `{ title, date, end?, hasTime, recurrence? }` |
|
|
122
|
+
| `toICS(result, options?)` | An `.ics` string (`title`, `timezone`, `durationMinutes`) |
|
|
123
|
+
|
|
124
|
+
| `ParseResult` | |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `date` | The resolved time. Start of a range; the reference time for a deadline |
|
|
127
|
+
| `end` | End of a range or the deadline |
|
|
128
|
+
| `text` / `index` | The matched part of the sentence and its position |
|
|
129
|
+
| `hasTime` | Whether a time of day was given. If `false`, `date` is at `defaultHour` |
|
|
130
|
+
| `recurrence` | `{ freq, interval, byWeekday, …, until?, count?, rrule }` |
|
|
131
|
+
| `ambiguity` | `{ reason: 'meridiem' \| 'next-weekday' \| 'approximate-time', alternatives }` |
|
|
132
|
+
|
|
133
|
+
| Option | Default | |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `referenceDate` | `new Date()` | What "tomorrow" and "next week" are relative to |
|
|
136
|
+
| `timezone` | runtime | IANA name such as `'Asia/Seoul'` |
|
|
137
|
+
| `weekStart` | `'monday'` | First day of the week (`'sunday'` also works) |
|
|
138
|
+
| `defaultHour` | `0` | Hour used for dates without a time |
|
|
139
|
+
|
|
140
|
+
## Contributing
|
|
141
|
+
|
|
142
|
+
The most useful contribution is **real sentences**. If a sentence isn't recognised or is read wrong, please [open an issue](https://github.com/jwn7/ko-chrono/issues/new?template=sentence.yml) (Korean or English is fine).
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
[MIT](./LICENSE)
|
package/README.md
CHANGED
|
@@ -9,162 +9,112 @@
|
|
|
9
9
|
[](./package.json)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
|
|
12
|
-
<
|
|
13
|
-
<source media="(prefers-color-scheme: dark)" srcset="./docs/banner-dark.svg">
|
|
14
|
-
<img alt="'그럼 다음주 화요일 오후 3시에 볼까?'에서 '다음주 화요일 오후 3시'를 찾아 2026-11-03 15:00으로 바꾼다" src="./docs/banner-light.svg" width="760">
|
|
15
|
-
</picture>
|
|
12
|
+
<a href="https://jwn7.github.io/ko-chrono/"><img alt="playground 데모: 문장을 입력하면 날짜 부분이 형광펜으로 표시되고, 반복 일정·모호한 시각·마감·명절 연휴가 카드로 나온다" src="./docs/demo.gif" width="720"></a>
|
|
16
13
|
|
|
17
|
-
**[▶ 브라우저에서 바로 해 보기](https://jwn7.github.io/ko-chrono/)**
|
|
18
|
-
|
|
19
|
-
[설치](#설치) · [사용법](#사용법) · [지원 표현](#지원-표현) · [해석 규칙](#해석-규칙) · [예제](#예제) · [문장 제보](https://github.com/jwn7/ko-chrono/issues/new?template=sentence.yml)
|
|
14
|
+
**[▶ 브라우저에서 바로 해 보기](https://jwn7.github.io/ko-chrono/)** · [English](./README.en.md)
|
|
20
15
|
|
|
21
16
|
</div>
|
|
22
17
|
|
|
23
18
|
<br>
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
[es-hangul](https://github.com/toss/es-hangul)이 **글자**를 다룬다면, ko-chrono는 **문장 속 시간**을 다룹니다.
|
|
27
|
-
|
|
28
|
-
| 입력 | 결과 <sub>(기준: 2026-10-30 금 14:00)</sub> |
|
|
29
|
-
|---|---|
|
|
30
|
-
| 그럼 **다음주 수요일 저녁 7시**에 강남에서 볼까? | `2026-11-04 19:00` |
|
|
31
|
-
| **이번 주 안에** 보내주세요 | 마감 `2026-11-01 23:59` |
|
|
32
|
-
| **오늘부터 3일간** 휴가 | `10-30` \~ `11-01 23:59` |
|
|
33
|
-
| **담주 월욜 점심** ㄱ? | `2026-11-02 12:00` |
|
|
34
|
-
| **11월 첫째 주 월요일**에 시작 | `2026-11-02` |
|
|
35
|
-
| **3시**에 보자 | `2026-10-30 15:00` |
|
|
36
|
-
| 내가 낼게 | `null` (날짜 아님) |
|
|
37
|
-
|
|
38
|
-
<br>
|
|
39
|
-
|
|
40
|
-
## 특징
|
|
41
|
-
|
|
42
|
-
- **문장 중간에서 찾기** — 앞뒤 말은 무시하고 날짜 부분만, 여러 개면 전부
|
|
43
|
-
- **구간과 마감** — "\~까지", "A부터 B까지", "3\~5시", "오늘부터 3일간", "이번 주 내내"
|
|
44
|
-
- **반복 일정** — "매주 월수금 7시", "격주 화요일", "매달 마지막 주 금요일" → 캘린더용 RRULE
|
|
45
|
-
- **공휴일·명절** — "크리스마스 이브", "설 연휴", "추석 전날" (음력은 한국천문연구원 자료 기준)
|
|
46
|
-
- **구어체** — "담주 월욜", "낼 오후 2시", "내일모레", "3시쯤", 띄어쓰기 생략
|
|
47
|
-
- **틀리느니 비운다** — "3일 연속", "2시간짜리", "1월생"은 날짜로 잡지 않음
|
|
48
|
-
- **서버에서도 정확** — `timezone: 'Asia/Seoul'` 한 줄이면 UTC 서버에서도 한국 시간
|
|
49
|
-
- **가볍다** — 런타임 의존성 0, ESM·CommonJS 둘 다, 브라우저·Node 모두
|
|
50
|
-
|
|
51
|
-
<br>
|
|
52
|
-
|
|
53
|
-
## 설치
|
|
20
|
+
`ko-chrono`는 "다음주 화요일 오후 3시" 같은 한국어 표현을 찾아 `Date`로 바꿔 주는 JavaScript 라이브러리입니다.
|
|
21
|
+
[chrono-node](https://github.com/wanasit/chrono)에 없는 한국어를 채우려고 만들었습니다. [es-hangul](https://github.com/toss/es-hangul)이 **글자**를 다룬다면, ko-chrono는 **문장 속 시간**을 다룹니다.
|
|
54
22
|
|
|
55
23
|
```bash
|
|
56
24
|
npm install ko-chrono
|
|
57
25
|
```
|
|
58
26
|
|
|
59
|
-
|
|
27
|
+
런타임 의존성은 없고, ESM·CommonJS·브라우저·Node 어디서나 씁니다.
|
|
60
28
|
|
|
61
|
-
|
|
62
|
-
|
|
29
|
+
## 사용 예시
|
|
30
|
+
|
|
31
|
+
> 아래 결과는 모두 기준 시각 **2026-10-30 (금) 14:00** 기준입니다.
|
|
63
32
|
|
|
64
|
-
|
|
33
|
+
### 채팅에서 약속 시각 찾기
|
|
65
34
|
|
|
66
|
-
|
|
67
|
-
|
|
35
|
+
앞뒤 말은 건너뛰고 날짜 부분만 찾습니다. `text`와 `index`로 메시지 안에서 강조 표시를 할 수 있습니다.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { parse } from 'ko-chrono';
|
|
68
39
|
|
|
69
|
-
parse('
|
|
70
|
-
// { date: 지금, end: 2026-10-31 23:59, text: '이번달 말까지', hasTime: false }
|
|
40
|
+
const r = parse('그럼 다음주 수요일 저녁 7시에 강남에서 볼까?');
|
|
71
41
|
|
|
72
|
-
|
|
73
|
-
//
|
|
42
|
+
r.date; // 2026-11-04 19:00
|
|
43
|
+
r.text; // '다음주 수요일 저녁 7시'
|
|
44
|
+
r.index; // 3
|
|
74
45
|
```
|
|
75
46
|
|
|
76
|
-
### 할
|
|
47
|
+
### 할 일 앱 빠른 입력
|
|
77
48
|
|
|
78
|
-
|
|
49
|
+
`parseEvent`는 한 줄을 **제목**과 **시점**으로 나눕니다. 마감 표현은 `end`에 들어갑니다.
|
|
79
50
|
|
|
80
51
|
```ts
|
|
81
52
|
import { parseEvent } from 'ko-chrono';
|
|
82
53
|
|
|
83
54
|
parseEvent('다음주 금요일까지 보고서 제출');
|
|
84
|
-
// { title: '보고서 제출',
|
|
55
|
+
// { title: '보고서 제출', end: 2026-11-06 23:59, hasTime: false, ... }
|
|
85
56
|
|
|
86
57
|
parseEvent('내일 오후 3시에 강남역에서 친구 만나기');
|
|
87
58
|
// { title: '강남역에서 친구 만나기', date: 2026-10-31 15:00, hasTime: true }
|
|
88
|
-
|
|
89
|
-
parseEvent('우유 사기');
|
|
90
|
-
// { title: '우유 사기', date: null, hasTime: false }
|
|
91
59
|
```
|
|
92
60
|
|
|
93
|
-
###
|
|
61
|
+
### 반복 일정을 캘린더로
|
|
94
62
|
|
|
95
|
-
|
|
96
|
-
사용자에게 되묻거나, 이런 문장만 LLM에 넘길 때 쓰면 됩니다.
|
|
63
|
+
반복 표현은 [RRULE](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html)로 나와서 Google·Apple 캘린더에 그대로 넣을 수 있습니다.
|
|
97
64
|
|
|
98
65
|
```ts
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!r) askLLM(text); // 날짜를 못 찾음
|
|
104
|
-
else if (r.ambiguity?.alternatives.length) confirm(r); // "오후 3시 맞으세요?"
|
|
66
|
+
parse('매주 월수금 오전 7시 운동')?.recurrence?.rrule; // 'FREQ=WEEKLY;BYDAY=MO,WE,FR'
|
|
67
|
+
parse('매달 마지막 주 금요일 회식')?.recurrence?.rrule; // 'FREQ=MONTHLY;BYDAY=-1FR'
|
|
68
|
+
parse('다음달까지 매주 월요일 9시')?.recurrence?.rrule; // 'FREQ=WEEKLY;BYDAY=MO;UNTIL=20261130T235900'
|
|
69
|
+
parse('매주 수요일 2시, 총 8회')?.recurrence?.rrule; // 'FREQ=WEEKLY;BYDAY=WE;COUNT=8'
|
|
105
70
|
```
|
|
106
71
|
|
|
107
|
-
|
|
108
|
-
|---|---|---|
|
|
109
|
-
| `meridiem` | 오전/오후 없는 1\~12시 ("3시", "내일 7시") | 반대쪽 12시간 |
|
|
110
|
-
| `next-weekday` | "다음 토요일"이 이번 주 토요일일 수도, 다음 주 토요일일 수도 | 다른 쪽 날짜 |
|
|
111
|
-
| `approximate-time` | "저녁"(19시), "아침"(8시)처럼 관례로 정한 시각 | 없음 |
|
|
72
|
+
`toICS`로 바로 캘린더 파일(.ics)을 만들 수도 있습니다.
|
|
112
73
|
|
|
113
|
-
|
|
74
|
+
```ts
|
|
75
|
+
import { parseEvent, toICS } from 'ko-chrono';
|
|
76
|
+
|
|
77
|
+
toICS(parseEvent('다음달까지 매주 월요일 9시 팀 회의'), { timezone: 'Asia/Seoul' });
|
|
78
|
+
// BEGIN:VCALENDAR … SUMMARY:팀 회의 … DTSTART;TZID=Asia/Seoul:20261102T090000
|
|
79
|
+
// RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20261130T145900Z …
|
|
80
|
+
```
|
|
114
81
|
|
|
115
|
-
|
|
82
|
+
### 애매하면 알려 주기
|
|
83
|
+
|
|
84
|
+
"3시"처럼 해석이 갈리는 문장은 기본 해석과 함께 다른 후보를 줍니다. 사용자에게 되묻거나, 이런 문장만 LLM에 넘길 때 씁니다.
|
|
116
85
|
|
|
117
86
|
```ts
|
|
118
|
-
parse('
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
// recurrence: { freq: 'WEEKLY', interval: 1, byWeekday: [1, 3, 5], rrule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR' }
|
|
123
|
-
// }
|
|
124
|
-
|
|
125
|
-
parse('매달 마지막 주 금요일 회식')?.recurrence?.rrule; // 'FREQ=MONTHLY;BYDAY=-1FR'
|
|
126
|
-
parse('격주 화요일')?.recurrence?.rrule; // 'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU'
|
|
87
|
+
const r = parse('3시에 보자');
|
|
88
|
+
|
|
89
|
+
r.date; // 오늘 15:00
|
|
90
|
+
r.ambiguity; // { reason: 'meridiem', alternatives: [내일 03:00] }
|
|
127
91
|
```
|
|
128
92
|
|
|
129
|
-
###
|
|
93
|
+
### 서버에서 쓸 때
|
|
130
94
|
|
|
131
|
-
|
|
132
|
-
|---|---|
|
|
133
|
-
| `parse(text, options?)` | 첫 번째 표현의 `ParseResult`, 없으면 `null` |
|
|
134
|
-
| `parseAll(text, options?)` | 모든 표현의 `ParseResult[]` |
|
|
135
|
-
| `parseDate(text, options?)` | `parse(...)?.date` 단축형 |
|
|
136
|
-
| `parseEvent(text, options?)` | `{ title, date, end?, hasTime, recurrence? }` — 날짜를 뺀 나머지가 제목 |
|
|
95
|
+
서버는 보통 UTC로 돌아서 "오후 3시"가 한국 시간과 어긋납니다. `timezone`을 넘기세요.
|
|
137
96
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
| `end` | 구간의 끝 또는 마감. 없으면 `undefined` |
|
|
142
|
-
| `text` | 문장에서 찾은 부분 (`'다음주 수요일 저녁 7시'`) |
|
|
143
|
-
| `index` | 문장 안에서의 위치 |
|
|
144
|
-
| `hasTime` | 시각까지 말했는지. `false`면 `date`는 그날 `defaultHour`시 |
|
|
145
|
-
| `recurrence` | 반복 일정이면 규칙 (`freq`, `interval`, `byWeekday`, …, `rrule`) |
|
|
146
|
-
| `ambiguity` | 해석이 갈릴 수 있으면 `{ reason, alternatives }` |
|
|
97
|
+
```ts
|
|
98
|
+
parse('내일 오후 3시', { timezone: 'Asia/Seoul' }); // 서버 시간대와 상관없이 서울 오후 3시
|
|
99
|
+
```
|
|
147
100
|
|
|
148
|
-
|
|
101
|
+
## 이런 문장을 읽습니다
|
|
149
102
|
|
|
150
|
-
|
|
|
151
|
-
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
103
|
+
| 입력 | 결과 |
|
|
104
|
+
|---|---|
|
|
105
|
+
| **이번 주 안에** 보내주세요 | 마감 `11-01 23:59` |
|
|
106
|
+
| **오늘부터 3일간** 휴가 | `10-30` \~ `11-01 23:59` |
|
|
107
|
+
| **담주 월욜 점심** ㄱ? | `11-02 12:00` |
|
|
108
|
+
| **11월 첫째 주 월요일**에 시작 | `11-02` |
|
|
109
|
+
| **차주 중으로** 회신 부탁드립니다 | 마감 `11-08 23:59` |
|
|
110
|
+
| **설 연휴**에 내려가 | `2027-02-06` \~ `02-08` |
|
|
111
|
+
| 내가 낼게 | `null` (날짜가 아님) |
|
|
156
112
|
|
|
157
|
-
>
|
|
158
|
-
|
|
159
|
-
> ```ts
|
|
160
|
-
> parse('내일 오후 3시', { timezone: 'Asia/Seoul' }); // 서버 시간대와 상관없이 서울 오후 3시
|
|
161
|
-
> ```
|
|
162
|
-
> 브라우저에서는 사용자의 시간대가 기본값이라 보통 필요 없습니다. 내장 `Intl`로 계산해서 의존성이 늘지 않습니다.
|
|
113
|
+
<details>
|
|
114
|
+
<summary><b>지원하는 표현 전체</b></summary>
|
|
163
115
|
|
|
164
116
|
<br>
|
|
165
117
|
|
|
166
|
-
## 지원 표현
|
|
167
|
-
|
|
168
118
|
| 분류 | 예 |
|
|
169
119
|
|---|---|
|
|
170
120
|
| 상대 날짜 | 오늘 · 내일 · 모레 · 글피 · 어제 · 그저께 · 내일모레 · 낼 |
|
|
@@ -180,27 +130,26 @@ parse('격주 화요일')?.recurrence?.rrule; // 'FREQ=WEEKLY;IN
|
|
|
180
130
|
| 음력 | 음력 3월 3일 · 음력 윤5월 1일 · 2028년 음력 9월 9일 |
|
|
181
131
|
| 업무용 한자어 | 금일 · 명일 · 익일 · 작일 · 금주 · 차주 · 전주 수요일 · 익월 초 · 금월 말까지 · 익년 |
|
|
182
132
|
| 반복 | 매일 · 매주 월수금 · 매주 월\~금 · 격주 · 평일마다 · 매달 15일 · 매월 말일 · 매달 마지막 주 금요일 · 매년 3월 1일 · 2시간마다 |
|
|
133
|
+
| 반복의 시작·끝 | 다음달까지 매주 월요일 · 다음주부터 매일 · 10주 동안 매주 화요일 · 총 8회 · 10번 반복 · 매주 화목 오후 3\~5시 |
|
|
183
134
|
| 문장 | 띄어쓰기 생략 · 조사(은·는·도) · 어림말(쯤·경·께) · 여러 개 추출 |
|
|
184
135
|
|
|
185
|
-
입력별
|
|
186
|
-
|
|
187
|
-
<br>
|
|
136
|
+
입력별 결과는 [SPEC.md](./SPEC.md)에 모두 있습니다.
|
|
188
137
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
애매한 표현은 아래처럼 정해 두었습니다. 같은 문장은 언제나 같은 답을 냅니다.
|
|
138
|
+
</details>
|
|
192
139
|
|
|
193
140
|
<details>
|
|
194
|
-
<summary><b
|
|
141
|
+
<summary><b>애매한 표현은 이렇게 정했습니다</b></summary>
|
|
195
142
|
|
|
196
143
|
<br>
|
|
197
144
|
|
|
145
|
+
같은 문장은 언제나 같은 답을 냅니다.
|
|
146
|
+
|
|
198
147
|
**요일과 주**
|
|
199
148
|
- "다음주 월요일" = 기준일이 속한 주의 다음 주 월요일 (금요일에 말하면 3일 뒤)
|
|
200
149
|
- "월요일" 단독 = 오늘 포함 가장 가까운 미래의 월요일
|
|
201
150
|
- "지난 금요일" = 오늘 제외 가장 가까운 과거, "다음 금요일" = 오늘 제외 가장 가까운 미래
|
|
202
151
|
- "이번 주말" = 그 주의 토요일. "주말" 단독 = 다가오는 토요일 (토·일이면 오늘)
|
|
203
|
-
- "11월 첫째 주 월요일" = 11월의 첫 번째
|
|
152
|
+
- "11월 첫째 주 월요일" = 11월의 첫 번째 월요일
|
|
204
153
|
|
|
205
154
|
**시각**
|
|
206
155
|
- 오전/오후가 없는 1\~6시는 오후 ("3시에 보자" → 15:00). 7\~11시는 그대로
|
|
@@ -210,9 +159,10 @@ parse('격주 화요일')?.recurrence?.rrule; // 'FREQ=WEEKLY;IN
|
|
|
210
159
|
|
|
211
160
|
**날짜**
|
|
212
161
|
- 월일만 있고 이미 지났으면 내년. "올해 3월 1일"처럼 연도를 말하면 그대로
|
|
162
|
+
- "5일까지"처럼 일만 있으면 이번 달, 오늘이 그날을 넘었으면 다음 달. 그 달에 없는 날(11월의 "31일")은 `null`
|
|
213
163
|
- "3월", "2월 말"도 그 달이 다 지났으면 내년
|
|
214
164
|
- 두 자리 연도: 00\~69 → 20xx, 70\~99 → 19xx
|
|
215
|
-
- "내년 이맘때", "1년 뒤"는 오늘의
|
|
165
|
+
- "한 달 뒤", "내년 이맘때", "1년 뒤"는 오늘의 날짜 유지 (1/31 + 한 달 → 2/28, 2/29 + 1년 → 2/28)
|
|
216
166
|
|
|
217
167
|
**구간·마감**
|
|
218
168
|
- "\~까지", "\~안에" → `date`는 기준 시각, `end`는 마감 (시각이 없으면 23:59)
|
|
@@ -220,66 +170,77 @@ parse('격주 화요일')?.recurrence?.rrule; // 'FREQ=WEEKLY;IN
|
|
|
220
170
|
- "오늘부터 3일간" → `end`는 3일째 되는 날 23:59
|
|
221
171
|
|
|
222
172
|
**명절**
|
|
223
|
-
-
|
|
224
|
-
- 설날·추석·부처님 오신 날은 [한국천문연구원](https://astro.kasi.re.kr/life/post/calendarData) 자료가 있는 연도(현재 2025\~2028)만. 그 뒤는 추측하지 않고 `null`
|
|
173
|
+
- 설날·추석·부처님 오신 날과 음력 날짜는 [한국천문연구원](https://astro.kasi.re.kr/life/post/calendarData) 자료가 있는 연도(현재 2025\~2028)만. 그 밖은 추측하지 않고 `null`
|
|
225
174
|
- "설 연휴", "추석 연휴" = 전날부터 다음날까지 3일
|
|
226
175
|
|
|
227
176
|
**잡지 않는 것**
|
|
228
|
-
- 없는 날짜 (2월 30
|
|
229
|
-
- 기간만 있는 표현 ("3일간", "3박 4일"), 날짜가 아닌 말 ("3일 연속", "
|
|
230
|
-
- 한자어가 다른 뜻일 때: "금주 결심"(禁酒), "전주 여행"(도시), "차주에게 연락"(車主)
|
|
177
|
+
- 없는 날짜 (2월 30일), 날짜와 요일이 안 맞는 경우 ("11월 5일 수요일")
|
|
178
|
+
- 기간만 있는 표현 ("3일간", "3박 4일"), 날짜가 아닌 말 ("3일 연속", "주 5일 근무", "30일 챌린지", "24시 편의점", "내가 낼게")
|
|
179
|
+
- 한자어가 다른 뜻일 때: "금주 결심"(禁酒), "전주 여행"(도시), "차주에게 연락"(車主)
|
|
231
180
|
- "내일 30분 후"처럼 날짜와 상대 시간이 섞이면 두 개의 결과로 나눔
|
|
232
181
|
|
|
233
182
|
</details>
|
|
234
183
|
|
|
235
|
-
|
|
184
|
+
## API
|
|
236
185
|
|
|
237
|
-
|
|
186
|
+
| 함수 | 반환 |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `parse(text, options?)` | 첫 번째 표현의 `ParseResult`, 없으면 `null` |
|
|
189
|
+
| `parseAll(text, options?)` | 모든 표현의 `ParseResult[]` |
|
|
190
|
+
| `parseDate(text, options?)` | `parse(...)?.date` 단축형 |
|
|
191
|
+
| `parseEvent(text, options?)` | `{ title, date, end?, hasTime, recurrence? }` |
|
|
192
|
+
| `toICS(result, options?)` | `parse`·`parseEvent` 결과 → `.ics` 문자열 (`title`, `timezone`, `durationMinutes`) |
|
|
238
193
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
| ✅ | [**할 일 빠른 입력**](./examples/quick-add.mjs)<br>`parseEvent`로 "다음주 금요일까지 보고서 제출" → 제목 "보고서 제출" + 마감 11/6 23:59 | `node examples/quick-add.mjs "..."` |
|
|
242
|
-
| 💬 | [**Slack 봇**](./examples/slack-bot)<br>채널 메시지에서 날짜를 찾아 스레드로 알려줌 (UTC 서버 + `timezone`) | `node examples/slack-bot/try.mjs` |
|
|
243
|
-
| 🧪 | [**Playground**](https://jwn7.github.io/ko-chrono/)<br>브라우저에서 직접 입력해 보기 (문장을 넣으면 공유 링크가 생김) | [jwn7.github.io/ko-chrono](https://jwn7.github.io/ko-chrono/) · 로컬은 `npm run playground` |
|
|
194
|
+
<details>
|
|
195
|
+
<summary><b><code>ParseResult</code>와 옵션</b></summary>
|
|
244
196
|
|
|
245
197
|
<br>
|
|
246
198
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
199
|
+
| `ParseResult` | |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `date` | 찾은 시점. 구간이면 시작, 마감이면 기준 시각 |
|
|
202
|
+
| `end` | 구간의 끝 또는 마감. 없으면 `undefined` |
|
|
203
|
+
| `text` | 문장에서 찾은 부분 |
|
|
204
|
+
| `index` | 문장 안에서의 위치 |
|
|
205
|
+
| `hasTime` | 시각까지 말했는지. `false`면 `date`는 그날 `defaultHour`시 |
|
|
206
|
+
| `recurrence` | 반복 일정이면 `{ freq, interval, byWeekday, …, until?, count?, rrule }` |
|
|
207
|
+
| `ambiguity` | 해석이 갈리면 `{ reason, alternatives }` |
|
|
250
208
|
|
|
251
|
-
|
|
209
|
+
| `ambiguity.reason` | 언제 |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `meridiem` | 오전/오후 없는 1\~12시 ("3시", "내일 7시") |
|
|
212
|
+
| `next-weekday` | "다음 토요일"이 이번 주일 수도, 다음 주일 수도 |
|
|
213
|
+
| `approximate-time` | "저녁"(19시)처럼 관례로 정한 시각 |
|
|
252
214
|
|
|
253
|
-
| |
|
|
215
|
+
| 옵션 | 기본값 | 설명 |
|
|
254
216
|
|---|---|---|
|
|
255
|
-
|
|
|
256
|
-
|
|
|
257
|
-
|
|
|
258
|
-
|
|
|
259
|
-
| 시제·문맥 ("깨서 못 잤어") | 모름 | 앎 |
|
|
260
|
-
| 처음 보는 표현 | 못 함 (`null`) | 대체로 함 |
|
|
217
|
+
| `referenceDate` | `new Date()` | "내일", "다음주"의 기준 시각 |
|
|
218
|
+
| `timezone` | 실행 환경 | 어느 시간대의 시각으로 볼지 (`'Asia/Seoul'` 같은 IANA 이름) |
|
|
219
|
+
| `weekStart` | `'monday'` | 한 주의 시작 (`'sunday'` 가능) |
|
|
220
|
+
| `defaultHour` | `0` | 시각 없는 날짜에 넣을 시 |
|
|
261
221
|
|
|
262
|
-
|
|
222
|
+
</details>
|
|
263
223
|
|
|
264
|
-
|
|
224
|
+
## 예제
|
|
225
|
+
|
|
226
|
+
- [할 일 빠른 입력](./examples/quick-add.mjs) — `node examples/quick-add.mjs "다음주 금요일까지 보고서 제출"`
|
|
227
|
+
- [Slack 봇](./examples/slack-bot) — 채널 메시지에서 날짜를 찾아 스레드로 알려 줌
|
|
228
|
+
- [Playground](https://jwn7.github.io/ko-chrono/) — 브라우저에서 직접 입력. 로컬은 `npm run playground`
|
|
229
|
+
|
|
230
|
+
## LLM에 물어보면 되지 않나요?
|
|
265
231
|
|
|
266
|
-
|
|
232
|
+
됩니다. 다만 규칙으로 풀리는 날짜라면 이쪽이 빠르고, 공짜이고, 매번 같은 답을 냅니다.
|
|
233
|
+
메신저 문장 112개로 비교해 보니 ko-chrono는 문장당 **0.017ms**, Gemini Flash-Lite(무료 티어)는 **987ms**였습니다 ([자세히](./bench/RESULTS.md)).
|
|
267
234
|
|
|
268
|
-
-
|
|
269
|
-
- [x] 제목 분리 (`parseEvent`)
|
|
270
|
-
- [x] 명절·공휴일 (설 연휴, 추석 전날, 크리스마스) — 음력 명절은 한국천문연구원 자료가 있는 2025\~2028년
|
|
271
|
-
- [x] 일반 음력 날짜 (음력 3월 3일) — 한국천문연구원 자료가 있는 2025\~2028년
|
|
272
|
-
- [x] 모호성 정보 (`ambiguity`)
|
|
273
|
-
- [ ] chrono-node `ko` 로케일 기여 — 첫 PR 분량 준비됨 ([contrib/chrono-ko](./contrib/chrono-ko))
|
|
235
|
+
평소엔 ko-chrono로 처리하고, `null`이 나오거나 `ambiguity`가 있는 문장만 LLM에 보내는 조합을 권합니다.
|
|
274
236
|
|
|
275
237
|
## 기여
|
|
276
238
|
|
|
277
|
-
가장 큰 도움은 **실제로 쓰이는 문장**입니다. 인식이 안 되거나 다르게
|
|
239
|
+
가장 큰 도움은 **실제로 쓰이는 문장**입니다. 인식이 안 되거나 다르게 읽히는 문장을 [이슈로 제보](https://github.com/jwn7/ko-chrono/issues/new?template=sentence.yml)해 주세요.
|
|
278
240
|
|
|
279
241
|
```bash
|
|
280
242
|
npm install
|
|
281
243
|
npm test
|
|
282
|
-
npm run build
|
|
283
244
|
```
|
|
284
245
|
|
|
285
246
|
## 라이선스
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ParseResult } from './types.js';
|
|
2
|
+
import type { ParsedEvent } from './event.js';
|
|
3
|
+
export interface ICSOptions {
|
|
4
|
+
/** 일정 제목. 기본값: parseEvent 결과면 그 title, parse 결과면 문장에서 찾은 부분 */
|
|
5
|
+
title?: string;
|
|
6
|
+
/**
|
|
7
|
+
* 시각을 이 시간대의 벽시계로 쓴다 (`DTSTART;TZID=Asia/Seoul:…`). 서머타임이 있는 곳의 반복 일정은 이걸 넘겨야
|
|
8
|
+
* 시각이 유지된다. 없으면 UTC로 쓴다. 종일 일정의 날짜도 이 시간대 기준.
|
|
9
|
+
*/
|
|
10
|
+
timezone?: string;
|
|
11
|
+
/** 시각은 있는데 끝이 없을 때 일정 길이(분). 기본값: 60 */
|
|
12
|
+
durationMinutes?: number;
|
|
13
|
+
/** VEVENT의 UID. 기본값: 무작위 */
|
|
14
|
+
uid?: string;
|
|
15
|
+
/** DTSTAMP. 기본값: new Date() */
|
|
16
|
+
now?: Date;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* parse / parseEvent 결과를 iCalendar(.ics) 문자열로 바꾼다. 날짜가 없으면 null.
|
|
20
|
+
* Google·Apple·Outlook 캘린더에서 그대로 가져올 수 있다.
|
|
21
|
+
*
|
|
22
|
+
* - 시각이 있으면 DTSTART/DTEND (끝이 없으면 durationMinutes 뒤)
|
|
23
|
+
* - 시각이 없으면 종일 일정 (`VALUE=DATE`, DTEND는 마지막 날의 다음날)
|
|
24
|
+
* - 반복이면 RRULE. UNTIL은 DTSTART 형식에 맞춰 UTC 또는 날짜로 다시 쓴다
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const ics = toICS(parseEvent('다음주 월요일 9시 팀 회의'), { timezone: 'Asia/Seoul' });
|
|
28
|
+
* // BEGIN:VCALENDAR … SUMMARY:팀 회의 … DTSTART;TZID=Asia/Seoul:20261102T090000 …
|
|
29
|
+
*/
|
|
30
|
+
export declare function toICS(input: ParseResult | ParsedEvent, options?: ICSOptions): string | null;
|
package/dist/cjs/ics.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toICS = toICS;
|
|
4
|
+
const recurrence_js_1 = require("./parsers/recurrence.js");
|
|
5
|
+
const timezone_js_1 = require("./util/timezone.js");
|
|
6
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
7
|
+
const ymd = (d) => `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
|
8
|
+
const hms = (d) => `${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
9
|
+
const utc = (d) => `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}T` +
|
|
10
|
+
`${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`;
|
|
11
|
+
/** TEXT 값 이스케이프 (RFC 5545 3.3.11) */
|
|
12
|
+
const escapeText = (s) => s.replace(/[\\;,]/g, (c) => `\\${c}`).replace(/\r?\n/g, '\\n');
|
|
13
|
+
/** 75바이트가 넘는 줄은 CRLF + 공백으로 접는다 (RFC 5545 3.1). UTF-8 글자 중간에서는 끊지 않는다 */
|
|
14
|
+
function fold(line) {
|
|
15
|
+
const out = [];
|
|
16
|
+
let cur = '';
|
|
17
|
+
let bytes = 0;
|
|
18
|
+
for (const ch of line) {
|
|
19
|
+
const cp = ch.codePointAt(0);
|
|
20
|
+
const len = cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
|
|
21
|
+
// 이어지는 줄은 앞의 공백 1바이트를 뺀 74바이트까지
|
|
22
|
+
if (bytes + len > (out.length ? 74 : 75)) {
|
|
23
|
+
out.push(cur);
|
|
24
|
+
cur = '';
|
|
25
|
+
bytes = 0;
|
|
26
|
+
}
|
|
27
|
+
cur += ch;
|
|
28
|
+
bytes += len;
|
|
29
|
+
}
|
|
30
|
+
out.push(cur);
|
|
31
|
+
return out.join('\r\n ');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* parse / parseEvent 결과를 iCalendar(.ics) 문자열로 바꾼다. 날짜가 없으면 null.
|
|
35
|
+
* Google·Apple·Outlook 캘린더에서 그대로 가져올 수 있다.
|
|
36
|
+
*
|
|
37
|
+
* - 시각이 있으면 DTSTART/DTEND (끝이 없으면 durationMinutes 뒤)
|
|
38
|
+
* - 시각이 없으면 종일 일정 (`VALUE=DATE`, DTEND는 마지막 날의 다음날)
|
|
39
|
+
* - 반복이면 RRULE. UNTIL은 DTSTART 형식에 맞춰 UTC 또는 날짜로 다시 쓴다
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const ics = toICS(parseEvent('다음주 월요일 9시 팀 회의'), { timezone: 'Asia/Seoul' });
|
|
43
|
+
* // BEGIN:VCALENDAR … SUMMARY:팀 회의 … DTSTART;TZID=Asia/Seoul:20261102T090000 …
|
|
44
|
+
*/
|
|
45
|
+
function toICS(input, options = {}) {
|
|
46
|
+
if (!input.date)
|
|
47
|
+
return null;
|
|
48
|
+
const tz = options.timezone;
|
|
49
|
+
const wall = (d) => (tz ? (0, timezone_js_1.toWallClock)(d, tz) : d);
|
|
50
|
+
const title = options.title ?? ('title' in input ? input.title : input.text);
|
|
51
|
+
const lines = [
|
|
52
|
+
'BEGIN:VCALENDAR',
|
|
53
|
+
'VERSION:2.0',
|
|
54
|
+
'PRODID:-//ko-chrono//KO',
|
|
55
|
+
'CALSCALE:GREGORIAN',
|
|
56
|
+
'BEGIN:VEVENT',
|
|
57
|
+
`UID:${options.uid ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}@ko-chrono`}`,
|
|
58
|
+
`DTSTAMP:${utc(options.now ?? new Date())}`,
|
|
59
|
+
`SUMMARY:${escapeText(title)}`,
|
|
60
|
+
];
|
|
61
|
+
if (input.hasTime) {
|
|
62
|
+
const end = input.end ?? new Date(input.date.getTime() + (options.durationMinutes ?? 60) * 60_000);
|
|
63
|
+
const at = (name, d) => (tz ? `${name};TZID=${tz}:${ymd(wall(d))}T${hms(wall(d))}` : `${name}:${utc(d)}`);
|
|
64
|
+
lines.push(at('DTSTART', input.date), at('DTEND', end));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const first = wall(input.date);
|
|
68
|
+
const last = input.end ? wall(input.end) : first;
|
|
69
|
+
const after = new Date(last.getFullYear(), last.getMonth(), last.getDate() + 1);
|
|
70
|
+
lines.push(`DTSTART;VALUE=DATE:${ymd(first)}`, `DTEND;VALUE=DATE:${ymd(after)}`);
|
|
71
|
+
}
|
|
72
|
+
const r = input.recurrence;
|
|
73
|
+
if (r) {
|
|
74
|
+
// recurrence.rrule의 UNTIL은 벽시계(floating) 형식이라, 시각이 있는 일정이면 UTC로 바꿔 쓴다
|
|
75
|
+
const rrule = r.until && input.hasTime ? (0, recurrence_js_1.toRRule)({ ...r, until: undefined }) + `;UNTIL=${utc(r.until)}` : r.rrule;
|
|
76
|
+
lines.push(`RRULE:${rrule}`);
|
|
77
|
+
}
|
|
78
|
+
lines.push('END:VEVENT', 'END:VCALENDAR', '');
|
|
79
|
+
return lines.map(fold).join('\r\n');
|
|
80
|
+
}
|