ko-chrono 0.3.0 → 0.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.md CHANGED
@@ -14,6 +14,8 @@
14
14
  <img alt="'그럼 다음주 화요일 오후 3시에 볼까?'에서 '다음주 화요일 오후 3시'를 찾아 2026-11-03 15:00으로 바꾼다" src="./docs/banner-light.svg" width="760">
15
15
  </picture>
16
16
 
17
+ **[▶ 브라우저에서 바로 해 보기](https://jwn7.github.io/ko-chrono/)**
18
+
17
19
  [설치](#설치) · [사용법](#사용법) · [지원 표현](#지원-표현) · [해석 규칙](#해석-규칙) · [예제](#예제) · [문장 제보](https://github.com/jwn7/ko-chrono/issues/new?template=sentence.yml)
18
20
 
19
21
  </div>
@@ -176,6 +178,7 @@ parse('격주 화요일')?.recurrence?.rrule; // 'FREQ=WEEKLY;IN
176
178
  | 구간 | A부터 B까지 · 3\~5시 · 월\~금 · 오늘부터 3일간 · 3박 4일 · 이번 주 내내 |
177
179
  | 공휴일·명절 | 크리스마스 · 빼빼로데이 · 광복절 전날 · 설날 · 설 연휴 · 추석 전날 · 부처님 오신 날 |
178
180
  | 음력 | 음력 3월 3일 · 음력 윤5월 1일 · 2028년 음력 9월 9일 |
181
+ | 업무용 한자어 | 금일 · 명일 · 익일 · 작일 · 금주 · 차주 · 전주 수요일 · 익월 초 · 금월 말까지 · 익년 |
179
182
  | 반복 | 매일 · 매주 월수금 · 매주 월\~금 · 격주 · 평일마다 · 매달 15일 · 매월 말일 · 매달 마지막 주 금요일 · 매년 3월 1일 · 2시간마다 |
180
183
  | 문장 | 띄어쓰기 생략 · 조사(은·는·도) · 어림말(쯤·경·께) · 여러 개 추출 |
181
184
 
@@ -224,6 +227,7 @@ parse('격주 화요일')?.recurrence?.rrule; // 'FREQ=WEEKLY;IN
224
227
  **잡지 않는 것**
225
228
  - 없는 날짜 (2월 30일, 11월 31일), 날짜와 요일이 안 맞는 경우 ("11월 5일 수요일")
226
229
  - 기간만 있는 표현 ("3일간", "3박 4일"), 날짜가 아닌 말 ("3일 연속", "1월생", "내가 낼게")
230
+ - 한자어가 다른 뜻일 때: "금주 결심"(禁酒), "전주 여행"(도시), "차주에게 연락"(車主), "전월세"
227
231
  - "내일 30분 후"처럼 날짜와 상대 시간이 섞이면 두 개의 결과로 나눔
228
232
 
229
233
  </details>
@@ -236,7 +240,7 @@ parse('격주 화요일')?.recurrence?.rrule; // 'FREQ=WEEKLY;IN
236
240
  |---|---|---|
237
241
  | ✅ | [**할 일 빠른 입력**](./examples/quick-add.mjs)<br>`parseEvent`로 "다음주 금요일까지 보고서 제출" → 제목 "보고서 제출" + 마감 11/6 23:59 | `node examples/quick-add.mjs "..."` |
238
242
  | 💬 | [**Slack 봇**](./examples/slack-bot)<br>채널 메시지에서 날짜를 찾아 스레드로 알려줌 (UTC 서버 + `timezone`) | `node examples/slack-bot/try.mjs` |
239
- | 🧪 | [**Playground**](./playground/index.html)<br>브라우저에서 직접 입력해 보기 | `npm run playground` |
243
+ | 🧪 | [**Playground**](https://jwn7.github.io/ko-chrono/)<br>브라우저에서 직접 입력해 보기 (문장을 넣으면 공유 링크가 생김) | [jwn7.github.io/ko-chrono](https://jwn7.github.io/ko-chrono/) · 로컬은 `npm run playground` |
240
244
 
241
245
  <br>
242
246
 
@@ -3,6 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.relativeDay = void 0;
4
4
  const WORDS = {
5
5
  그저께: -2,
6
+ // 업무용 한자어 (규칙 33)
7
+ 금일: 0,
8
+ 명일: 1,
9
+ 익일: 1,
10
+ 작일: -1,
6
11
  내일모레: 2,
7
12
  낼모레: 2,
8
13
  낼: 1,
@@ -13,8 +18,8 @@ const WORDS = {
13
18
  모레: 2,
14
19
  글피: 3,
15
20
  };
16
- // "내일모레"는 "내일"+"모레"가 아니라 모레. "낼"은 "내가 낼게"(내다)와 겹쳐서 뒤에 시각·약속 표현이 올 때만
17
- const RE = /(그저께|그제|어제|오늘|내일모레|낼모레|내일|모레|글피|(?<![가-힣])낼(?=\s*(?:오전|오후|아침|점심|저녁|밤|새벽|낮|\d|봐|보자|만나|까지)))/g;
21
+ // "금일", "익일 배송", "작일" (업무용 한자어). "내일모레"는 "내일"+"모레"가 아니라 모레. "낼"은 "내가 낼게"(내다)와 겹쳐서 뒤에 시각·약속 표현이 올 때만
22
+ const RE = /(그저께|그제|어제|오늘|내일모레|낼모레|내일|모레|글피|(?<![가-힣])(?:금일|명일|익일|작일)|(?<![가-힣])낼(?=\s*(?:오전|오후|아침|점심|저녁|밤|새벽|낮|\d|봐|보자|만나|까지)))/g;
18
23
  /** 오늘/내일/모레/글피/어제/그저께 */
19
24
  const relativeDay = (text) => {
20
25
  const out = [];
@@ -7,6 +7,9 @@ const PART = { 초: 'start', 중순: 'middle', 말: 'end' };
7
7
  const RE = /(지난|저번|이번|다다음|다음|담)\s*달\s*(초|중순|말|이맘때)?|(?<![가-힣])월(초|말)/g;
8
8
  // "11월 말", "12월 초" (absoluteDate의 MONTH_ONLY와 겹치지만 part가 있는 경우 우선)
9
9
  const ABS_PART = /(?<![\d가-힣])(\d{1,2})\s*월\s*(초|중순|말)/g;
10
+ // 업무용 한자어 달 (규칙 33): "익월 초", "금월 말까지", "전월 실적". "전월세"는 제외
11
+ const FORMAL_MONTH = /(?<![가-힣])(금|익|차|전)월(?!세)(?:\s*(초|중순|말))?/g;
12
+ const FORMAL_MONTH_OFFSET = { 금: 0, 익: 1, 차: 1, 전: -1 };
10
13
  /** 이번달/다음달 (초/중순/말) */
11
14
  const relativeMonth = (text) => {
12
15
  const out = [];
@@ -22,6 +25,12 @@ const relativeMonth = (text) => {
22
25
  c.monthPart = PART[m[2]];
23
26
  out.push({ text: m[0], index: m.index, components: c });
24
27
  }
28
+ for (const m of text.matchAll(FORMAL_MONTH)) {
29
+ const c = { monthOffset: FORMAL_MONTH_OFFSET[m[1]] };
30
+ if (m[2])
31
+ c.monthPart = PART[m[2]];
32
+ out.push({ text: m[0], index: m.index, components: c });
33
+ }
25
34
  for (const m of text.matchAll(ABS_PART)) {
26
35
  out.push({ text: m[0], index: m.index, components: { month: +m[1], monthPart: PART[m[2]] } });
27
36
  }
@@ -2,10 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.relativeYear = void 0;
4
4
  const WORDS = {
5
- 재작년: -2, 작년: -1, 지난해: -1, 올해: 0, 금년: 0, 내년: 1, 명년: 1, 내후년: 2,
5
+ 재작년: -2, 작년: -1, 지난해: -1, 올해: 0, 금년: 0, 내년: 1, 명년: 1, 익년: 1, 내후년: 2,
6
6
  };
7
7
  // "올해 말", "내년 초", "작년", "재작년", "내후년", "지난해", "내년 이맘때", "연말", "연초"
8
- const RE = /(재작년|작년|지난해|올해|금년|내후년|내년|명년)(?:\s*(초|말|이맘때))?|(?<![가-힣])(연말(?!정산)|연초)/g;
8
+ const RE = /(재작년|작년|지난해|올해|금년|내후년|내년|명년|익년)(?:\s*(초|말|이맘때))?|(?<![가-힣])(연말(?!정산)|연초)/g;
9
9
  /** 올해/내년/작년 (초/말) */
10
10
  const relativeYear = (text) => {
11
11
  const out = [];
@@ -20,6 +20,13 @@ const NTH = new RegExp(`(?:(\\d{1,2})\\s*월\\s*)?(첫째|첫|둘째|셋째|넷
20
20
  const NTH_NUM = { 첫째: 1, 첫: 1, 둘째: 2, 셋째: 3, 넷째: 4, 다섯째: 5, 마지막: -1 };
21
21
  // "2주 뒤 월요일", "3주 후 금요일" → N주 뒤의 그 주의 X요일 ("다다음주 월요일"과 같은 계산)
22
22
  const WEEKS_LATER = new RegExp(`(\\d{1,2}|한|두|세|네|다섯)\\s*주\\s*(?:뒤|후)\\s*(${DAY})(?:요일|욜)`, 'g');
23
+ // 업무용 한자어 주 (규칙 33): "차주 금요일", "금주 월요일", "전주 수요일". 금주(禁酒)·전주(도시)·차주(車主)와
24
+ // 겹치므로 요일은 "요일/욜"까지 붙어야 하고, 요일 없이 쓸 때는 뒤에 날짜 말(내로·중으로·에·까지…)이 와야 한다.
25
+ // "전주"는 도시 이름과 너무 자주 겹쳐서 요일이 붙을 때만
26
+ const FORMAL_WEEK = { 전전: -2, 전: -1, 금: 0, 차: 1, 익: 1, 차차: 2 };
27
+ const FORMAL_WITH_DAY = new RegExp(`(?<![가-힣])(전전|차차|전|금|차|익)주\\s*(${DAY})(?:요일|욜)`, 'g');
28
+ // "차주 중으로", "금주 내로", "차주에 회의", "익주까지". "금주에"는 "금주에 성공했어"(禁酒)와 겹쳐서 제외
29
+ const FORMAL_WEEK_ONLY = /(?<![가-힣])(?:(전전|차차|차|익)주(?=\s*(?:에(?!게|서)|에는|중으로|중에|내로|내|안에|안으로|까지|초|말|부터))|(금)주(?=\s*(?:중으로|중에|내로|내|안에|안으로|까지|초|말|부터)))/g;
23
30
  // "지난 금요일" = 오늘 제외 가장 가까운 과거, "다음 금요일" = 오늘 제외 가장 가까운 미래
24
31
  const DIR = new RegExp(`(지난|저번|다음)\\s*(${DAY})(?:요일|욜)`, 'g');
25
32
  // "주말에 보자" ("이번 주말"은 위 WEEKEND가 먼저 잡는다)
@@ -35,6 +42,16 @@ const weekday = (text) => {
35
42
  const out = [];
36
43
  const taken = [];
37
44
  const overlaps = (i, len) => taken.some(([s, e]) => i < e && i + len > s);
45
+ for (const m of text.matchAll(FORMAL_WITH_DAY)) {
46
+ out.push({ text: m[0], index: m.index, components: { weekOffset: FORMAL_WEEK[m[1]], weekday: WEEKDAY[m[2]] } });
47
+ taken.push([m.index, m.index + m[0].length]);
48
+ }
49
+ for (const m of text.matchAll(FORMAL_WEEK_ONLY)) {
50
+ if (overlaps(m.index, m[0].length))
51
+ continue;
52
+ out.push({ text: m[0], index: m.index, components: { weekOffset: FORMAL_WEEK[m[1] ?? m[2]] } });
53
+ taken.push([m.index, m.index + m[0].length]);
54
+ }
38
55
  for (const m of text.matchAll(WEEKS_LATER)) {
39
56
  const n = (0, number_js_1.parseKoreanNumber)(m[1]);
40
57
  if (n == null)
@@ -1,5 +1,10 @@
1
1
  const WORDS = {
2
2
  그저께: -2,
3
+ // 업무용 한자어 (규칙 33)
4
+ 금일: 0,
5
+ 명일: 1,
6
+ 익일: 1,
7
+ 작일: -1,
3
8
  내일모레: 2,
4
9
  낼모레: 2,
5
10
  낼: 1,
@@ -10,8 +15,8 @@ const WORDS = {
10
15
  모레: 2,
11
16
  글피: 3,
12
17
  };
13
- // "내일모레"는 "내일"+"모레"가 아니라 모레. "낼"은 "내가 낼게"(내다)와 겹쳐서 뒤에 시각·약속 표현이 올 때만
14
- const RE = /(그저께|그제|어제|오늘|내일모레|낼모레|내일|모레|글피|(?<![가-힣])낼(?=\s*(?:오전|오후|아침|점심|저녁|밤|새벽|낮|\d|봐|보자|만나|까지)))/g;
18
+ // "금일", "익일 배송", "작일" (업무용 한자어). "내일모레"는 "내일"+"모레"가 아니라 모레. "낼"은 "내가 낼게"(내다)와 겹쳐서 뒤에 시각·약속 표현이 올 때만
19
+ const RE = /(그저께|그제|어제|오늘|내일모레|낼모레|내일|모레|글피|(?<![가-힣])(?:금일|명일|익일|작일)|(?<![가-힣])낼(?=\s*(?:오전|오후|아침|점심|저녁|밤|새벽|낮|\d|봐|보자|만나|까지)))/g;
15
20
  /** 오늘/내일/모레/글피/어제/그저께 */
16
21
  export const relativeDay = (text) => {
17
22
  const out = [];
@@ -4,6 +4,9 @@ const PART = { 초: 'start', 중순: 'middle', 말: 'end' };
4
4
  const RE = /(지난|저번|이번|다다음|다음|담)\s*달\s*(초|중순|말|이맘때)?|(?<![가-힣])월(초|말)/g;
5
5
  // "11월 말", "12월 초" (absoluteDate의 MONTH_ONLY와 겹치지만 part가 있는 경우 우선)
6
6
  const ABS_PART = /(?<![\d가-힣])(\d{1,2})\s*월\s*(초|중순|말)/g;
7
+ // 업무용 한자어 달 (규칙 33): "익월 초", "금월 말까지", "전월 실적". "전월세"는 제외
8
+ const FORMAL_MONTH = /(?<![가-힣])(금|익|차|전)월(?!세)(?:\s*(초|중순|말))?/g;
9
+ const FORMAL_MONTH_OFFSET = { 금: 0, 익: 1, 차: 1, 전: -1 };
7
10
  /** 이번달/다음달 (초/중순/말) */
8
11
  export const relativeMonth = (text) => {
9
12
  const out = [];
@@ -19,6 +22,12 @@ export const relativeMonth = (text) => {
19
22
  c.monthPart = PART[m[2]];
20
23
  out.push({ text: m[0], index: m.index, components: c });
21
24
  }
25
+ for (const m of text.matchAll(FORMAL_MONTH)) {
26
+ const c = { monthOffset: FORMAL_MONTH_OFFSET[m[1]] };
27
+ if (m[2])
28
+ c.monthPart = PART[m[2]];
29
+ out.push({ text: m[0], index: m.index, components: c });
30
+ }
22
31
  for (const m of text.matchAll(ABS_PART)) {
23
32
  out.push({ text: m[0], index: m.index, components: { month: +m[1], monthPart: PART[m[2]] } });
24
33
  }
@@ -1,8 +1,8 @@
1
1
  const WORDS = {
2
- 재작년: -2, 작년: -1, 지난해: -1, 올해: 0, 금년: 0, 내년: 1, 명년: 1, 내후년: 2,
2
+ 재작년: -2, 작년: -1, 지난해: -1, 올해: 0, 금년: 0, 내년: 1, 명년: 1, 익년: 1, 내후년: 2,
3
3
  };
4
4
  // "올해 말", "내년 초", "작년", "재작년", "내후년", "지난해", "내년 이맘때", "연말", "연초"
5
- const RE = /(재작년|작년|지난해|올해|금년|내후년|내년|명년)(?:\s*(초|말|이맘때))?|(?<![가-힣])(연말(?!정산)|연초)/g;
5
+ const RE = /(재작년|작년|지난해|올해|금년|내후년|내년|명년|익년)(?:\s*(초|말|이맘때))?|(?<![가-힣])(연말(?!정산)|연초)/g;
6
6
  /** 올해/내년/작년 (초/말) */
7
7
  export const relativeYear = (text) => {
8
8
  const out = [];
@@ -17,6 +17,13 @@ const NTH = new RegExp(`(?:(\\d{1,2})\\s*월\\s*)?(첫째|첫|둘째|셋째|넷
17
17
  const NTH_NUM = { 첫째: 1, 첫: 1, 둘째: 2, 셋째: 3, 넷째: 4, 다섯째: 5, 마지막: -1 };
18
18
  // "2주 뒤 월요일", "3주 후 금요일" → N주 뒤의 그 주의 X요일 ("다다음주 월요일"과 같은 계산)
19
19
  const WEEKS_LATER = new RegExp(`(\\d{1,2}|한|두|세|네|다섯)\\s*주\\s*(?:뒤|후)\\s*(${DAY})(?:요일|욜)`, 'g');
20
+ // 업무용 한자어 주 (규칙 33): "차주 금요일", "금주 월요일", "전주 수요일". 금주(禁酒)·전주(도시)·차주(車主)와
21
+ // 겹치므로 요일은 "요일/욜"까지 붙어야 하고, 요일 없이 쓸 때는 뒤에 날짜 말(내로·중으로·에·까지…)이 와야 한다.
22
+ // "전주"는 도시 이름과 너무 자주 겹쳐서 요일이 붙을 때만
23
+ const FORMAL_WEEK = { 전전: -2, 전: -1, 금: 0, 차: 1, 익: 1, 차차: 2 };
24
+ const FORMAL_WITH_DAY = new RegExp(`(?<![가-힣])(전전|차차|전|금|차|익)주\\s*(${DAY})(?:요일|욜)`, 'g');
25
+ // "차주 중으로", "금주 내로", "차주에 회의", "익주까지". "금주에"는 "금주에 성공했어"(禁酒)와 겹쳐서 제외
26
+ const FORMAL_WEEK_ONLY = /(?<![가-힣])(?:(전전|차차|차|익)주(?=\s*(?:에(?!게|서)|에는|중으로|중에|내로|내|안에|안으로|까지|초|말|부터))|(금)주(?=\s*(?:중으로|중에|내로|내|안에|안으로|까지|초|말|부터)))/g;
20
27
  // "지난 금요일" = 오늘 제외 가장 가까운 과거, "다음 금요일" = 오늘 제외 가장 가까운 미래
21
28
  const DIR = new RegExp(`(지난|저번|다음)\\s*(${DAY})(?:요일|욜)`, 'g');
22
29
  // "주말에 보자" ("이번 주말"은 위 WEEKEND가 먼저 잡는다)
@@ -32,6 +39,16 @@ export const weekday = (text) => {
32
39
  const out = [];
33
40
  const taken = [];
34
41
  const overlaps = (i, len) => taken.some(([s, e]) => i < e && i + len > s);
42
+ for (const m of text.matchAll(FORMAL_WITH_DAY)) {
43
+ out.push({ text: m[0], index: m.index, components: { weekOffset: FORMAL_WEEK[m[1]], weekday: WEEKDAY[m[2]] } });
44
+ taken.push([m.index, m.index + m[0].length]);
45
+ }
46
+ for (const m of text.matchAll(FORMAL_WEEK_ONLY)) {
47
+ if (overlaps(m.index, m[0].length))
48
+ continue;
49
+ out.push({ text: m[0], index: m.index, components: { weekOffset: FORMAL_WEEK[m[1] ?? m[2]] } });
50
+ taken.push([m.index, m.index + m[0].length]);
51
+ }
35
52
  for (const m of text.matchAll(WEEKS_LATER)) {
36
53
  const n = parseKoreanNumber(m[1]);
37
54
  if (n == null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ko-chrono",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "한국어 자연어 날짜/시간 파서 — '다음주 화요일 오후 3시' → Date",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -22,6 +22,7 @@
22
22
  "files": ["dist", "README.md", "LICENSE"],
23
23
  "scripts": {
24
24
  "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.cjs.json && node scripts/mark-cjs.mjs",
25
+ "prepublishOnly": "npm run build && npm test",
25
26
  "test": "vitest run",
26
27
  "test:watch": "vitest",
27
28
  "typecheck": "tsc --noEmit",