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 +21 -0
- package/README.md +211 -0
- package/absolute.parser.js +43 -0
- package/constants.js +37 -0
- package/duration.parser.js +26 -0
- package/helper.js +120 -0
- package/index.js +7 -0
- package/package.json +34 -0
- package/regex.js +37 -0
- package/relative.parser.js +93 -0
- package/src/helper/helper.js +120 -0
- package/src/index.js +7 -0
- package/src/parsers/absolute.parser.js +43 -0
- package/src/parsers/duration.parser.js +26 -0
- package/src/parsers/relative.parser.js +93 -0
- package/src/utils/constants.js +37 -0
- package/src/utils/regex.js +37 -0
- package/test.js +69 -0
- package/tests/use-cases.test.js +137 -0
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
|
+
[](https://www.npmjs.com/package/chronolite-time)
|
|
4
|
+
[](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
|
+
|