ko-chrono 0.0.0-stage → 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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +256 -2
  3. package/dist/cjs/event.d.ts +18 -0
  4. package/dist/cjs/event.js +31 -0
  5. package/dist/cjs/index.d.ts +55 -0
  6. package/dist/cjs/index.js +189 -0
  7. package/dist/cjs/package.json +1 -0
  8. package/dist/cjs/parsers/absoluteDate.d.ts +3 -0
  9. package/dist/cjs/parsers/absoluteDate.js +95 -0
  10. package/dist/cjs/parsers/duration.d.ts +3 -0
  11. package/dist/cjs/parsers/duration.js +45 -0
  12. package/dist/cjs/parsers/holiday.d.ts +3 -0
  13. package/dist/cjs/parsers/holiday.js +64 -0
  14. package/dist/cjs/parsers/offset.d.ts +3 -0
  15. package/dist/cjs/parsers/offset.js +64 -0
  16. package/dist/cjs/parsers/recurrence.d.ts +5 -0
  17. package/dist/cjs/parsers/recurrence.js +117 -0
  18. package/dist/cjs/parsers/relativeDay.d.ts +3 -0
  19. package/dist/cjs/parsers/relativeDay.js +26 -0
  20. package/dist/cjs/parsers/relativeMonth.d.ts +3 -0
  21. package/dist/cjs/parsers/relativeMonth.js +30 -0
  22. package/dist/cjs/parsers/relativeYear.d.ts +3 -0
  23. package/dist/cjs/parsers/relativeYear.js +30 -0
  24. package/dist/cjs/parsers/time.d.ts +3 -0
  25. package/dist/cjs/parsers/time.js +149 -0
  26. package/dist/cjs/parsers/weekday.d.ts +3 -0
  27. package/dist/cjs/parsers/weekday.js +109 -0
  28. package/dist/cjs/resolve.d.ts +21 -0
  29. package/dist/cjs/resolve.js +414 -0
  30. package/dist/cjs/types.d.ts +105 -0
  31. package/dist/cjs/types.js +2 -0
  32. package/dist/cjs/util/lunarHolidays.d.ts +3 -0
  33. package/dist/cjs/util/lunarHolidays.js +16 -0
  34. package/dist/cjs/util/number.d.ts +5 -0
  35. package/dist/cjs/util/number.js +42 -0
  36. package/dist/cjs/util/timezone.d.ts +4 -0
  37. package/dist/cjs/util/timezone.js +55 -0
  38. package/dist/event.d.ts +18 -0
  39. package/dist/event.js +28 -0
  40. package/dist/index.d.ts +55 -0
  41. package/dist/index.js +183 -0
  42. package/dist/parsers/absoluteDate.d.ts +3 -0
  43. package/dist/parsers/absoluteDate.js +91 -0
  44. package/dist/parsers/duration.d.ts +3 -0
  45. package/dist/parsers/duration.js +41 -0
  46. package/dist/parsers/holiday.d.ts +3 -0
  47. package/dist/parsers/holiday.js +60 -0
  48. package/dist/parsers/offset.d.ts +3 -0
  49. package/dist/parsers/offset.js +60 -0
  50. package/dist/parsers/recurrence.d.ts +5 -0
  51. package/dist/parsers/recurrence.js +112 -0
  52. package/dist/parsers/relativeDay.d.ts +3 -0
  53. package/dist/parsers/relativeDay.js +22 -0
  54. package/dist/parsers/relativeMonth.d.ts +3 -0
  55. package/dist/parsers/relativeMonth.js +26 -0
  56. package/dist/parsers/relativeYear.d.ts +3 -0
  57. package/dist/parsers/relativeYear.js +26 -0
  58. package/dist/parsers/time.d.ts +3 -0
  59. package/dist/parsers/time.js +145 -0
  60. package/dist/parsers/weekday.d.ts +3 -0
  61. package/dist/parsers/weekday.js +105 -0
  62. package/dist/resolve.d.ts +21 -0
  63. package/dist/resolve.js +405 -0
  64. package/dist/types.d.ts +105 -0
  65. package/dist/types.js +1 -0
  66. package/dist/util/lunarHolidays.d.ts +3 -0
  67. package/dist/util/lunarHolidays.js +13 -0
  68. package/dist/util/number.d.ts +5 -0
  69. package/dist/util/number.js +39 -0
  70. package/dist/util/timezone.d.ts +4 -0
  71. package/dist/util/timezone.js +51 -0
  72. package/package.json +40 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 최지원
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,257 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # ko-chrono
4
+
5
+ **한국어 문장 속 날짜·시간을 `Date`로**
6
+
7
+ [![npm](https://img.shields.io/npm/v/ko-chrono?color=0969da)](https://www.npmjs.com/package/ko-chrono)
8
+ [![CI](https://github.com/jwn7/ko-chrono/actions/workflows/ci.yml/badge.svg)](https://github.com/jwn7/ko-chrono/actions/workflows/ci.yml)
9
+ [![dependencies](https://img.shields.io/badge/dependencies-0-1a7f37)](./package.json)
10
+ [![license](https://img.shields.io/npm/l/ko-chrono)](./LICENSE)
11
+
12
+ <picture>
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>
16
+
17
+ [설치](#설치) · [사용법](#사용법) · [지원 표현](#지원-표현) · [해석 규칙](#해석-규칙) · [예제](#예제) · [문장 제보](https://github.com/jwn7/ko-chrono/issues/new?template=sentence.yml)
18
+
19
+ </div>
20
+
21
+ <br>
22
+
23
+ [chrono-node](https://github.com/wanasit/chrono)는 영어·일본어·중국어 등은 지원하지만 한국어는 없습니다.
24
+ [es-hangul](https://github.com/toss/es-hangul)이 **글자**를 다룬다면, ko-chrono는 **문장 속 시간**을 다룹니다.
25
+
26
+ | 입력 | 결과 <sub>(기준: 2026-10-30 금 14:00)</sub> |
27
+ |---|---|
28
+ | 그럼 **다음주 수요일 저녁 7시**에 강남에서 볼까? | `2026-11-04 19:00` |
29
+ | **이번 주 안에** 보내주세요 | 마감 `2026-11-01 23:59` |
30
+ | **오늘부터 3일간** 휴가 | `10-30` \~ `11-01 23:59` |
31
+ | **담주 월욜 점심** ㄱ? | `2026-11-02 12:00` |
32
+ | **11월 첫째 주 월요일**에 시작 | `2026-11-02` |
33
+ | **3시**에 보자 | `2026-10-30 15:00` |
34
+ | 내가 낼게 | `null` (날짜 아님) |
35
+
36
+ <br>
37
+
38
+ ## 특징
39
+
40
+ - **문장 중간에서 찾기** — 앞뒤 말은 무시하고 날짜 부분만, 여러 개면 전부
41
+ - **구간과 마감** — "\~까지", "A부터 B까지", "3\~5시", "오늘부터 3일간", "이번 주 내내"
42
+ - **반복 일정** — "매주 월수금 7시", "격주 화요일", "매달 마지막 주 금요일" → 캘린더용 RRULE
43
+ - **공휴일·명절** — "크리스마스 이브", "설 연휴", "추석 전날" (음력은 한국천문연구원 자료 기준)
44
+ - **구어체** — "담주 월욜", "낼 오후 2시", "내일모레", "3시쯤", 띄어쓰기 생략
45
+ - **틀리느니 비운다** — "3일 연속", "2시간짜리", "1월생"은 날짜로 잡지 않음
46
+ - **서버에서도 정확** — `timezone: 'Asia/Seoul'` 한 줄이면 UTC 서버에서도 한국 시간
47
+ - **가볍다** — 런타임 의존성 0, ESM·CommonJS 둘 다, 브라우저·Node 모두
48
+
49
+ <br>
50
+
51
+ ## 설치
52
+
53
+ ```bash
54
+ npm install ko-chrono
55
+ ```
56
+
57
+ ## 사용법
58
+
59
+ ```ts
60
+ import { parse, parseAll, parseDate } from 'ko-chrono';
61
+
62
+ parseDate('다음주 화요일 오후 3시'); // Date: 2026-11-03 15:00
63
+
64
+ parse('그럼 다음주 수요일 저녁 7시에 강남에서 볼까?');
65
+ // { date: 2026-11-04 19:00, text: '다음주 수요일 저녁 7시', index: 3, hasTime: true }
66
+
67
+ parse('이번달 말까지 제출');
68
+ // { date: 지금, end: 2026-10-31 23:59, text: '이번달 말까지', hasTime: false }
69
+
70
+ parseAll('내일 10시 아니면 모레 2시');
71
+ // [ { date: 10-31 10:00, ... }, { date: 11-01 14:00, ... } ]
72
+ ```
73
+
74
+ ### 할 일·일정 한 줄: `parseEvent`
75
+
76
+ 할 일 앱이나 캘린더의 빠른 입력처럼, 한 줄을 **제목과 시점**으로 나눕니다.
77
+
78
+ ```ts
79
+ import { parseEvent } from 'ko-chrono';
80
+
81
+ parseEvent('다음주 금요일까지 보고서 제출');
82
+ // { title: '보고서 제출', date: 지금, end: 2026-11-06 23:59, hasTime: false }
83
+
84
+ parseEvent('내일 오후 3시에 강남역에서 친구 만나기');
85
+ // { title: '강남역에서 친구 만나기', date: 2026-10-31 15:00, hasTime: true }
86
+
87
+ parseEvent('우유 사기');
88
+ // { title: '우유 사기', date: null, hasTime: false }
89
+ ```
90
+
91
+ ### 반복 일정
92
+
93
+ `recurrence.rrule`은 [RFC 5545 RRULE](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html)이라 Google·Apple 캘린더, iCal에 그대로 넣을 수 있습니다. `date`는 기준 시각 이후 첫 발생입니다.
94
+
95
+ ```ts
96
+ parse('매주 월수금 오전 7시 운동');
97
+ // {
98
+ // date: 2026-11-02 07:00,
99
+ // text: '매주 월수금 오전 7시',
100
+ // recurrence: { freq: 'WEEKLY', interval: 1, byWeekday: [1, 3, 5], rrule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR' }
101
+ // }
102
+
103
+ parse('매달 마지막 주 금요일 회식')?.recurrence?.rrule; // 'FREQ=MONTHLY;BYDAY=-1FR'
104
+ parse('격주 화요일')?.recurrence?.rrule; // 'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU'
105
+ ```
106
+
107
+ ### API
108
+
109
+ | 함수 | 반환 |
110
+ |---|---|
111
+ | `parse(text, options?)` | 첫 번째 표현의 `ParseResult`, 없으면 `null` |
112
+ | `parseAll(text, options?)` | 모든 표현의 `ParseResult[]` |
113
+ | `parseDate(text, options?)` | `parse(...)?.date` 단축형 |
114
+ | `parseEvent(text, options?)` | `{ title, date, end?, hasTime, recurrence? }` — 날짜를 뺀 나머지가 제목 |
115
+
116
+ | `ParseResult` | |
117
+ |---|---|
118
+ | `date` | 찾은 시점. 구간이면 시작, 마감이면 기준 시각 |
119
+ | `end` | 구간의 끝 또는 마감. 없으면 `undefined` |
120
+ | `text` | 문장에서 찾은 부분 (`'다음주 수요일 저녁 7시'`) |
121
+ | `index` | 문장 안에서의 위치 |
122
+ | `hasTime` | 시각까지 말했는지. `false`면 `date`는 그날 `defaultHour`시 |
123
+ | `recurrence` | 반복 일정이면 규칙 (`freq`, `interval`, `byWeekday`, …, `rrule`) |
124
+
125
+ ### 옵션
126
+
127
+ | 옵션 | 기본값 | 설명 |
128
+ |---|---|---|
129
+ | `referenceDate` | `new Date()` | "내일", "다음주"의 기준 시각 |
130
+ | `timezone` | 실행 환경 | "오후 3시"를 어느 시간대의 3시로 볼지 (IANA 이름) |
131
+ | `weekStart` | `'monday'` | "이번주/다음주" 계산 기준 (`'sunday'` 가능) |
132
+ | `defaultHour` | `0` | 시각 없는 날짜에 넣을 시 |
133
+
134
+ > [!TIP]
135
+ > **서버에서는 `timezone`을 꼭 넘기세요.** Vercel, AWS Lambda 같은 서버는 보통 UTC로 돌아서, 그대로 쓰면 한국 사용자의 "오후 3시"가 한국 시간 자정이 됩니다.
136
+ > ```ts
137
+ > parse('내일 오후 3시', { timezone: 'Asia/Seoul' }); // 서버 시간대와 상관없이 서울 오후 3시
138
+ > ```
139
+ > 브라우저에서는 사용자의 시간대가 기본값이라 보통 필요 없습니다. 내장 `Intl`로 계산해서 의존성이 늘지 않습니다.
140
+
141
+ <br>
142
+
143
+ ## 지원 표현
144
+
145
+ | 분류 | 예 |
146
+ |---|---|
147
+ | 상대 날짜 | 오늘 · 내일 · 모레 · 글피 · 어제 · 그저께 · 내일모레 · 낼 |
148
+ | N 뒤/전 | 3일 뒤 · 이틀 후 · 2주 전 · 한 달 뒤 · 1년 뒤 · 한 시간 반 뒤 · 30분 내로 |
149
+ | 주·요일 | 다음주 화요일 · 담주 월욜 · 지난 금요일 · 이번 주말 · 다다음주 |
150
+ | N째 주 | 11월 첫째 주 월요일 · 둘째 주 · 마지막 주 금요일 |
151
+ | 날짜 | 11월 5일 · 2026-11-05 · 11/5 · 26.11.5 · 11/3(화) · 십일월 오일 · 2027년 |
152
+ | 월·연 | 이번달 말 · 다음달 중순 · 올해 말 · 내년 초 · 연말 · 월초 · 내년 이맘때 |
153
+ | 시각 | 오후 3시 · 3시 반 · 15:30 · 3pm · 정오 · 자정 · 점심 · 퇴근 · 5시 10분 전 |
154
+ | 마감 | \~까지 · \~안에 · \~중으로 · 이번 주 안에 · 오전 중으로 · 퇴근 전까지 |
155
+ | 구간 | A부터 B까지 · 3\~5시 · 월\~금 · 오늘부터 3일간 · 3박 4일 · 이번 주 내내 |
156
+ | 공휴일·명절 | 크리스마스 · 빼빼로데이 · 광복절 전날 · 설날 · 설 연휴 · 추석 전날 · 부처님 오신 날 |
157
+ | 반복 | 매일 · 매주 월수금 · 매주 월\~금 · 격주 · 평일마다 · 매달 15일 · 매월 말일 · 매달 마지막 주 금요일 · 매년 3월 1일 · 2시간마다 |
158
+ | 문장 | 띄어쓰기 생략 · 조사(은·는·도) · 어림말(쯤·경·께) · 여러 개 추출 |
159
+
160
+ 입력별 결과 전체는 [SPEC.md](./SPEC.md)에 있습니다. 인식 못 하는 문장이 있으면 [제보해 주세요](https://github.com/jwn7/ko-chrono/issues/new?template=sentence.yml).
161
+
162
+ <br>
163
+
164
+ ## 해석 규칙
165
+
166
+ 애매한 표현은 아래처럼 정해 두었습니다. 같은 문장은 언제나 같은 답을 냅니다.
167
+
168
+ <details>
169
+ <summary><b>규칙 전체 보기</b> (24개 중 주요 규칙)</summary>
170
+
171
+ <br>
172
+
173
+ **요일과 주**
174
+ - "다음주 월요일" = 기준일이 속한 주의 다음 주 월요일 (금요일에 말하면 3일 뒤)
175
+ - "월요일" 단독 = 오늘 포함 가장 가까운 미래의 월요일
176
+ - "지난 금요일" = 오늘 제외 가장 가까운 과거, "다음 금요일" = 오늘 제외 가장 가까운 미래
177
+ - "이번 주말" = 그 주의 토요일. "주말" 단독 = 다가오는 토요일 (토·일이면 오늘)
178
+ - "11월 첫째 주 월요일" = 11월의 첫 번째 월요일. 요일이 없으면 N번째 월요일
179
+
180
+ **시각**
181
+ - 오전/오후가 없는 1\~6시는 오후 ("3시에 보자" → 15:00). 7\~11시는 그대로
182
+ - 시각만 있으면 오늘, 이미 지났으면 내일. 날짜를 말했으면 지났어도 그대로 ("오늘 아침" → 오늘 08:00)
183
+ - "오후 12시" = 정오. "내일 자정" · "내일 24시" · "내일 밤 12시" = 내일이 끝나는 시점 (모레 00:00)
184
+ - 시 범위의 끝은 시작의 오전·오후를 이어받음: "오후 3\~5시" → 15\~17시
185
+
186
+ **날짜**
187
+ - 월일만 있고 이미 지났으면 내년. "올해 3월 1일"처럼 연도를 말하면 그대로
188
+ - "3월", "2월 말"도 그 달이 다 지났으면 내년
189
+ - 두 자리 연도: 00\~69 → 20xx, 70\~99 → 19xx
190
+ - "내년 이맘때", "1년 뒤"는 오늘의 월·일 유지 (2/29 + 1년 → 2/28)
191
+
192
+ **구간·마감**
193
+ - "\~까지", "\~안에" → `date`는 기준 시각, `end`는 마감 (시각이 없으면 23:59)
194
+ - "이번 주 안에", "이번달 안에"는 그 기간의 마지막 날. "이번 주 내내"는 시작\~끝 구간
195
+ - "오늘부터 3일간" → `end`는 3일째 되는 날 23:59
196
+
197
+ **명절**
198
+ - 설날·추석·부처님 오신 날은 [한국천문연구원](https://astro.kasi.re.kr/life/post/calendarData) 자료가 있는 연도(현재 2025\~2028)만. 그 뒤는 추측하지 않고 `null`
199
+ - "설 연휴", "추석 연휴" = 전날부터 다음날까지 3일
200
+
201
+ **잡지 않는 것**
202
+ - 없는 날짜 (2월 30일, 11월 31일), 날짜와 요일이 안 맞는 경우 ("11월 5일 수요일")
203
+ - 기간만 있는 표현 ("3일간", "3박 4일"), 날짜가 아닌 말 ("3일 연속", "1월생", "내가 낼게")
204
+ - "내일 30분 후"처럼 날짜와 상대 시간이 섞이면 두 개의 결과로 나눔
205
+
206
+ </details>
207
+
208
+ <br>
209
+
210
+ ## 예제
211
+
212
+ | | 예제 | 실행 |
213
+ |---|---|---|
214
+ | ✅ | [**할 일 빠른 입력**](./examples/quick-add.mjs)<br>`parseEvent`로 "다음주 금요일까지 보고서 제출" → 제목 "보고서 제출" + 마감 11/6 23:59 | `node examples/quick-add.mjs "..."` |
215
+ | 💬 | [**Slack 봇**](./examples/slack-bot)<br>채널 메시지에서 날짜를 찾아 스레드로 알려줌 (UTC 서버 + `timezone`) | `node examples/slack-bot/try.mjs` |
216
+ | 🧪 | [**Playground**](./playground/index.html)<br>브라우저에서 직접 입력해 보기 | `npm run playground` |
217
+
218
+ <br>
219
+
220
+ ## LLM 대신 쓰는 이유
221
+
222
+ 날짜를 LLM에 물어볼 수도 있지만, 정해진 규칙으로 풀리는 문제라면 이쪽이 빠르고 공짜이고 매번 같은 답을 냅니다.
223
+
224
+ | | ko-chrono | LLM API |
225
+ |---|---|---|
226
+ | 지연 | 문장당 약 **0.01ms** | 문장당 수백 ms \~ 수 초 |
227
+ | 비용 | **0** | 토큰당 과금 |
228
+ | 같은 문장 → 같은 답 | **항상** | 보장 안 됨 |
229
+ | 오프라인 · 브라우저 | **됨** | 안 됨 |
230
+ | 처음 보는 표현 | 못 함 (`null`) | 대체로 함 |
231
+
232
+ 같은 메신저 문장 112개를 양쪽에 넣어 비교하는 벤치마크가 [bench/](./bench)에 있습니다 (Claude, Gemini 지원).
233
+ 규칙으로 풀리는 날짜는 ko-chrono로, 그 밖의 자유로운 문장만 LLM으로 보내는 조합을 권합니다.
234
+
235
+ <br>
236
+
237
+ ## 로드맵
238
+
239
+ - [x] 반복 일정 (매주 월요일 9시, 격주) → RRULE
240
+ - [x] 제목 분리 (`parseEvent`)
241
+ - [x] 명절·공휴일 (설 연휴, 추석 전날, 크리스마스) — 음력 명절은 한국천문연구원 자료가 있는 2025\~2028년
242
+ - [ ] 일반 음력 날짜 (음력 3월 3일)
243
+ - [ ] chrono-node `ko` 로케일 기여
244
+
245
+ ## 기여
246
+
247
+ 가장 큰 도움은 **실제로 쓰이는 문장**입니다. 인식이 안 되거나 다르게 해석되는 문장을 [이슈로 제보](https://github.com/jwn7/ko-chrono/issues/new?template=sentence.yml)해 주세요.
248
+
249
+ ```bash
250
+ npm install
251
+ npm test
252
+ npm run build
253
+ ```
254
+
255
+ ## 라이선스
256
+
257
+ [MIT](./LICENSE)
@@ -0,0 +1,18 @@
1
+ import type { ParseResult, Recurrence } from './types.js';
2
+ /** parseEvent()의 결과: 할 일·일정 한 줄을 제목과 시점으로 나눈 것 */
3
+ export interface ParsedEvent {
4
+ /** 날짜 부분을 뺀 나머지 ("내일 3시 치과" → "치과"). 없으면 '' */
5
+ title: string;
6
+ /** 첫 번째 날짜 표현의 date. 날짜가 없으면 null */
7
+ date: Date | null;
8
+ /** 구간의 끝이나 마감 */
9
+ end?: Date;
10
+ hasTime: boolean;
11
+ /** 반복 일정이면 그 규칙 */
12
+ recurrence?: Recurrence;
13
+ }
14
+ /**
15
+ * 문장에서 날짜로 인식된 부분과 거기 붙은 조사를 빼서 제목을 만든다 (규칙 28).
16
+ * 장소("강남역에서")나 동사("만나기")는 그대로 남긴다.
17
+ */
18
+ export declare function toEvent(text: string, results: ParseResult[]): ParsedEvent;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toEvent = toEvent;
4
+ // 날짜 바로 뒤에 붙은 조사: "3시에", "모레까지는", "주말엔"
5
+ const TRAILING_PARTICLE = /^(?:에는|에도|에|엔|은|는|도|까지는|까지|부터|쯤|께|경)(?=[\s,.!?]|$)/;
6
+ /**
7
+ * 문장에서 날짜로 인식된 부분과 거기 붙은 조사를 빼서 제목을 만든다 (규칙 28).
8
+ * 장소("강남역에서")나 동사("만나기")는 그대로 남긴다.
9
+ */
10
+ function toEvent(text, results) {
11
+ let rest = text;
12
+ // 뒤에서부터 지워야 앞쪽 index가 어긋나지 않는다
13
+ for (const r of [...results].sort((a, b) => b.index - a.index)) {
14
+ const after = rest.slice(r.index + r.text.length);
15
+ const particle = TRAILING_PARTICLE.exec(after)?.[0] ?? '';
16
+ rest = rest.slice(0, r.index) + ' ' + after.slice(particle.length);
17
+ }
18
+ const title = rest
19
+ .replace(/\s+/g, ' ')
20
+ .replace(/\s+([,.!?])/g, '$1')
21
+ .replace(/^[\s,]+|[\s,]+$/g, '');
22
+ const first = results[0];
23
+ if (!first)
24
+ return { title, date: null, hasTime: false };
25
+ const event = { title, date: first.date, hasTime: first.hasTime };
26
+ if (first.end)
27
+ event.end = first.end;
28
+ if (first.recurrence)
29
+ event.recurrence = first.recurrence;
30
+ return event;
31
+ }
@@ -0,0 +1,55 @@
1
+ import type { ParseOptions, ParseResult } from './types.js';
2
+ import { type ParsedEvent } from './event.js';
3
+ export type { Frequency, ParseOptions, ParseResult, Recurrence, WeekStart } from './types.js';
4
+ export type { ParsedEvent } from './event.js';
5
+ /**
6
+ * 문장에서 모든 날짜/시간 표현을 찾는다. 등장 순서대로 반환하며, 없으면 빈 배열.
7
+ *
8
+ * @example
9
+ * parseAll('내일 10시 아니면 모레 2시');
10
+ * // → [{ date: 내일 10:00, text: '내일 10시', ... }, { date: 모레 14:00, text: '모레 2시', ... }]
11
+ *
12
+ * @example
13
+ * parseAll('11월 1일부터 11월 3일까지');
14
+ * // → [{ date: 11-01 00:00, end: 11-03 00:00, ... }] "A부터 B까지"는 하나의 구간으로 합쳐진다
15
+ */
16
+ export declare function parseAll(text: string, options?: ParseOptions): ParseResult[];
17
+ /**
18
+ * 문장에서 첫 번째 날짜/시간 표현을 찾는다. 없으면 null.
19
+ *
20
+ * @example
21
+ * parse('그럼 다음주 수요일 저녁 7시에 강남에서 볼까?');
22
+ * // → { date: 다음주 수요일 19:00, text: '다음주 수요일 저녁 7시', index: 3, hasTime: true }
23
+ *
24
+ * @example
25
+ * parse('이번달 말까지 보내주세요');
26
+ * // → { date: 기준 시각, end: 이번달 마지막날 23:59, text: '이번달 말까지', hasTime: false, ... }
27
+ *
28
+ * @example
29
+ * parse('오후 3시', { referenceDate: new Date(2026, 9, 30, 14) });
30
+ * // → 2026-10-30 15:00 (시간만 있으면 오늘, 이미 지났으면 내일)
31
+ */
32
+ export declare function parse(text: string, options?: ParseOptions): ParseResult | null;
33
+ /**
34
+ * parse()의 date만 돌려주는 단축형. 구간이면 시작 시각.
35
+ *
36
+ * @example
37
+ * parseDate('다음주 화요일 오후 3시'); // → Date
38
+ * parseDate('아무 날짜도 없음'); // → null
39
+ */
40
+ export declare function parseDate(text: string, options?: ParseOptions): Date | null;
41
+ /**
42
+ * 할 일·일정 한 줄을 제목과 시점으로 나눈다. 날짜로 인식된 부분과 거기 붙은 조사를 뺀 나머지가 제목.
43
+ *
44
+ * @example
45
+ * parseEvent('다음주 금요일까지 보고서 제출');
46
+ * // → { title: '보고서 제출', date: 기준 시각, end: 금요일 23:59, hasTime: false }
47
+ *
48
+ * @example
49
+ * parseEvent('매주 월요일 오전 9시 팀 회의');
50
+ * // → { title: '팀 회의', date: 다음 월요일 09:00, hasTime: true, recurrence: { rrule: 'FREQ=WEEKLY;BYDAY=MO', ... } }
51
+ *
52
+ * @example
53
+ * parseEvent('우유 사기'); // → { title: '우유 사기', date: null, hasTime: false }
54
+ */
55
+ export declare function parseEvent(text: string, options?: ParseOptions): ParsedEvent;
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseAll = parseAll;
4
+ exports.parse = parse;
5
+ exports.parseDate = parseDate;
6
+ exports.parseEvent = parseEvent;
7
+ const relativeDay_js_1 = require("./parsers/relativeDay.js");
8
+ const offset_js_1 = require("./parsers/offset.js");
9
+ const weekday_js_1 = require("./parsers/weekday.js");
10
+ const absoluteDate_js_1 = require("./parsers/absoluteDate.js");
11
+ const relativeMonth_js_1 = require("./parsers/relativeMonth.js");
12
+ const relativeYear_js_1 = require("./parsers/relativeYear.js");
13
+ const time_js_1 = require("./parsers/time.js");
14
+ const duration_js_1 = require("./parsers/duration.js");
15
+ const recurrence_js_1 = require("./parsers/recurrence.js");
16
+ const holiday_js_1 = require("./parsers/holiday.js");
17
+ const resolve_js_1 = require("./resolve.js");
18
+ const timezone_js_1 = require("./util/timezone.js");
19
+ const event_js_1 = require("./event.js");
20
+ const PARSERS = [relativeDay_js_1.relativeDay, offset_js_1.offset, weekday_js_1.weekday, absoluteDate_js_1.absoluteDate, relativeMonth_js_1.relativeMonth, relativeYear_js_1.relativeYear, time_js_1.time, duration_js_1.duration, recurrence_js_1.recurrence, holiday_js_1.holiday];
21
+ // "~까지" / "~부터" 접미 감지. "3시~5시"의 "~"도 시작 표시로 본다
22
+ const UNTIL = /^\s*(전까지|까지|이내|안에|안으로|내로|중으로)/;
23
+ const FROM = /^\s*(부터|~)\s*/;
24
+ // "이번 주 내내" → 그 기간 전체
25
+ const WHOLE = /^\s*내내/;
26
+ function withDefaults(opts) {
27
+ return {
28
+ referenceDate: opts?.referenceDate ?? new Date(),
29
+ weekStart: opts?.weekStart ?? 'monday',
30
+ defaultHour: opts?.defaultHour ?? 0,
31
+ };
32
+ }
33
+ /** 겹치는 조각 제거: 더 긴 것 우선 */
34
+ function dedupe(frags) {
35
+ const sorted = [...frags].sort((a, b) => b.text.length - a.text.length);
36
+ const kept = [];
37
+ for (const f of sorted) {
38
+ const s = f.index, e = f.index + f.text.length;
39
+ if (kept.some((k) => s < k.index + k.text.length && e > k.index))
40
+ continue;
41
+ kept.push(f);
42
+ }
43
+ return kept;
44
+ }
45
+ /**
46
+ * 문장에서 모든 날짜/시간 표현을 찾는다. 등장 순서대로 반환하며, 없으면 빈 배열.
47
+ *
48
+ * @example
49
+ * parseAll('내일 10시 아니면 모레 2시');
50
+ * // → [{ date: 내일 10:00, text: '내일 10시', ... }, { date: 모레 14:00, text: '모레 2시', ... }]
51
+ *
52
+ * @example
53
+ * parseAll('11월 1일부터 11월 3일까지');
54
+ * // → [{ date: 11-01 00:00, end: 11-03 00:00, ... }] "A부터 B까지"는 하나의 구간으로 합쳐진다
55
+ */
56
+ function parseAll(text, options) {
57
+ const opts = withDefaults(options);
58
+ const timezone = options?.timezone;
59
+ if (timezone === undefined)
60
+ return parseLocal(text, opts);
61
+ // 그 시간대의 벽시계로 계산하고, 결과를 실제 시점으로 되돌린다 (util/timezone.ts)
62
+ const local = parseLocal(text, { ...opts, referenceDate: (0, timezone_js_1.toWallClock)(opts.referenceDate, timezone) });
63
+ return local.map((r) => {
64
+ const out = { ...r, date: (0, timezone_js_1.fromWallClock)(r.date, timezone) };
65
+ if (r.end)
66
+ out.end = (0, timezone_js_1.fromWallClock)(r.end, timezone);
67
+ return out;
68
+ });
69
+ }
70
+ /** 실행 환경의 로컬 시간대로 계산 */
71
+ function parseLocal(text, opts) {
72
+ const frags = dedupe(PARSERS.flatMap((p) => p(text)));
73
+ const groups = (0, resolve_js_1.groupFragments)(frags, text);
74
+ const results = [];
75
+ const comps = [];
76
+ for (const g of groups) {
77
+ const last = g[g.length - 1];
78
+ const tail = text.slice(last.index + last.text.length);
79
+ const until = UNTIL.exec(tail) ?? WHOLE.exec(tail);
80
+ if (until && until[0].includes('내내'))
81
+ last.components.whole = true;
82
+ else if (until)
83
+ last.components.isEnd = true;
84
+ if (FROM.test(tail))
85
+ last.components.isStart = true;
86
+ const r = (0, resolve_js_1.resolve)(g, opts);
87
+ if (!r)
88
+ continue;
89
+ let end = r._end;
90
+ if (until)
91
+ end += until[0].length;
92
+ delete r._end;
93
+ // 조각이 공백으로 시작할 수 있다 ("오후 3시" 앞의 공백). index는 첫 글자를 가리키게
94
+ const raw = text.slice(g[0].index, end);
95
+ r.index = g[0].index + (raw.length - raw.trimStart().length);
96
+ r.text = raw.trim();
97
+ results.push(r);
98
+ comps.push((0, resolve_js_1.merge)(g.map((f) => f.components)));
99
+ }
100
+ // "A부터 B까지", "A~B" → 하나의 구간으로 합침
101
+ const merged = [];
102
+ for (let i = 0; i < results.length; i++) {
103
+ const a = results[i], b = results[i + 1];
104
+ const from = FROM.exec(text.slice(a.index + a.text.length));
105
+ // "~"는 바로 뒤에 B가 붙어 있을 때만. "부터"는 B가 "까지"로 끝나거나,
106
+ // 바로 뒤에 붙은 B가 시각·일뿐일 때 ("2시부터 4시", "1일부터 3일")
107
+ const adjacent = b !== undefined && from !== null && b.index === a.index + a.text.length + from[0].length;
108
+ const tilde = from?.[1] === '~' && adjacent;
109
+ const bareFrom = from?.[1] === '부터' && adjacent && ((0, resolve_js_1.isTimeOnly)(comps[i + 1]) || (0, resolve_js_1.isDayOnly)(comps[i + 1]));
110
+ if (b && from && (b.end || tilde || bareFrom)) {
111
+ const bc = comps[i + 1];
112
+ let end = b.end ?? b.date;
113
+ if ((0, resolve_js_1.isTimeOnly)(bc)) {
114
+ end = (0, resolve_js_1.alignEnd)(a.date, bc);
115
+ }
116
+ else {
117
+ // "11월 1일부터 3일까지": 끝에 일만 있으면 시작의 연월을 이어받는다
118
+ // ("11월 28일부터 2일까지"처럼 시작보다 이르면 다음 달)
119
+ if ((0, resolve_js_1.isDayOnly)(bc)) {
120
+ const y = a.date.getFullYear(), m0 = a.date.getMonth();
121
+ end = new Date(y, m0, bc.day, end.getHours(), end.getMinutes());
122
+ if (end < a.date)
123
+ end = new Date(y, m0 + 1, bc.day, end.getHours(), end.getMinutes());
124
+ }
125
+ // "월요일부터 금요일까지": 끝 요일이 시작보다 앞이면 다음 주
126
+ if ((0, resolve_js_1.isWeekdayOnly)(bc) && end < a.date)
127
+ end.setDate(end.getDate() + 7);
128
+ if (!b.end && !b.hasTime)
129
+ end = new Date(end.getFullYear(), end.getMonth(), end.getDate(), 23, 59);
130
+ }
131
+ // "추석 ~ 11월"처럼 끝이 시작보다 앞이면 구간이 아니라 별개 표현
132
+ if (end < a.date) {
133
+ merged.push(a);
134
+ continue;
135
+ }
136
+ merged.push({ ...a, end, text: text.slice(a.index, b.index + b.text.length).trim() });
137
+ i++;
138
+ }
139
+ else {
140
+ merged.push(a);
141
+ }
142
+ }
143
+ return merged;
144
+ }
145
+ /**
146
+ * 문장에서 첫 번째 날짜/시간 표현을 찾는다. 없으면 null.
147
+ *
148
+ * @example
149
+ * parse('그럼 다음주 수요일 저녁 7시에 강남에서 볼까?');
150
+ * // → { date: 다음주 수요일 19:00, text: '다음주 수요일 저녁 7시', index: 3, hasTime: true }
151
+ *
152
+ * @example
153
+ * parse('이번달 말까지 보내주세요');
154
+ * // → { date: 기준 시각, end: 이번달 마지막날 23:59, text: '이번달 말까지', hasTime: false, ... }
155
+ *
156
+ * @example
157
+ * parse('오후 3시', { referenceDate: new Date(2026, 9, 30, 14) });
158
+ * // → 2026-10-30 15:00 (시간만 있으면 오늘, 이미 지났으면 내일)
159
+ */
160
+ function parse(text, options) {
161
+ return parseAll(text, options)[0] ?? null;
162
+ }
163
+ /**
164
+ * parse()의 date만 돌려주는 단축형. 구간이면 시작 시각.
165
+ *
166
+ * @example
167
+ * parseDate('다음주 화요일 오후 3시'); // → Date
168
+ * parseDate('아무 날짜도 없음'); // → null
169
+ */
170
+ function parseDate(text, options) {
171
+ return parse(text, options)?.date ?? null;
172
+ }
173
+ /**
174
+ * 할 일·일정 한 줄을 제목과 시점으로 나눈다. 날짜로 인식된 부분과 거기 붙은 조사를 뺀 나머지가 제목.
175
+ *
176
+ * @example
177
+ * parseEvent('다음주 금요일까지 보고서 제출');
178
+ * // → { title: '보고서 제출', date: 기준 시각, end: 금요일 23:59, hasTime: false }
179
+ *
180
+ * @example
181
+ * parseEvent('매주 월요일 오전 9시 팀 회의');
182
+ * // → { title: '팀 회의', date: 다음 월요일 09:00, hasTime: true, recurrence: { rrule: 'FREQ=WEEKLY;BYDAY=MO', ... } }
183
+ *
184
+ * @example
185
+ * parseEvent('우유 사기'); // → { title: '우유 사기', date: null, hasTime: false }
186
+ */
187
+ function parseEvent(text, options) {
188
+ return (0, event_js_1.toEvent)(text, parseAll(text, options));
189
+ }
@@ -0,0 +1 @@
1
+ { "type": "commonjs" }
@@ -0,0 +1,3 @@
1
+ import type { Parser } from '../types.js';
2
+ /** 절대 날짜 표현 */
3
+ export declare const absoluteDate: Parser;