chronolite-time 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adithya Gowda
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 ADDED
@@ -0,0 +1,211 @@
1
+ # chronolite-time ā±ļø — Natural Language Time Parser
2
+
3
+ [![npm version](https://img.shields.io/npm/v/chronolite-time.svg)](https://www.npmjs.com/package/chronolite-time)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ chronolite-time is a lightweight JavaScript library that converts **human-readable time expressions** into machine-friendly objects and timestamps.
7
+
8
+ It supports:
9
+
10
+ - ā³ **Durations** (`3 hours`, `1.5 days`)
11
+ - šŸ—“ **Relative dates** (`tomorrow`, `in 2 hours`, `next monday`)
12
+ - šŸ“… **Absolute dates** (`2026-03-15`, `Feb 20 2026`, `15/03/2026`)
13
+
14
+ Built for schedulers, reminders, bots, dashboards, and automation systems.
15
+
16
+ ---
17
+
18
+ ## ✨ Features
19
+
20
+ - **Natural language duration parsing**
21
+ - **Relative time calculation** based on current system time
22
+ - **Absolute calendar date parsing**
23
+ - **Strict validation** (blocks invalid dates like `31/02/2026`)
24
+ - **Supports mixed units** (`1 hour and 30 minutes`)
25
+ - **Handles time fragments** (`at 5pm`, `10:30am`)
26
+ - **Returns Unix timestamps and ISO strings**
27
+ - **Zero dependencies**
28
+ - **ES Modules support**
29
+
30
+ ---
31
+
32
+ ## šŸš€ Installation
33
+
34
+ ```bash
35
+ npm install chronolite-time
36
+ ```
37
+
38
+ ---
39
+
40
+ ## šŸ“– Usage
41
+
42
+ ### Basic Example
43
+
44
+ ```javascript
45
+ import { parseTime } from "chronolite-time";
46
+
47
+ // Duration parsing
48
+ const duration = parseTime("3 hours");
49
+ console.log(duration);
50
+
51
+ // Relative time parsing
52
+ const relative = parseTime("tomorrow at 5pm");
53
+ console.log(relative);
54
+
55
+ // Absolute date parsing
56
+ const absolute = parseTime("2026-03-15");
57
+ console.log(absolute);
58
+ ```
59
+
60
+ ### CommonJS
61
+
62
+ If using CommonJS:
63
+
64
+ ```javascript
65
+ const { parseTime } = await import("chronolite-time");
66
+ ```
67
+
68
+ ---
69
+
70
+ ## šŸ“¦ Return Format
71
+
72
+ chronolite-time returns different objects depending on the input type:
73
+
74
+ ### Duration
75
+ ```javascript
76
+ {
77
+ type: "duration",
78
+ milliseconds: number
79
+ }
80
+ ```
81
+
82
+ ### Relative / Absolute
83
+ ```javascript
84
+ {
85
+ type: "relative" | "absolute",
86
+ unix: number,
87
+ iso: string
88
+ }
89
+ ```
90
+
91
+ ### Ranges
92
+ Ranges like `this week` or `next month` return:
93
+ ```javascript
94
+ {
95
+ type: "relative-range",
96
+ start: string,
97
+ end: string
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ## 🧪 Examples
104
+
105
+ ### Duration
106
+ ```javascript
107
+ parseTime("1 hour and 30 minutes");
108
+ ```
109
+ **Output:**
110
+ ```json
111
+ {
112
+ "type": "duration",
113
+ "milliseconds": 5400000
114
+ }
115
+ ```
116
+
117
+ ### Relative
118
+ ```javascript
119
+ parseTime("in 2 hours");
120
+ ```
121
+ **Output (example):**
122
+ ```json
123
+ {
124
+ "type": "relative",
125
+ "unix": 1707678000000,
126
+ "iso": "2026-02-11T19:00:00.000Z"
127
+ }
128
+ ```
129
+
130
+ ### Absolute
131
+ ```javascript
132
+ parseTime("Feb 20 2026");
133
+ ```
134
+ **Output:**
135
+ ```json
136
+ {
137
+ "type": "absolute",
138
+ "unix": 1771545600000,
139
+ "iso": "2026-02-20T00:00:00.000Z"
140
+ }
141
+ ```
142
+
143
+ ---
144
+
145
+ ## 🧠 Supported Formats
146
+
147
+ ### Durations
148
+ - `3h`, `3 hours`
149
+ - `2 hours`
150
+ - `1.5 days`
151
+ - `1 hour and 30 minutes`
152
+
153
+ ### Relative
154
+ - `today`, `tomorrow`, `yesterday`
155
+ - `in 3 hours`
156
+ - `next monday`
157
+ - `this week`
158
+ - `next month`
159
+ - `tomorrow at 5pm`
160
+
161
+ ### Absolute
162
+ - `2026-03-15`
163
+ - `15/03/2026`
164
+ - `Feb 20 2026`
165
+
166
+ > [!NOTE]
167
+ > Invalid dates are strictly rejected (e.g., `31/02/2026` or `2026-02-31`).
168
+
169
+ ---
170
+
171
+ ## šŸ›  Typical Use Cases
172
+
173
+ - **Reminder apps**
174
+ - **Scheduling systems**
175
+ - **Chatbots / AI assistants**
176
+ - **Automation tools**
177
+ - **DevOps scripts**
178
+ - **Analytics dashboards**
179
+
180
+ chronolite-time translates human time into timestamps your software can understand.
181
+
182
+ ---
183
+
184
+ ## šŸ—ŗ Roadmap
185
+
186
+ Planned features:
187
+ - [ ] `ago` support (`3 hours ago`)
188
+ - [ ] `last week` / `last month`
189
+ - [ ] Short weekdays (`mon`, `tue`)
190
+ - [ ] Timezone parsing
191
+ - [ ] Recurring schedules (`every 3 hours`)
192
+
193
+ ---
194
+
195
+ ## šŸ¤ Contributing
196
+
197
+ Contributions are welcome! If you have a feature request or found a bug, please open an issue.
198
+
199
+ 1. Fork the repository
200
+ 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
201
+ 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
202
+ 4. Push to the branch (`git push origin feature/AmazingFeature`)
203
+ 5. Open a Pull Request
204
+
205
+ ---
206
+
207
+ ## āš–ļø License
208
+
209
+ Distributed under the MIT License. See `LICENSE` for more information.
210
+
211
+ Developed by [Adithya Gowda](https://github.com/AdityaGowda)
@@ -0,0 +1,43 @@
1
+ import { buildSafeDate, monthIndex } from "../helper/helper.js";
2
+ import {
3
+ getISODateRegex,
4
+ getSlashDateRegex,
5
+ getWordDateRegex
6
+ } from "../utils/regex.js";
7
+
8
+ export function parseAbsolute(text) {
9
+ if (typeof text !== "string") return null;
10
+
11
+ const input = text.trim();
12
+
13
+ // YYYY-MM-DD
14
+ if (getISODateRegex().test(input)) {
15
+ const [y, m, d] = input.split("-").map(Number);
16
+ return buildSafeDate(y, m - 1, d);
17
+ }
18
+
19
+ // 15/03/2026 or 15-03-2026
20
+ const slashMatch = input.match(getSlashDateRegex());
21
+ if (slashMatch) {
22
+ return buildSafeDate(
23
+ Number(slashMatch[3]),
24
+ Number(slashMatch[2]) - 1,
25
+ Number(slashMatch[1])
26
+ );
27
+ }
28
+
29
+ // Feb 20 2026
30
+ const wordMatch = input.match(getWordDateRegex());
31
+ if (wordMatch) {
32
+ const month = monthIndex(wordMatch[1]);
33
+ if (month === undefined) return null;
34
+
35
+ return buildSafeDate(
36
+ Number(wordMatch[3]),
37
+ month,
38
+ Number(wordMatch[2])
39
+ );
40
+ }
41
+
42
+ return null;
43
+ }
package/constants.js ADDED
@@ -0,0 +1,37 @@
1
+ export const TIME_UNITS = {
2
+ ms: 1,
3
+ millisecond: 1,
4
+ milliseconds: 1,
5
+
6
+ s: 1000,
7
+ sec: 1000,
8
+ second: 1000,
9
+ seconds: 1000,
10
+
11
+ m: 60 * 1000,
12
+ min: 60 * 1000,
13
+ minute: 60 * 1000,
14
+ minutes: 60 * 1000,
15
+
16
+ h: 60 * 60 * 1000,
17
+ hr: 60 * 60 * 1000,
18
+ hour: 60 * 60 * 1000,
19
+ hours: 60 * 60 * 1000,
20
+
21
+ d: 24 * 60 * 60 * 1000,
22
+ day: 24 * 60 * 60 * 1000,
23
+ days: 24 * 60 * 60 * 1000,
24
+
25
+ w: 7 * 24 * 60 * 60 * 1000,
26
+ week: 7 * 24 * 60 * 60 * 1000,
27
+ weeks: 7 * 24 * 60 * 60 * 1000,
28
+
29
+ mon: 30 * 24 * 60 * 60 * 1000,
30
+ month: 30 * 24 * 60 * 60 * 1000,
31
+ months: 30 * 24 * 60 * 60 * 1000,
32
+
33
+ y: 365 * 24 * 60 * 60 * 1000,
34
+ yr: 365 * 24 * 60 * 60 * 1000,
35
+ year: 365 * 24 * 60 * 60 * 1000,
36
+ years: 365 * 24 * 60 * 60 * 1000
37
+ };
@@ -0,0 +1,26 @@
1
+ import { getDurationRegex } from "../utils/regex.js";
2
+ import { TIME_UNITS } from "../utils/constants.js";
3
+
4
+ export function parseDuration(text) {
5
+
6
+ if (typeof text !== "string") return null;
7
+ let totalMilliseconds = 0;
8
+ const matches = text.matchAll(getDurationRegex());
9
+
10
+ for (const match of matches) {
11
+ const value = parseFloat(match[2]);
12
+ const unit = match[3].toLowerCase();
13
+
14
+ const multiplier = TIME_UNITS[unit];
15
+
16
+ if (!multiplier) continue;
17
+
18
+ totalMilliseconds += value * multiplier;
19
+ }
20
+ if (totalMilliseconds === 0) return null;
21
+
22
+ return {
23
+ type: "duration",
24
+ milliseconds: Math.round(totalMilliseconds)
25
+ };
26
+ }
package/helper.js ADDED
@@ -0,0 +1,120 @@
1
+
2
+ export function buildDate(date, timeMatch) {
3
+ const d = new Date(date);
4
+ if (timeMatch) {
5
+ let hour = Number(timeMatch[1]);
6
+ const minute = Number(timeMatch[2] || 0);
7
+ const meridian = timeMatch[3];
8
+
9
+ if (meridian === "pm" && hour < 12) hour += 12;
10
+ if (meridian === "am" && hour === 12) hour = 0;
11
+
12
+ d.setHours(hour, minute, 0, 0);
13
+ }
14
+
15
+ return {
16
+ type: "relative",
17
+ iso: d.toISOString(),
18
+ unix: d.getTime()
19
+ };
20
+ }
21
+
22
+ export function buildSafeDate(year, month, day) {
23
+ if (
24
+ !Number.isInteger(year) ||
25
+ !Number.isInteger(month) ||
26
+ !Number.isInteger(day)
27
+ )
28
+ return null;
29
+
30
+ const d = new Date(year, month, day);
31
+
32
+ // 🚨 reject JS rollover
33
+ if (
34
+ d.getFullYear() !== year ||
35
+ d.getMonth() !== month ||
36
+ d.getDate() !== day
37
+ ) {
38
+ return null;
39
+ }
40
+
41
+ return buildAbsolute(d);
42
+ }
43
+
44
+
45
+ export function buildWeekRange(now, offset) {
46
+ const d = new Date(now);
47
+ d.setDate(d.getDate() + offset * 7);
48
+
49
+ const day = d.getDay();
50
+ const diff = day === 0 ? -6 : 1 - day;
51
+
52
+ const start = new Date(d);
53
+ start.setDate(d.getDate() + diff);
54
+ start.setHours(0, 0, 0, 0);
55
+
56
+ const end = new Date(start);
57
+ end.setDate(start.getDate() + 6);
58
+ end.setHours(23, 59, 59, 999);
59
+
60
+ return {
61
+ type: "relative-range",
62
+ start: start.toISOString(),
63
+ end: end.toISOString()
64
+ };
65
+ }
66
+
67
+ export function buildMonthRange(now, offset) {
68
+ const start = new Date(now.getFullYear(), now.getMonth() + offset, 1);
69
+ const end = new Date(start.getFullYear(), start.getMonth() + 1, 0);
70
+ end.setHours(23, 59, 59, 999);
71
+
72
+ return {
73
+ type: "relative-range",
74
+ start: start.toISOString(),
75
+ end: end.toISOString()
76
+ };
77
+ }
78
+
79
+ export function buildYearRange(now, offset) {
80
+ const year = now.getFullYear() + offset;
81
+
82
+ const start = new Date(year, 0, 1);
83
+ const end = new Date(year, 11, 31);
84
+ end.setHours(23, 59, 59, 999);
85
+
86
+ return {
87
+ type: "relative-range",
88
+ start: start.toISOString(),
89
+ end: end.toISOString()
90
+ };
91
+ }
92
+
93
+ export function buildAbsolute(date) {
94
+ const d = new Date(date);
95
+ d.setHours(0, 0, 0, 0);
96
+
97
+ return {
98
+ type: "absolute",
99
+ iso: d.toISOString(),
100
+ unix: d.getTime()
101
+ };
102
+ }
103
+
104
+ export function monthIndex(m) {
105
+ if (!m) return undefined;
106
+ return {
107
+ jan: 0, january: 0,
108
+ feb: 1, february: 1,
109
+ mar: 2, march: 2,
110
+ apr: 3, april: 3,
111
+ may: 4,
112
+ jun: 5, june: 5,
113
+ jul: 6, july: 6,
114
+ aug: 7, august: 7,
115
+ sep: 8, september: 8,
116
+ oct: 9, october: 9,
117
+ nov: 10, november: 10,
118
+ dec: 11, december: 11
119
+ }[m.toLowerCase()];
120
+ }
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import { parseDuration } from "./parsers/duration.parser.js";
2
+ import { parseRelative } from "./parsers/relative.parser.js";
3
+ import { parseAbsolute } from "./parsers/absolute.parser.js";
4
+
5
+ export function parseTime(text) {
6
+ return parseRelative(text) || parseDuration(text) || parseAbsolute(text);
7
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "chronolite-time",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight natural language time parser for JavaScript",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "keywords": [
8
+ "time",
9
+ "date",
10
+ "parser",
11
+ "nlp",
12
+ "natural-language",
13
+ "datetime",
14
+ "duration",
15
+ "relative-date",
16
+ "scheduler",
17
+ "human-time",
18
+ "time-parser",
19
+ "date-parser",
20
+ "javascript",
21
+ "human-readable",
22
+ "automation"
23
+ ],
24
+ "author": "Adithya M R",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/AdityaGowda/chronolite-time.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/AdityaGowda/chronolite-time/issues"
32
+ },
33
+ "homepage": "https://github.com/AdityaGowda/chronolite-time#readme"
34
+ }
package/regex.js ADDED
@@ -0,0 +1,37 @@
1
+ export function getDurationRegex() {
2
+ return /(^|[^a-zA-Z0-9.])(\d+(?:\.\d+)?)\s*(milliseconds?|ms|seconds?|Seconds?|Sec|sec|s|minutes?|min|Minutes?|Min|m|hours?|hr|Hrs?|Hr|h|days?|day|Days?|Day|d|weeks?|week|Weeks?|Week|w|years?|year|Years?|Year|y)\b/gi;
3
+ }
4
+
5
+
6
+
7
+ // Relative regex
8
+
9
+ export function getTimeRegex() {
10
+ return /\bat\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b/i;
11
+ }
12
+
13
+ export function getInRegex() {
14
+ return /^in\s+(\d+(?:\.\d+)?)\s+(seconds?|minutes?|hours?|days?|weeks?|months?|years?)/i;
15
+ }
16
+
17
+ export function getWeekdayRegex() {
18
+ return /^(next|this)\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i;
19
+ }
20
+
21
+
22
+ // -------- ABSOLUTE DATE REGEX --------
23
+
24
+ // 2026-03-15
25
+ export function getISODateRegex() {
26
+ return /^\d{4}-\d{2}-\d{2}$/;
27
+ }
28
+
29
+ // dd/mm/yyyy or dd-mm-yyyy
30
+ export function getSlashDateRegex() {
31
+ return /^(\d{1,2})[\/-](\d{1,2})[\/-](\d{4})$/;
32
+ }
33
+
34
+ // format: "Month day, year" (e.g., "January 12, 2026" or "Jan 12 2026")
35
+ export function getWordDateRegex() {
36
+ return /^(jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|september|oct|october|nov|november|dec|december)\s+(\d{1,2}),?\s+(\d{4})$/i;
37
+ }
@@ -0,0 +1,93 @@
1
+ import { buildDate, buildMonthRange, buildWeekRange, buildYearRange } from "../helper/helper.js";
2
+ import { getInRegex,getTimeRegex,getWeekdayRegex } from "../utils/regex.js";
3
+
4
+ const WEEKDAYS = {
5
+ sunday: 0,
6
+ monday: 1,
7
+ tuesday: 2,
8
+ wednesday: 3,
9
+ thursday: 4,
10
+ friday: 5,
11
+ saturday: 6
12
+ };
13
+
14
+ const UNIT_SETTERS = {
15
+ second: (d, v) => d.setSeconds(d.getSeconds() + v),
16
+ minute: (d, v) => d.setMinutes(d.getMinutes() + v),
17
+ hour: (d, v) => d.setHours(d.getHours() + v),
18
+ day: (d, v) => d.setDate(d.getDate() + v),
19
+ week: (d, v) => d.setDate(d.getDate() + v * 7),
20
+ month: (d, v) => d.setMonth(d.getMonth() + v),
21
+ year: (d, v) => d.setFullYear(d.getFullYear() + v)
22
+ };
23
+
24
+ export function parseRelative(text) {
25
+ if (typeof text !== "string") return null;
26
+
27
+ const input = text.toLowerCase().trim();
28
+ const now = new Date();
29
+
30
+ const timeMatch = input.match(getTimeRegex());
31
+
32
+ // -------- keywords --------
33
+ const keywordMap = {
34
+ today: 0,
35
+ tomorrow: 1,
36
+ yesterday: -1
37
+ };
38
+
39
+ if (keywordMap[input] !== undefined) {
40
+ const d = new Date(now);
41
+ d.setDate(d.getDate() + keywordMap[input]);
42
+ return buildDate(d, timeMatch);
43
+ }
44
+
45
+ // -------- in X units --------
46
+ const inMatch = input.match(getInRegex());
47
+
48
+ if (inMatch) {
49
+ const value = Number(inMatch[1]);
50
+ const unit = inMatch[2].replace(/s$/, "");
51
+
52
+ if (!Number.isFinite(value)) return null;
53
+ if (!UNIT_SETTERS[unit]) return null;
54
+
55
+ const d = new Date(now);
56
+ UNIT_SETTERS[unit](d, value);
57
+
58
+ return buildDate(d, timeMatch);
59
+ }
60
+
61
+ // -------- ranges --------
62
+
63
+ const ranges = {
64
+ "this week": () => buildWeekRange(now, 0),
65
+ "next week": () => buildWeekRange(now, 1),
66
+ "this month": () => buildMonthRange(now, 0),
67
+ "next month": () => buildMonthRange(now, 1),
68
+ "this year": () => buildYearRange(now, 0),
69
+ "next year": () => buildYearRange(now, 1)
70
+ };
71
+
72
+ if (ranges[input]) return ranges[input]();
73
+
74
+ // -------- weekdays --------
75
+
76
+ const weekdayMatch = input.match(getWeekdayRegex());
77
+
78
+ if (weekdayMatch) {
79
+ const [, modifier, weekday] = weekdayMatch;
80
+
81
+ const target = WEEKDAYS[weekday];
82
+ const d = new Date(now);
83
+
84
+ let diff = target - d.getDay();
85
+ if (modifier === "next" || diff <= 0) diff += 7;
86
+
87
+ d.setDate(d.getDate() + diff);
88
+
89
+ return buildDate(d, timeMatch);
90
+ }
91
+
92
+ return null;
93
+ }
@@ -0,0 +1,120 @@
1
+
2
+ export function buildDate(date, timeMatch) {
3
+ const d = new Date(date);
4
+ if (timeMatch) {
5
+ let hour = Number(timeMatch[1]);
6
+ const minute = Number(timeMatch[2] || 0);
7
+ const meridian = timeMatch[3];
8
+
9
+ if (meridian === "pm" && hour < 12) hour += 12;
10
+ if (meridian === "am" && hour === 12) hour = 0;
11
+
12
+ d.setHours(hour, minute, 0, 0);
13
+ }
14
+
15
+ return {
16
+ type: "relative",
17
+ iso: d.toISOString(),
18
+ unix: d.getTime()
19
+ };
20
+ }
21
+
22
+ export function buildSafeDate(year, month, day) {
23
+ if (
24
+ !Number.isInteger(year) ||
25
+ !Number.isInteger(month) ||
26
+ !Number.isInteger(day)
27
+ )
28
+ return null;
29
+
30
+ const d = new Date(year, month, day);
31
+
32
+ // 🚨 reject JS rollover
33
+ if (
34
+ d.getFullYear() !== year ||
35
+ d.getMonth() !== month ||
36
+ d.getDate() !== day
37
+ ) {
38
+ return null;
39
+ }
40
+
41
+ return buildAbsolute(d);
42
+ }
43
+
44
+
45
+ export function buildWeekRange(now, offset) {
46
+ const d = new Date(now);
47
+ d.setDate(d.getDate() + offset * 7);
48
+
49
+ const day = d.getDay();
50
+ const diff = day === 0 ? -6 : 1 - day;
51
+
52
+ const start = new Date(d);
53
+ start.setDate(d.getDate() + diff);
54
+ start.setHours(0, 0, 0, 0);
55
+
56
+ const end = new Date(start);
57
+ end.setDate(start.getDate() + 6);
58
+ end.setHours(23, 59, 59, 999);
59
+
60
+ return {
61
+ type: "relative-range",
62
+ start: start.toISOString(),
63
+ end: end.toISOString()
64
+ };
65
+ }
66
+
67
+ export function buildMonthRange(now, offset) {
68
+ const start = new Date(now.getFullYear(), now.getMonth() + offset, 1);
69
+ const end = new Date(start.getFullYear(), start.getMonth() + 1, 0);
70
+ end.setHours(23, 59, 59, 999);
71
+
72
+ return {
73
+ type: "relative-range",
74
+ start: start.toISOString(),
75
+ end: end.toISOString()
76
+ };
77
+ }
78
+
79
+ export function buildYearRange(now, offset) {
80
+ const year = now.getFullYear() + offset;
81
+
82
+ const start = new Date(year, 0, 1);
83
+ const end = new Date(year, 11, 31);
84
+ end.setHours(23, 59, 59, 999);
85
+
86
+ return {
87
+ type: "relative-range",
88
+ start: start.toISOString(),
89
+ end: end.toISOString()
90
+ };
91
+ }
92
+
93
+ export function buildAbsolute(date) {
94
+ const d = new Date(date);
95
+ d.setHours(0, 0, 0, 0);
96
+
97
+ return {
98
+ type: "absolute",
99
+ iso: d.toISOString(),
100
+ unix: d.getTime()
101
+ };
102
+ }
103
+
104
+ export function monthIndex(m) {
105
+ if (!m) return undefined;
106
+ return {
107
+ jan: 0, january: 0,
108
+ feb: 1, february: 1,
109
+ mar: 2, march: 2,
110
+ apr: 3, april: 3,
111
+ may: 4,
112
+ jun: 5, june: 5,
113
+ jul: 6, july: 6,
114
+ aug: 7, august: 7,
115
+ sep: 8, september: 8,
116
+ oct: 9, october: 9,
117
+ nov: 10, november: 10,
118
+ dec: 11, december: 11
119
+ }[m.toLowerCase()];
120
+ }
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import { parseDuration } from "./parsers/duration.parser.js";
2
+ import { parseRelative } from "./parsers/relative.parser.js";
3
+ import { parseAbsolute } from "./parsers/absolute.parser.js";
4
+
5
+ export function parseTime(text) {
6
+ return parseRelative(text) || parseDuration(text) || parseAbsolute(text);
7
+ }
@@ -0,0 +1,43 @@
1
+ import { buildSafeDate, monthIndex } from "../helper/helper.js";
2
+ import {
3
+ getISODateRegex,
4
+ getSlashDateRegex,
5
+ getWordDateRegex
6
+ } from "../utils/regex.js";
7
+
8
+ export function parseAbsolute(text) {
9
+ if (typeof text !== "string") return null;
10
+
11
+ const input = text.trim();
12
+
13
+ // YYYY-MM-DD
14
+ if (getISODateRegex().test(input)) {
15
+ const [y, m, d] = input.split("-").map(Number);
16
+ return buildSafeDate(y, m - 1, d);
17
+ }
18
+
19
+ // 15/03/2026 or 15-03-2026
20
+ const slashMatch = input.match(getSlashDateRegex());
21
+ if (slashMatch) {
22
+ return buildSafeDate(
23
+ Number(slashMatch[3]),
24
+ Number(slashMatch[2]) - 1,
25
+ Number(slashMatch[1])
26
+ );
27
+ }
28
+
29
+ // Feb 20 2026
30
+ const wordMatch = input.match(getWordDateRegex());
31
+ if (wordMatch) {
32
+ const month = monthIndex(wordMatch[1]);
33
+ if (month === undefined) return null;
34
+
35
+ return buildSafeDate(
36
+ Number(wordMatch[3]),
37
+ month,
38
+ Number(wordMatch[2])
39
+ );
40
+ }
41
+
42
+ return null;
43
+ }
@@ -0,0 +1,26 @@
1
+ import { getDurationRegex } from "../utils/regex.js";
2
+ import { TIME_UNITS } from "../utils/constants.js";
3
+
4
+ export function parseDuration(text) {
5
+
6
+ if (typeof text !== "string") return null;
7
+ let totalMilliseconds = 0;
8
+ const matches = text.matchAll(getDurationRegex());
9
+
10
+ for (const match of matches) {
11
+ const value = parseFloat(match[2]);
12
+ const unit = match[3].toLowerCase();
13
+
14
+ const multiplier = TIME_UNITS[unit];
15
+
16
+ if (!multiplier) continue;
17
+
18
+ totalMilliseconds += value * multiplier;
19
+ }
20
+ if (totalMilliseconds === 0) return null;
21
+
22
+ return {
23
+ type: "duration",
24
+ milliseconds: Math.round(totalMilliseconds)
25
+ };
26
+ }
@@ -0,0 +1,93 @@
1
+ import { buildDate, buildMonthRange, buildWeekRange, buildYearRange } from "../helper/helper.js";
2
+ import { getInRegex,getTimeRegex,getWeekdayRegex } from "../utils/regex.js";
3
+
4
+ const WEEKDAYS = {
5
+ sunday: 0,
6
+ monday: 1,
7
+ tuesday: 2,
8
+ wednesday: 3,
9
+ thursday: 4,
10
+ friday: 5,
11
+ saturday: 6
12
+ };
13
+
14
+ const UNIT_SETTERS = {
15
+ second: (d, v) => d.setSeconds(d.getSeconds() + v),
16
+ minute: (d, v) => d.setMinutes(d.getMinutes() + v),
17
+ hour: (d, v) => d.setHours(d.getHours() + v),
18
+ day: (d, v) => d.setDate(d.getDate() + v),
19
+ week: (d, v) => d.setDate(d.getDate() + v * 7),
20
+ month: (d, v) => d.setMonth(d.getMonth() + v),
21
+ year: (d, v) => d.setFullYear(d.getFullYear() + v)
22
+ };
23
+
24
+ export function parseRelative(text) {
25
+ if (typeof text !== "string") return null;
26
+
27
+ const input = text.toLowerCase().trim();
28
+ const now = new Date();
29
+
30
+ const timeMatch = input.match(getTimeRegex());
31
+
32
+ // -------- keywords --------
33
+ const keywordMap = {
34
+ today: 0,
35
+ tomorrow: 1,
36
+ yesterday: -1
37
+ };
38
+
39
+ if (keywordMap[input] !== undefined) {
40
+ const d = new Date(now);
41
+ d.setDate(d.getDate() + keywordMap[input]);
42
+ return buildDate(d, timeMatch);
43
+ }
44
+
45
+ // -------- in X units --------
46
+ const inMatch = input.match(getInRegex());
47
+
48
+ if (inMatch) {
49
+ const value = Number(inMatch[1]);
50
+ const unit = inMatch[2].replace(/s$/, "");
51
+
52
+ if (!Number.isFinite(value)) return null;
53
+ if (!UNIT_SETTERS[unit]) return null;
54
+
55
+ const d = new Date(now);
56
+ UNIT_SETTERS[unit](d, value);
57
+
58
+ return buildDate(d, timeMatch);
59
+ }
60
+
61
+ // -------- ranges --------
62
+
63
+ const ranges = {
64
+ "this week": () => buildWeekRange(now, 0),
65
+ "next week": () => buildWeekRange(now, 1),
66
+ "this month": () => buildMonthRange(now, 0),
67
+ "next month": () => buildMonthRange(now, 1),
68
+ "this year": () => buildYearRange(now, 0),
69
+ "next year": () => buildYearRange(now, 1)
70
+ };
71
+
72
+ if (ranges[input]) return ranges[input]();
73
+
74
+ // -------- weekdays --------
75
+
76
+ const weekdayMatch = input.match(getWeekdayRegex());
77
+
78
+ if (weekdayMatch) {
79
+ const [, modifier, weekday] = weekdayMatch;
80
+
81
+ const target = WEEKDAYS[weekday];
82
+ const d = new Date(now);
83
+
84
+ let diff = target - d.getDay();
85
+ if (modifier === "next" || diff <= 0) diff += 7;
86
+
87
+ d.setDate(d.getDate() + diff);
88
+
89
+ return buildDate(d, timeMatch);
90
+ }
91
+
92
+ return null;
93
+ }
@@ -0,0 +1,37 @@
1
+ export const TIME_UNITS = {
2
+ ms: 1,
3
+ millisecond: 1,
4
+ milliseconds: 1,
5
+
6
+ s: 1000,
7
+ sec: 1000,
8
+ second: 1000,
9
+ seconds: 1000,
10
+
11
+ m: 60 * 1000,
12
+ min: 60 * 1000,
13
+ minute: 60 * 1000,
14
+ minutes: 60 * 1000,
15
+
16
+ h: 60 * 60 * 1000,
17
+ hr: 60 * 60 * 1000,
18
+ hour: 60 * 60 * 1000,
19
+ hours: 60 * 60 * 1000,
20
+
21
+ d: 24 * 60 * 60 * 1000,
22
+ day: 24 * 60 * 60 * 1000,
23
+ days: 24 * 60 * 60 * 1000,
24
+
25
+ w: 7 * 24 * 60 * 60 * 1000,
26
+ week: 7 * 24 * 60 * 60 * 1000,
27
+ weeks: 7 * 24 * 60 * 60 * 1000,
28
+
29
+ mon: 30 * 24 * 60 * 60 * 1000,
30
+ month: 30 * 24 * 60 * 60 * 1000,
31
+ months: 30 * 24 * 60 * 60 * 1000,
32
+
33
+ y: 365 * 24 * 60 * 60 * 1000,
34
+ yr: 365 * 24 * 60 * 60 * 1000,
35
+ year: 365 * 24 * 60 * 60 * 1000,
36
+ years: 365 * 24 * 60 * 60 * 1000
37
+ };
@@ -0,0 +1,37 @@
1
+ export function getDurationRegex() {
2
+ return /(^|[^a-zA-Z0-9.])(\d+(?:\.\d+)?)\s*(milliseconds?|ms|seconds?|Seconds?|Sec|sec|s|minutes?|min|Minutes?|Min|m|hours?|hr|Hrs?|Hr|h|days?|day|Days?|Day|d|weeks?|week|Weeks?|Week|w|years?|year|Years?|Year|y)\b/gi;
3
+ }
4
+
5
+
6
+
7
+ // Relative regex
8
+
9
+ export function getTimeRegex() {
10
+ return /\bat\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b/i;
11
+ }
12
+
13
+ export function getInRegex() {
14
+ return /^in\s+(\d+(?:\.\d+)?)\s+(seconds?|minutes?|hours?|days?|weeks?|months?|years?)/i;
15
+ }
16
+
17
+ export function getWeekdayRegex() {
18
+ return /^(next|this)\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i;
19
+ }
20
+
21
+
22
+ // -------- ABSOLUTE DATE REGEX --------
23
+
24
+ // 2026-03-15
25
+ export function getISODateRegex() {
26
+ return /^\d{4}-\d{2}-\d{2}$/;
27
+ }
28
+
29
+ // dd/mm/yyyy or dd-mm-yyyy
30
+ export function getSlashDateRegex() {
31
+ return /^(\d{1,2})[\/-](\d{1,2})[\/-](\d{4})$/;
32
+ }
33
+
34
+ // format: "Month day, year" (e.g., "January 12, 2026" or "Jan 12 2026")
35
+ export function getWordDateRegex() {
36
+ return /^(jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|september|oct|october|nov|november|dec|december)\s+(\d{1,2}),?\s+(\d{4})$/i;
37
+ }
package/test.js ADDED
@@ -0,0 +1,69 @@
1
+ import { parseTime } from "./src/index.js";
2
+ import { generateTestCases } from "./tests/use-cases.test.js";
3
+
4
+ const cases = generateTestCases();
5
+
6
+ console.log("Total cases:", cases.length);
7
+
8
+ // ---------------- Counters ----------------
9
+
10
+ let durationCount = 0;
11
+ let relativeCount = 0;
12
+ let absoluteCount = 0;
13
+ let nullCount = 0;
14
+ let errorCount = 0;
15
+
16
+ // ---------------- Performance ----------------
17
+
18
+ console.time("chronolite-time Parse");
19
+
20
+ // ---------------- Run Tests ----------------
21
+
22
+ cases.forEach((input) => {
23
+ try {
24
+ const result = parseTime(input);
25
+
26
+ if (!result) {
27
+ nullCount++;
28
+ return;
29
+ }
30
+
31
+ if (result.type === "duration") durationCount++;
32
+ else if (result.type === "absolute") absoluteCount++;
33
+ else if (
34
+ result.type === "relative" ||
35
+ result.type === "relative-range"
36
+ )
37
+ relativeCount++;
38
+ else {
39
+ console.error("āŒ Unknown result type:", input, result);
40
+ errorCount++;
41
+ }
42
+ } catch (e) {
43
+ console.error("šŸ’„ Exception for:", input, e.message);
44
+ errorCount++;
45
+ }
46
+ });
47
+
48
+ console.timeEnd("chronolite-time Parse");
49
+
50
+ // ---------------- Summary ----------------
51
+
52
+ console.log("\n===== SUMMARY =====");
53
+ console.log("Duration:", durationCount);
54
+ console.log("Relative:", relativeCount);
55
+ console.log("Absolute:", absoluteCount);
56
+ console.log("Null:", nullCount);
57
+ console.log("Errors:", errorCount);
58
+
59
+ // ---------------- Basic Quality Check ----------------
60
+
61
+ if (errorCount === 0) {
62
+ console.log("\nāœ… No runtime errors.");
63
+ }
64
+
65
+ if (nullCount > cases.length * 0.6) {
66
+ console.warn("\nāš ļø High null rate — many formats unsupported.");
67
+ }
68
+
69
+ console.log("\nDone.");
@@ -0,0 +1,137 @@
1
+ export function generateTestCases() {
2
+ const cases = [];
3
+
4
+ // ---------------- DURATION ----------------
5
+
6
+ const nums = [0, 0.1, 0.5, 1, 1.5, 2, 3, 5, 7, 10, 15, 20, 25, 30, 45, 50, 100];
7
+ const units = [
8
+ "second", "seconds", "sec", "secs",
9
+ "minute", "minutes", "min", "mins",
10
+ "hour", "hours", "hr", "hrs",
11
+ "day", "days", "d",
12
+ "week", "weeks", "w",
13
+ "month", "months", "mo",
14
+ "year", "years", "y"
15
+ ];
16
+
17
+ nums.forEach(n => {
18
+ units.forEach(u => {
19
+ cases.push(`${n} ${u}`);
20
+ cases.push(`${n}${u[0]}`);
21
+ cases.push(`${n} ${u} and ${n} ${u}`);
22
+ cases.push(`${n} ${u}, ${n} ${u}`);
23
+ });
24
+ });
25
+
26
+ // Mixed durations
27
+ for (let i = 1; i <= 200; i++) {
28
+ cases.push(`${i} hours ${i} minutes`);
29
+ cases.push(`${i}h ${i}m`);
30
+ cases.push(`${i} days ${i} hours`);
31
+ cases.push(`${i} weeks ${i} days`);
32
+ }
33
+
34
+ // ---------------- RELATIVE ----------------
35
+
36
+ const relatives = [
37
+ "today",
38
+ "tomorrow",
39
+ "yesterday",
40
+ "this week",
41
+ "next week",
42
+ "last week",
43
+ "this month",
44
+ "next month",
45
+ "last month",
46
+ "this year",
47
+ "next year",
48
+ "last year",
49
+ "now"
50
+ ];
51
+
52
+ relatives.forEach(r => cases.push(r));
53
+
54
+ for (let i = 1; i <= 300; i++) {
55
+ cases.push(`in ${i} seconds`);
56
+ cases.push(`in ${i} minutes`);
57
+ cases.push(`in ${i} hours`);
58
+ cases.push(`in ${i} days`);
59
+ cases.push(`in ${i} weeks`);
60
+ cases.push(`${i} hours ago`);
61
+ cases.push(`${i} days ago`);
62
+ }
63
+
64
+ const weekdays = [
65
+ "monday","tuesday","wednesday",
66
+ "thursday","friday","saturday","sunday",
67
+ "mon", "tue", "wed", "thu", "fri", "sat", "sun"
68
+ ];
69
+
70
+ weekdays.forEach(d => {
71
+ cases.push(`next ${d}`);
72
+ cases.push(`this ${d}`);
73
+ cases.push(`last ${d}`);
74
+ cases.push(`next ${d} at 5pm`);
75
+ cases.push(`this ${d} at 10:30am`);
76
+ cases.push(`${d} 5pm`);
77
+ });
78
+
79
+ // ---------------- ABSOLUTE ----------------
80
+
81
+ for (let y = 2000; y <= 2050; y += 2) {
82
+ for (let m = 1; m <= 12; m++) {
83
+ for (let d = 1; d <= 10; d++) {
84
+ cases.push(`${y}-${String(m).padStart(2,"0")}-${String(d).padStart(2,"0")}`);
85
+ cases.push(`${d}/${m}/${y}`);
86
+ cases.push(`${m}-${d}-${y}`);
87
+ }
88
+ }
89
+ }
90
+
91
+ const months = [
92
+ "Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec",
93
+ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
94
+ ];
95
+
96
+ months.forEach(m => {
97
+ for (let d = 1; d <= 20; d++) {
98
+ cases.push(`${m} ${d} 2026`);
99
+ cases.push(`${d} ${m} 2026`);
100
+ cases.push(`${m} ${d}`);
101
+ }
102
+ });
103
+
104
+ // Time variations
105
+ for (let h = 0; h < 24; h++) {
106
+ cases.push(`${h}:00`);
107
+ cases.push(`${h}:30`);
108
+ if (h > 0 && h <= 12) {
109
+ cases.push(`${h}am`);
110
+ cases.push(`${h}pm`);
111
+ }
112
+ }
113
+
114
+ // ---------------- INVALID ----------------
115
+
116
+ const invalids = [
117
+ "",
118
+ " ",
119
+ "abc",
120
+ "1e3h",
121
+ "31/02/2026",
122
+ "2026-02-31",
123
+ "15/13/2026",
124
+ "foo bar",
125
+ "-5 hours",
126
+ "1..5h",
127
+ "@@@",
128
+ "2026/13/45",
129
+ "yesterday tomorrow",
130
+ "infinity hours"
131
+ ];
132
+
133
+ invalids.forEach(i => cases.push(i));
134
+
135
+ return cases;
136
+ }
137
+