ko-chrono 0.1.0 → 0.3.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 (57) hide show
  1. package/README.md +237 -66
  2. package/dist/cjs/event.d.ts +20 -0
  3. package/dist/cjs/event.js +33 -0
  4. package/dist/cjs/index.d.ts +55 -0
  5. package/dist/cjs/index.js +192 -0
  6. package/dist/cjs/package.json +1 -0
  7. package/dist/cjs/parsers/absoluteDate.d.ts +3 -0
  8. package/dist/cjs/parsers/absoluteDate.js +95 -0
  9. package/dist/cjs/parsers/duration.d.ts +3 -0
  10. package/dist/cjs/parsers/duration.js +45 -0
  11. package/dist/cjs/parsers/holiday.d.ts +3 -0
  12. package/dist/cjs/parsers/holiday.js +72 -0
  13. package/dist/cjs/parsers/offset.d.ts +3 -0
  14. package/dist/cjs/parsers/offset.js +64 -0
  15. package/dist/cjs/parsers/recurrence.d.ts +5 -0
  16. package/dist/cjs/parsers/recurrence.js +117 -0
  17. package/dist/cjs/parsers/relativeDay.d.ts +3 -0
  18. package/dist/cjs/parsers/relativeDay.js +26 -0
  19. package/dist/cjs/parsers/relativeMonth.d.ts +3 -0
  20. package/dist/cjs/parsers/relativeMonth.js +30 -0
  21. package/dist/cjs/parsers/relativeYear.d.ts +3 -0
  22. package/dist/cjs/parsers/relativeYear.js +30 -0
  23. package/dist/cjs/parsers/time.d.ts +3 -0
  24. package/dist/cjs/parsers/time.js +152 -0
  25. package/dist/cjs/parsers/weekday.d.ts +3 -0
  26. package/dist/cjs/parsers/weekday.js +109 -0
  27. package/dist/cjs/resolve.d.ts +21 -0
  28. package/dist/cjs/resolve.js +446 -0
  29. package/dist/cjs/types.d.ts +125 -0
  30. package/dist/cjs/types.js +2 -0
  31. package/dist/cjs/util/lunarHolidays.d.ts +11 -0
  32. package/dist/cjs/util/lunarHolidays.js +82 -0
  33. package/dist/cjs/util/number.d.ts +5 -0
  34. package/dist/cjs/util/number.js +42 -0
  35. package/dist/cjs/util/timezone.d.ts +4 -0
  36. package/dist/cjs/util/timezone.js +55 -0
  37. package/dist/event.d.ts +20 -0
  38. package/dist/event.js +30 -0
  39. package/dist/index.d.ts +18 -1
  40. package/dist/index.js +54 -6
  41. package/dist/parsers/absoluteDate.js +6 -2
  42. package/dist/parsers/duration.js +4 -2
  43. package/dist/parsers/holiday.d.ts +3 -0
  44. package/dist/parsers/holiday.js +68 -0
  45. package/dist/parsers/offset.js +6 -3
  46. package/dist/parsers/recurrence.d.ts +5 -0
  47. package/dist/parsers/recurrence.js +112 -0
  48. package/dist/parsers/time.js +22 -2
  49. package/dist/parsers/weekday.js +10 -0
  50. package/dist/resolve.d.ts +2 -2
  51. package/dist/resolve.js +164 -13
  52. package/dist/types.d.ts +53 -0
  53. package/dist/util/lunarHolidays.d.ts +11 -0
  54. package/dist/util/lunarHolidays.js +76 -0
  55. package/dist/util/timezone.d.ts +4 -0
  56. package/dist/util/timezone.js +51 -0
  57. package/package.json +13 -5
@@ -0,0 +1,3 @@
1
+ import type { Parser } from '../types.js';
2
+ /** 절대 날짜 표현 */
3
+ export declare const absoluteDate: Parser;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.absoluteDate = void 0;
4
+ const number_js_1 = require("../util/number.js");
5
+ // 2026년 11월 5일 / 26년 11월 5일 / 11월 5일 / 11/5 / 2026-11-05 / 26.11.5 / 11.5(월일로 해석)
6
+ const FULL = /(?:(?<!\d)(\d{4}|\d{2})\s*년\s*)?(\d{1,2})\s*월\s*(\d{1,2})\s*일(?!간|\s*동안)/g;
7
+ // "2027-01-01T09:00" (ISO 8601 날짜+시각)
8
+ const ISO = /(?<![\d/.-])(\d{4}|\d{2})[-./]\s*(\d{1,2})[-./]\s*(\d{1,2})(?!\d)(?:T(\d{2}):(\d{2}))?/g;
9
+ const SLASH = /(?<![\d/.-])(\d{1,2})\s*[/.]\s*(\d{1,2})(?![\d/.:])/g;
10
+ // "십일월 오일", "유월 이십오일" (오탐 방지를 위해 월과 일이 모두 있을 때만)
11
+ const HANGUL = /(?<![가-힣])(유|시|[일이삼사오육칠팔구십]{1,2})월\s*([일이삼사오육칠팔구십]{1,3})일(?!요일)/g;
12
+ const HANGUL_MONTH = { 유: 6, 시: 10 };
13
+ // "2027년" 단독 (월이 뒤따르지 않을 때). "26년"은 "26년 동안" 오탐이 있어 4자리만
14
+ const YEAR_ONLY = /(?<!\d)((?:19|20)\d{2})\s*년(?!\s*\d{1,2}\s*월)/g;
15
+ // "11월" 단독, "5일" 단독
16
+ const MONTH_ONLY = /(?<![\d가-힣])(\d{1,2})\s*월(?!\s*\d{1,2}\s*일|생|세)/g;
17
+ // "5일간", "3일 동안", "3박 4일"은 기간이지 날짜가 아니다
18
+ const DAY_ONLY = /(?<!\d)(?<!\d\s*박\s*)(\d{1,2})\s*일(?!요일|간|장|째|차|분|\s*동안|\s*연속)/g;
19
+ /** 두 자리 연도 확장: 00~69 → 20xx, 70~99 → 19xx */
20
+ const expandYear = (y) => {
21
+ const n = +y;
22
+ if (y.length !== 2)
23
+ return n;
24
+ return n < 70 ? 2000 + n : 1900 + n;
25
+ };
26
+ /** 절대 날짜 표현 */
27
+ const absoluteDate = (text) => {
28
+ const out = [];
29
+ const taken = [];
30
+ const take = (i, len) => {
31
+ if (taken.some(([s, e]) => i < e && i + len > s))
32
+ return false;
33
+ taken.push([i, i + len]);
34
+ return true;
35
+ };
36
+ for (const m of text.matchAll(ISO)) {
37
+ const month = +m[2], day = +m[3];
38
+ if (month < 1 || month > 12 || day < 1 || day > 31)
39
+ continue;
40
+ if (!take(m.index, m[0].length))
41
+ continue;
42
+ const c = { year: expandYear(m[1]), month, day };
43
+ if (m[4] && +m[4] < 24 && +m[5] < 60)
44
+ Object.assign(c, { hour: +m[4], minute: +m[5], meridiem: true });
45
+ out.push({ text: m[0], index: m.index, components: c });
46
+ }
47
+ for (const m of text.matchAll(FULL)) {
48
+ if (!take(m.index, m[0].length))
49
+ continue;
50
+ const c = { month: +m[2], day: +m[3] };
51
+ if (m[1])
52
+ c.year = expandYear(m[1]);
53
+ out.push({ text: m[0], index: m.index, components: c });
54
+ }
55
+ for (const m of text.matchAll(HANGUL)) {
56
+ const month = HANGUL_MONTH[m[1]] ?? (0, number_js_1.parseKoreanNumber)(m[1]);
57
+ const day = (0, number_js_1.parseKoreanNumber)(m[2]);
58
+ if (month == null || day == null || month < 1 || month > 12 || day < 1 || day > 31)
59
+ continue;
60
+ if (!take(m.index, m[0].length))
61
+ continue;
62
+ out.push({ text: m[0], index: m.index, components: { month, day } });
63
+ }
64
+ for (const m of text.matchAll(SLASH)) {
65
+ if (!take(m.index, m[0].length))
66
+ continue;
67
+ const month = +m[1], day = +m[2];
68
+ if (month < 1 || month > 12 || day < 1 || day > 31)
69
+ continue;
70
+ out.push({ text: m[0], index: m.index, components: { month, day } });
71
+ }
72
+ for (const m of text.matchAll(YEAR_ONLY)) {
73
+ if (!take(m.index, m[0].length))
74
+ continue;
75
+ out.push({ text: m[0], index: m.index, components: { year: +m[1] } });
76
+ }
77
+ for (const m of text.matchAll(MONTH_ONLY)) {
78
+ if (!take(m.index, m[0].length))
79
+ continue;
80
+ const month = +m[1];
81
+ if (month < 1 || month > 12)
82
+ continue;
83
+ out.push({ text: m[0], index: m.index, components: { month } });
84
+ }
85
+ for (const m of text.matchAll(DAY_ONLY)) {
86
+ if (!take(m.index, m[0].length))
87
+ continue;
88
+ const day = +m[1];
89
+ if (day < 1 || day > 31)
90
+ continue;
91
+ out.push({ text: m[0], index: m.index, components: { day } });
92
+ }
93
+ return out;
94
+ };
95
+ exports.absoluteDate = absoluteDate;
@@ -0,0 +1,3 @@
1
+ import type { Parser } from '../types.js';
2
+ /** 기간 표현. 날짜를 계산하지 않고 길이만 뽑는다. "A부터 N일간"으로 쓰일 때 resolve가 end를 만든다. */
3
+ export declare const duration: Parser;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.duration = void 0;
4
+ const number_js_1 = require("../util/number.js");
5
+ // "3일간", "일주일 동안", "2주간", "한 달간", "2시간 동안", "90분간", "이틀간"
6
+ const RE = /(?:(\d+|[일이삼사오육칠팔구십한두세네다섯여섯일곱여덟아홉열]+)\s*(일|주|주일|달|개월|시간|분)|(보름|열흘|이틀|사흘|나흘|닷새|엿새|이레|여드레|아흐레))\s*(간|동안)/g;
7
+ // "3박 4일" → 4일
8
+ const NIGHTS = /(\d+)\s*박\s*(\d+)\s*일/g;
9
+ /** 기간 표현. 날짜를 계산하지 않고 길이만 뽑는다. "A부터 N일간"으로 쓰일 때 resolve가 end를 만든다. */
10
+ const duration = (text) => {
11
+ const out = [];
12
+ for (const m of text.matchAll(NIGHTS)) {
13
+ if (+m[2] === 0)
14
+ continue;
15
+ out.push({ text: m[0], index: m.index, components: { durationDays: +m[2] } });
16
+ }
17
+ for (const m of text.matchAll(RE)) {
18
+ const n = (0, number_js_1.parseKoreanNumber)(m[3] ?? m[1]);
19
+ if (!n)
20
+ continue; // "0일간"은 기간이 아니다
21
+ const c = {};
22
+ switch (m[3] ? '일' : m[2]) {
23
+ case '일':
24
+ c.durationDays = n;
25
+ break;
26
+ case '주':
27
+ case '주일':
28
+ c.durationDays = n * 7;
29
+ break;
30
+ case '달':
31
+ case '개월':
32
+ c.durationMonths = n;
33
+ break;
34
+ case '시간':
35
+ c.durationMinutes = n * 60;
36
+ break;
37
+ case '분':
38
+ c.durationMinutes = n;
39
+ break;
40
+ }
41
+ out.push({ text: m[0], index: m.index, components: c });
42
+ }
43
+ return out;
44
+ };
45
+ exports.duration = duration;
@@ -0,0 +1,3 @@
1
+ import type { Parser } from '../types.js';
2
+ /** 공휴일·기념일·명절. 음력 명절의 양력 날짜는 resolve가 표에서 찾는다 */
3
+ export declare const holiday: Parser;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.holiday = void 0;
4
+ // 양력 고정 공휴일·기념일 → [월, 일]. 여러 단어는 띄어쓰기를 생략해도 된다
5
+ const FIXED = {
6
+ '크리스마스 이브': [12, 24],
7
+ 크리스마스: [12, 25],
8
+ 성탄절: [12, 25],
9
+ 신정: [1, 1],
10
+ 삼일절: [3, 1],
11
+ '근로자의 날': [5, 1],
12
+ 노동절: [5, 1],
13
+ 어린이날: [5, 5],
14
+ 어버이날: [5, 8],
15
+ '스승의 날': [5, 15],
16
+ 현충일: [6, 6],
17
+ 제헌절: [7, 17],
18
+ 광복절: [8, 15],
19
+ 개천절: [10, 3],
20
+ 한글날: [10, 9],
21
+ 할로윈: [10, 31],
22
+ 핼러윈: [10, 31],
23
+ 빼빼로데이: [11, 11],
24
+ 발렌타인데이: [2, 14],
25
+ 밸런타인데이: [2, 14],
26
+ 화이트데이: [3, 14],
27
+ };
28
+ const SHIFT = { 전날: -1, 다음날: 1, 이튿날: 1, 당일: 0 };
29
+ const MODIFIER = '(?:\\s*(전날|다음\\s*날|이튿날|당일))?';
30
+ // "크리스마스", "광복절 전날", "스승의날"
31
+ const FIXED_RE = new RegExp(`(${Object.keys(FIXED).map((k) => k.replace(/ /g, '\\s*')).join('|')})${MODIFIER}`, 'g');
32
+ // "설날", "구정", "설 연휴", "추석 전날", "한가위", "부처님 오신 날". "설"만은 "설명", "설거지"와 겹쳐서 뒤에 연휴·전날 등이 올 때만
33
+ const LUNAR_RE = /(설날|구정|설(?=\s*(?:연휴|전날|다음\s*날|당일))|추석|한가위|부처님\s*오신\s*날|석가\s*탄신일|석탄일|초파일)(?:\s*(연휴))?(?:\s*(전날|다음\s*날|이튿날|당일))?/g;
34
+ // "음력 3월 3일", "음력 윤5월 1일" (음력이라고 말한 경우만)
35
+ const LUNAR_DATE_RE = /음력\s*(윤\s*)?(\d{1,2})\s*월\s*(\d{1,2})\s*일/g;
36
+ const fixedKey = (name) => Object.keys(FIXED).find((k) => k.replace(/ /g, '') === name.replace(/\s/g, ''));
37
+ const shiftOf = (m) => (m ? SHIFT[m.replace(/\s/g, '')] : 0);
38
+ /** 공휴일·기념일·명절. 음력 명절의 양력 날짜는 resolve가 표에서 찾는다 */
39
+ const holiday = (text) => {
40
+ const out = [];
41
+ for (const m of text.matchAll(FIXED_RE)) {
42
+ const [month, day] = FIXED[fixedKey(m[1])];
43
+ const c = { month, day };
44
+ const shift = shiftOf(m[2]);
45
+ if (shift)
46
+ c.dayOffset = shift;
47
+ out.push({ text: m[0], index: m.index, components: c });
48
+ }
49
+ for (const m of text.matchAll(LUNAR_DATE_RE)) {
50
+ const month = +m[2], day = +m[3];
51
+ if (month < 1 || month > 12 || day < 1 || day > 30)
52
+ continue;
53
+ out.push({ text: m[0], index: m.index, components: { lunarDate: { month, day, leap: !!m[1] } } });
54
+ }
55
+ for (const m of text.matchAll(LUNAR_RE)) {
56
+ const name = m[1].replace(/\s/g, '');
57
+ const lunar = name === '설날' || name === '구정' || name === '설' ? 'seollal'
58
+ : name === '추석' || name === '한가위' ? 'chuseok' : 'buddha';
59
+ const c = { lunar };
60
+ // "설 연휴", "추석 연휴" = 전날부터 다음날까지 3일 (부처님 오신 날은 연휴가 없다)
61
+ if (m[2] && lunar !== 'buddha')
62
+ Object.assign(c, { dayOffset: -1, durationDays: 3 });
63
+ else {
64
+ const shift = shiftOf(m[3]);
65
+ if (shift)
66
+ c.dayOffset = shift;
67
+ }
68
+ out.push({ text: m[0], index: m.index, components: c });
69
+ }
70
+ return out;
71
+ };
72
+ exports.holiday = holiday;
@@ -0,0 +1,3 @@
1
+ import type { Parser } from '../types.js';
2
+ /** "N일 뒤 / N주 후 / N달 전 / N시간 뒤 / N분 후" */
3
+ export declare const offset: Parser;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.offset = void 0;
4
+ const number_js_1 = require("../util/number.js");
5
+ // "3일 뒤", "2주 후", "한 달 전", "세 시간 뒤", "30분 후", "열흘 뒤", "이틀 뒤", "1년 뒤", "30분 내로", "30분쯤 뒤"
6
+ const RE = /(?:(\d+|[일이삼사오육칠팔구십한두세네다섯여섯일곱여덟아홉열]+)\s*(일|주|주일|달|개월|년|시간|분)|(보름|열흘|이틀|사흘|나흘|닷새|엿새|이레|여드레|아흐레))\s*(?:쯤|정도)?\s*(뒤|후|전|이내|안에|안으로|내로|이후)/g;
7
+ // "한 시간 반 뒤", "1시간 30분 후", "2시간반 전"
8
+ const HOUR_MIN = /(\d+|[일이삼사오육칠팔구십한두세네다섯여섯일곱여덟아홉열]+)\s*시간\s*(?:(반)|(\d+)\s*분)\s*(?:쯤|정도)?\s*(뒤|후|전|이내|안에|이후)/g;
9
+ /** "N일 뒤 / N주 후 / N달 전 / N시간 뒤 / N분 후" */
10
+ const offset = (text) => {
11
+ const out = [];
12
+ const taken = [];
13
+ for (const m of text.matchAll(HOUR_MIN)) {
14
+ const h = (0, number_js_1.parseKoreanNumber)(m[1]);
15
+ if (h == null)
16
+ continue;
17
+ const sign = m[4] === '전' ? -1 : 1;
18
+ const c = { hourOffset: sign * h, minuteOffset: sign * (m[2] ? 30 : +m[3]) };
19
+ if (m[4] === '이내' || m[4] === '안에')
20
+ c.isEnd = true;
21
+ out.push({ text: m[0], index: m.index, components: c });
22
+ taken.push([m.index, m.index + m[0].length]);
23
+ }
24
+ for (const m of text.matchAll(RE)) {
25
+ if (taken.some(([s, e]) => m.index < e && m.index + m[0].length > s))
26
+ continue;
27
+ const n = (0, number_js_1.parseKoreanNumber)(m[3] ?? m[1]);
28
+ if (n == null)
29
+ continue;
30
+ const sign = m[4] === '전' ? -1 : 1;
31
+ const c = {};
32
+ switch (m[3] ? '일' : m[2]) {
33
+ case '일':
34
+ c.dayOffset = sign * n;
35
+ break;
36
+ case '주':
37
+ case '주일':
38
+ c.dayOffset = sign * n * 7;
39
+ break;
40
+ case '달':
41
+ case '개월':
42
+ c.monthOffset = sign * n;
43
+ break;
44
+ case '년':
45
+ // "2028년 후"는 "2028년 뒤"가 아니라 "2028년 이후"일 가능성이 크다. 100년 넘게는 상대 이동으로 보지 않는다
46
+ if (n > 100)
47
+ continue;
48
+ c.yearOffset = sign * n;
49
+ c.sameDay = true;
50
+ break;
51
+ case '시간':
52
+ c.hourOffset = sign * n;
53
+ break;
54
+ case '분':
55
+ c.minuteOffset = sign * n;
56
+ break;
57
+ }
58
+ if (['이내', '안에', '안으로', '내로'].includes(m[4]))
59
+ c.isEnd = true;
60
+ out.push({ text: m[0], index: m.index, components: c });
61
+ }
62
+ return out;
63
+ };
64
+ exports.offset = offset;
@@ -0,0 +1,5 @@
1
+ import type { Parser, Recurrence } from '../types.js';
2
+ /** 반복 일정. 날짜는 계산하지 않고 규칙만 뽑는다 (첫 발생일은 resolve가 계산) */
3
+ export declare const recurrence: Parser;
4
+ /** RFC 5545 RRULE 문자열. 요일은 월요일부터 */
5
+ export declare function toRRule(r: Omit<Recurrence, 'rrule'>): string;
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recurrence = void 0;
4
+ exports.toRRule = toRRule;
5
+ const number_js_1 = require("../util/number.js");
6
+ const WEEKDAY = { 일: 0, 월: 1, 화: 2, 수: 3, 목: 4, 금: 5, 토: 6 };
7
+ const NTH = { 첫째: 1, 첫: 1, 둘째: 2, 셋째: 3, 넷째: 4, 마지막: -1 };
8
+ const SETS = { 평일: [1, 2, 3, 4, 5], 주중: [1, 2, 3, 4, 5], 주말: [6, 0] };
9
+ // 요일 목록 "월수금", "화, 목", "월요일이랑 수요일", "월~금". 목록이 끝난 뒤에는 다른 한글이 붙으면 안 된다
10
+ // ("매주 일요일 일정"의 "일"을 요일로 잡지 않게). 조사·마다는 허용
11
+ const ITEM = '[월화수목금토일](?:요일|욜)?(?:\\s*~\\s*[월화수목금토일](?:요일|욜)?)?';
12
+ const LIST = `${ITEM}(?:\\s*(?:,|·|/|이랑|랑|하고|과|와)?\\s*${ITEM})*`;
13
+ const LIST_END = '(?:(?![가-힣])|(?=마다|에|엔|은|는|도))';
14
+ // "매달 첫째 주 월요일", "매월 마지막 주 금요일"
15
+ const MONTHLY_NTH = /(?:매달|매월|매\s*달)\s*(첫째|첫|둘째|셋째|넷째|마지막)\s*주\s*([월화수목금토일])(?:요일|욜)/g;
16
+ // "매달 15일", "매월 말일", "매달 말", "매달 초"
17
+ const MONTHLY_DAY = /(?:매달|매월|매\s*달|다달이)\s*(?:(\d{1,2})\s*일|(말일|마지막\s*날|말)|(초))/g;
18
+ // "매년 3월 1일", "해마다 12월 25일"
19
+ const YEARLY = /(?:매년|매해|해마다)\s*(\d{1,2})\s*월\s*(\d{1,2})\s*일/g;
20
+ // "매주 평일", "평일마다", "주말마다", "평일 매일"
21
+ const WEEKDAY_SET = /매주\s*(평일|주중|주말)|(?<![가-힣])(평일|주중|주말)\s*(?:마다|매일)/g;
22
+ // "매주 월요일", "매주 월수금", "격주 금요일", "2주마다 수요일", "2주에 한 번 화요일"
23
+ const WEEKLY = new RegExp(`(매주|매\\s*주|격주|(\\d{1,2}|두|세|네)\\s*주\\s*(?:마다|에\\s*한\\s*번))\\s*(${LIST})${LIST_END}(?:\\s*마다)?`, 'g');
24
+ // "월요일마다", "화요일이랑 목요일마다"
25
+ const DAYS_EVERY = /(?<![가-힣])([월화수목금토일](?:요일|욜)(?:\s*(?:,|·|이랑|랑|하고|과|와)?\s*[월화수목금토일](?:요일|욜))*)\s*마다/g;
26
+ // "매일", "날마다", "매일매일"
27
+ const DAILY = /(?<![가-힣])(?:매일매일|매일|날마다)/g;
28
+ // "2시간마다", "30분마다", "매시간"
29
+ const HOURLY = /(?<![가-힣\d])(?:(\d{1,2}|한|두|세|네|다섯|여섯)\s*시간\s*마다|매\s*시간)/g;
30
+ const MINUTELY = /(?<!\d)(\d{1,3})\s*분\s*마다/g;
31
+ /** "월수금", "화, 목요일", "월~금" → 요일 번호들. "요일" 안의 "일"은 일요일이 아니다 */
32
+ function weekdaysOf(list) {
33
+ const out = [];
34
+ const plain = list.replace(/요일|욜/g, '');
35
+ // "월~금" → 월화수목금 (월요일부터 센다, "금~일"은 금토일)
36
+ for (const m of plain.matchAll(/([월화수목금토일])\s*~\s*([월화수목금토일])|([월화수목금토일])/g)) {
37
+ if (m[3]) {
38
+ out.push(WEEKDAY[m[3]]);
39
+ continue;
40
+ }
41
+ const from = (WEEKDAY[m[1]] + 6) % 7, to = (WEEKDAY[m[2]] + 6) % 7;
42
+ for (let i = from; i !== (to + 1) % 7; i = (i + 1) % 7)
43
+ out.push((i + 1) % 7);
44
+ }
45
+ return [...new Set(out)];
46
+ }
47
+ /** 반복 일정. 날짜는 계산하지 않고 규칙만 뽑는다 (첫 발생일은 resolve가 계산) */
48
+ const recurrence = (text) => {
49
+ const out = [];
50
+ const taken = [];
51
+ const push = (m, recur) => {
52
+ const s = m.index, e = s + m[0].length;
53
+ if (taken.some(([a, b]) => s < b && e > a))
54
+ return;
55
+ taken.push([s, e]);
56
+ out.push({ text: m[0], index: s, components: { recur } });
57
+ };
58
+ for (const m of text.matchAll(MONTHLY_NTH)) {
59
+ push(m, { freq: 'MONTHLY', interval: 1, byWeekday: [WEEKDAY[m[2]]], byWeekdayNth: NTH[m[1]] });
60
+ }
61
+ for (const m of text.matchAll(MONTHLY_DAY)) {
62
+ const day = m[1] ? +m[1] : m[2] ? -1 : 1;
63
+ if (day === 0 || day > 31)
64
+ continue;
65
+ push(m, { freq: 'MONTHLY', interval: 1, byMonthDay: day });
66
+ }
67
+ for (const m of text.matchAll(YEARLY)) {
68
+ const month = +m[1], day = +m[2];
69
+ if (month < 1 || month > 12 || day < 1 || day > 31)
70
+ continue;
71
+ push(m, { freq: 'YEARLY', interval: 1, byMonth: month, byMonthDay: day });
72
+ }
73
+ for (const m of text.matchAll(WEEKDAY_SET)) {
74
+ push(m, { freq: 'WEEKLY', interval: 1, byWeekday: SETS[m[1] ?? m[2]] });
75
+ }
76
+ for (const m of text.matchAll(WEEKLY)) {
77
+ const n = m[1].startsWith('격주') ? 2 : m[2] ? (0, number_js_1.parseKoreanNumber)(m[2]) : 1;
78
+ if (!n)
79
+ continue;
80
+ push(m, { freq: 'WEEKLY', interval: n, byWeekday: weekdaysOf(m[3]) });
81
+ }
82
+ for (const m of text.matchAll(DAYS_EVERY)) {
83
+ push(m, { freq: 'WEEKLY', interval: 1, byWeekday: weekdaysOf(m[1]) });
84
+ }
85
+ for (const m of text.matchAll(DAILY)) {
86
+ push(m, { freq: 'DAILY', interval: 1 });
87
+ }
88
+ for (const m of text.matchAll(HOURLY)) {
89
+ const n = m[1] ? (0, number_js_1.parseKoreanNumber)(m[1]) : 1;
90
+ if (!n)
91
+ continue;
92
+ push(m, { freq: 'HOURLY', interval: n });
93
+ }
94
+ for (const m of text.matchAll(MINUTELY)) {
95
+ if (+m[1] === 0)
96
+ continue;
97
+ push(m, { freq: 'MINUTELY', interval: +m[1] });
98
+ }
99
+ return out;
100
+ };
101
+ exports.recurrence = recurrence;
102
+ const CODE = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
103
+ /** RFC 5545 RRULE 문자열. 요일은 월요일부터 */
104
+ function toRRule(r) {
105
+ const parts = [`FREQ=${r.freq}`];
106
+ if (r.interval > 1)
107
+ parts.push(`INTERVAL=${r.interval}`);
108
+ if (r.byMonth !== undefined)
109
+ parts.push(`BYMONTH=${r.byMonth}`);
110
+ if (r.byMonthDay !== undefined)
111
+ parts.push(`BYMONTHDAY=${r.byMonthDay}`);
112
+ if (r.byWeekday?.length) {
113
+ const days = [...r.byWeekday].sort((a, b) => ((a + 6) % 7) - ((b + 6) % 7)).map((d) => CODE[d]);
114
+ parts.push(`BYDAY=${days.map((c) => (r.byWeekdayNth ? `${r.byWeekdayNth}${c}` : c)).join(',')}`);
115
+ }
116
+ return parts.join(';');
117
+ }
@@ -0,0 +1,3 @@
1
+ import type { Parser } from '../types.js';
2
+ /** 오늘/내일/모레/글피/어제/그저께 */
3
+ export declare const relativeDay: Parser;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.relativeDay = void 0;
4
+ const WORDS = {
5
+ 그저께: -2,
6
+ 내일모레: 2,
7
+ 낼모레: 2,
8
+ 낼: 1,
9
+ 그제: -2,
10
+ 어제: -1,
11
+ 오늘: 0,
12
+ 내일: 1,
13
+ 모레: 2,
14
+ 글피: 3,
15
+ };
16
+ // "내일모레"는 "내일"+"모레"가 아니라 모레. "낼"은 "내가 낼게"(내다)와 겹쳐서 뒤에 시각·약속 표현이 올 때만
17
+ const RE = /(그저께|그제|어제|오늘|내일모레|낼모레|내일|모레|글피|(?<![가-힣])낼(?=\s*(?:오전|오후|아침|점심|저녁|밤|새벽|낮|\d|봐|보자|만나|까지)))/g;
18
+ /** 오늘/내일/모레/글피/어제/그저께 */
19
+ const relativeDay = (text) => {
20
+ const out = [];
21
+ for (const m of text.matchAll(RE)) {
22
+ out.push({ text: m[0], index: m.index, components: { dayOffset: WORDS[m[1]] } });
23
+ }
24
+ return out;
25
+ };
26
+ exports.relativeDay = relativeDay;
@@ -0,0 +1,3 @@
1
+ import type { Parser } from '../types.js';
2
+ /** 이번달/다음달 (초/중순/말) */
3
+ export declare const relativeMonth: Parser;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.relativeMonth = void 0;
4
+ const WORDS = { 지난: -1, 저번: -1, 이번: 0, 다음: 1, 다다음: 2, 담: 1 };
5
+ const PART = { 초: 'start', 중순: 'middle', 말: 'end' };
6
+ // "이번달 말", "다음 달 초", "이번달 중순", "다음달", "다음달 이맘때", "월초", "월말"
7
+ const RE = /(지난|저번|이번|다다음|다음|담)\s*달\s*(초|중순|말|이맘때)?|(?<![가-힣])월(초|말)/g;
8
+ // "11월 말", "12월 초" (absoluteDate의 MONTH_ONLY와 겹치지만 part가 있는 경우 우선)
9
+ const ABS_PART = /(?<![\d가-힣])(\d{1,2})\s*월\s*(초|중순|말)/g;
10
+ /** 이번달/다음달 (초/중순/말) */
11
+ const relativeMonth = (text) => {
12
+ const out = [];
13
+ for (const m of text.matchAll(RE)) {
14
+ if (m[3]) {
15
+ out.push({ text: m[0], index: m.index, components: { monthOffset: 0, monthPart: PART[m[3]] } });
16
+ continue;
17
+ }
18
+ const c = { monthOffset: WORDS[m[1]] };
19
+ if (m[2] === '이맘때')
20
+ c.sameDay = true;
21
+ else if (m[2])
22
+ c.monthPart = PART[m[2]];
23
+ out.push({ text: m[0], index: m.index, components: c });
24
+ }
25
+ for (const m of text.matchAll(ABS_PART)) {
26
+ out.push({ text: m[0], index: m.index, components: { month: +m[1], monthPart: PART[m[2]] } });
27
+ }
28
+ return out;
29
+ };
30
+ exports.relativeMonth = relativeMonth;
@@ -0,0 +1,3 @@
1
+ import type { Parser } from '../types.js';
2
+ /** 올해/내년/작년 (초/말) */
3
+ export declare const relativeYear: Parser;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.relativeYear = void 0;
4
+ const WORDS = {
5
+ 재작년: -2, 작년: -1, 지난해: -1, 올해: 0, 금년: 0, 내년: 1, 명년: 1, 내후년: 2,
6
+ };
7
+ // "올해 말", "내년 초", "작년", "재작년", "내후년", "지난해", "내년 이맘때", "연말", "연초"
8
+ const RE = /(재작년|작년|지난해|올해|금년|내후년|내년|명년)(?:\s*(초|말|이맘때))?|(?<![가-힣])(연말(?!정산)|연초)/g;
9
+ /** 올해/내년/작년 (초/말) */
10
+ const relativeYear = (text) => {
11
+ const out = [];
12
+ for (const m of text.matchAll(RE)) {
13
+ if (m[3]) {
14
+ // "연초"는 연도가 없으므로 지난 월일 → 내년 규칙(13)을 따른다
15
+ const c = m[3] === '연초' ? { month: 1, monthPart: 'start' } : { month: 12, monthPart: 'end' };
16
+ out.push({ text: m[0], index: m.index, components: c });
17
+ continue;
18
+ }
19
+ const c = { yearOffset: WORDS[m[1]] };
20
+ if (m[2] === '초')
21
+ Object.assign(c, { month: 1, monthPart: 'start' });
22
+ if (m[2] === '말')
23
+ Object.assign(c, { month: 12, monthPart: 'end' });
24
+ if (m[2] === '이맘때')
25
+ c.sameDay = true;
26
+ out.push({ text: m[0], index: m.index, components: c });
27
+ }
28
+ return out;
29
+ };
30
+ exports.relativeYear = relativeYear;
@@ -0,0 +1,3 @@
1
+ import type { Parser } from '../types.js';
2
+ /** 시각 표현 */
3
+ export declare const time: Parser;