@miso.ai/server-commons 0.6.5-beta.15 → 0.6.5-beta.16

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/package.json CHANGED
@@ -23,5 +23,5 @@
23
23
  "uuid": "^9.0.0",
24
24
  "yargs": "^17.5.1"
25
25
  },
26
- "version": "0.6.5-beta.15"
26
+ "version": "0.6.5-beta.16"
27
27
  }
package/src/date.js CHANGED
@@ -36,57 +36,142 @@ const RE_DATE_EXPR = /^(?:\d{4})|(?:\d{4}-[Qq\d]\d)|(?:\d{4}-\d{2}-[Ww\d]\d)$/;
36
36
  // TODO: support hour, minute
37
37
 
38
38
  export function startOfDate(expr) {
39
+ return floorDate(expr);
40
+ }
41
+
42
+ export function endOfDate(expr) {
39
43
  if (expr === undefined) {
40
44
  return undefined;
41
45
  }
42
- const parsed = parseDateExpr(expr);
43
- switch (parsed.type) {
44
- case 'ts':
45
- return parsed.ts;
46
- case 'year':
47
- return Date.UTC(parsed.year);
48
- case 'quarter':
49
- return Date.UTC(parsed.year, (parsed.quarter - 1) * 3);
50
- case 'month':
51
- return Date.UTC(parsed.year, parsed.month - 1);
52
- case 'week':
53
- return Date.UTC(parsed.year, parsed.month - 1, (parsed.week - 1) * 7 + 1);
46
+ return nextDate(expr) - 1000; // 1 sec
47
+ }
48
+
49
+ export function floorDate(expr, unit) {
50
+ validateUnit(unit);
51
+ if (expr === undefined) {
52
+ return undefined;
53
+ }
54
+ const [_unit, ts] = parseDateExpr(expr);
55
+ if (unit === undefined || !isGranular(unit, _unit)) {
56
+ return ts;
57
+ }
58
+ return floorTimestamp(ts, unit);
59
+ }
60
+
61
+ function floorTimestamp(ts, unit) {
62
+ switch (unit) {
63
+ case 'millisecond':
64
+ return ts;
65
+ case 'second':
66
+ return ts - ts % TS_PER_UNIT.s;
67
+ case 'minute':
68
+ return ts - ts % TS_PER_UNIT.m;
69
+ case 'hour':
70
+ return ts - ts % TS_PER_UNIT.h;
54
71
  case 'day':
55
- return Date.UTC(parsed.year, parsed.month - 1, parsed.day);
72
+ return ts - ts % TS_PER_UNIT.d;
73
+ }
74
+ const d = new Date(ts);
75
+ const year = d.getUTCFullYear();
76
+ const month = d.getUTCMonth() + 1;
77
+ const day = d.getUTCDate();
78
+ switch (unit) {
79
+ case 'week':
80
+ return Date.UTC(year, month - 1, day > 21 ? 22 : day - (day - 1) % 7);
81
+ case 'month':
82
+ return Date.UTC(year, month - 1);
83
+ case 'quarter':
84
+ return Date.UTC(year, month - month % 3);
85
+ case 'year':
86
+ return Date.UTC(year);
56
87
  default:
57
- throw new Error(`Unrecognized date: ${expr}`);
88
+ throw new Error(`Unrecognized unit: ${unit}`);
58
89
  }
59
90
  }
60
91
 
61
- export function nextOfDate(expr) {
92
+ export function ceilDate(expr, unit) {
93
+ validateUnit(unit);
62
94
  if (expr === undefined) {
63
95
  return undefined;
64
96
  }
65
- const parsed = parseDateExpr(expr);
66
- switch (parsed.type) {
67
- case 'ts':
68
- return parsed.ts + 1;
69
- case 'year':
70
- return Date.UTC(parsed.year + 1);
71
- case 'quarter':
72
- return Date.UTC(parsed.year, parsed.quarter * 3);
73
- case 'month':
74
- return Date.UTC(parsed.year, parsed.month);
75
- case 'week':
76
- // roll over to 1st of next month if week is 4
77
- return Date.UTC(parsed.year, parsed.week < 4 ? parsed.month - 1 : parsed.month, (parsed.week % 4) * 7 + 1);
97
+ const [_unit, ts] = parseDateExpr(expr);
98
+ return ceilTimestamp(ts, unit || _unit);
99
+ }
100
+
101
+ function ceilTimestamp(ts, unit) {
102
+ switch (unit) {
103
+ case 'millisecond':
104
+ return ts;
105
+ case 'second':
106
+ return ts + TS_PER_UNIT.s - ts % TS_PER_UNIT.s;
107
+ case 'minute':
108
+ return ts + TS_PER_UNIT.m - ts % TS_PER_UNIT.m;
109
+ case 'hour':
110
+ return ts + TS_PER_UNIT.h - ts % TS_PER_UNIT.h;
78
111
  case 'day':
79
- return Date.UTC(parsed.year, parsed.month - 1, parsed.day + 1);
112
+ return ts + TS_PER_UNIT.d - ts % TS_PER_UNIT.d;
113
+ }
114
+ const d = new Date(ts);
115
+ const year = d.getUTCFullYear();
116
+ const month = d.getUTCMonth() + 1;
117
+ const day = d.getUTCDate();
118
+ switch (unit) {
119
+ case 'week':
120
+ return day > 21 ? Date.UTC(year, month, 1) : Date.UTC(year, month - 1, day - (day - 1) % 7 + 7);
121
+ case 'month':
122
+ return Date.UTC(year, month, 1);
123
+ case 'quarter':
124
+ return Date.UTC(year, month - month % 3 + 3);
125
+ case 'year':
126
+ return Date.UTC(year + 1);
80
127
  default:
81
- throw new Error(`Unrecognized date: ${expr}`);
128
+ throw new Error(`Unrecognized unit: ${unit}`);
82
129
  }
83
130
  }
84
131
 
85
- export function endOfDate(expr) {
132
+ export function nextDate(expr, unit) {
133
+ validateUnit(unit);
134
+ if (expr === undefined) {
135
+ return undefined;
136
+ }
137
+ const [_unit, ts] = parseDateExpr(expr);
138
+ return ceilTimestamp(ts + 1, unit || _unit);
139
+ }
140
+
141
+ export function prevDate(expr, unit) {
142
+ validateUnit(unit);
86
143
  if (expr === undefined) {
87
144
  return undefined;
88
145
  }
89
- return nextOfDate(expr) - 1000; // 1 sec
146
+ const [_unit, ts] = parseDateExpr(expr);
147
+ return floorTimestamp(ts - 1, unit || _unit);
148
+ }
149
+
150
+
151
+
152
+ const UNIT_GRANULARITY = {
153
+ 'millisecond': 10,
154
+ 'second': 9,
155
+ 'minute': 8,
156
+ 'hour': 7,
157
+ 'day': 6,
158
+ 'week': 5,
159
+ 'month': 4,
160
+ 'quarter': 3,
161
+ 'year': 2,
162
+ };
163
+
164
+ function validateUnit(unit) {
165
+ if (unit === undefined) {
166
+ return;
167
+ }
168
+ if (typeof unit !== 'string' || !UNIT_GRANULARITY[unit]) {
169
+ throw new Error(`Unrecognized unit: ${unit}`);
170
+ }
171
+ }
172
+
173
+ function isGranular(unit0, unit1) {
174
+ return UNIT_GRANULARITY[unit0] > UNIT_GRANULARITY[unit1];
90
175
  }
91
176
 
92
177
  function parseDateExpr(expr) {
@@ -95,7 +180,7 @@ function parseDateExpr(expr) {
95
180
  if (expr < 100) {
96
181
  throw new Error(`Unrecognized date: ${expr}`);
97
182
  }
98
- return expr < 10000 ? { type: 'year', year: expr } : { type: 'ts', ts: expr };
183
+ return expr < 10000 ? ['year', Date.UTC(expr)] : ['millisecond', expr];
99
184
  case 'string':
100
185
  expr = expr.trim();
101
186
  const len = expr.length;
@@ -107,28 +192,28 @@ function parseDateExpr(expr) {
107
192
  throw new Error(`Unrecognized date: ${expr}`);
108
193
  }
109
194
  if (len === 4) {
110
- return { type: 'year', year };
195
+ return ['year', Date.UTC(year)];
111
196
  }
112
197
  if (len === 7 && (expr.charAt(5) === 'Q' || expr.charAt(5) === 'q')) {
113
198
  const quarter = parseInt(expr.charAt(6), 10);
114
199
  if (isNaN(quarter) || quarter < 1 || quarter > 4) {
115
200
  throw new Error(`Unrecognized date: ${expr}`);
116
201
  }
117
- return { type: 'quarter', year, quarter };
202
+ return ['quarter', Date.UTC(year, (quarter - 1) * 3)];
118
203
  }
119
204
  const month = parseInt(expr.slice(5, 7), 10);
120
205
  if (isNaN(month) || month < 1 || month > 12) {
121
206
  throw new Error(`Unrecognized date: ${expr}`);
122
207
  }
123
208
  if (len === 7) {
124
- return { type: 'month', year, month };
209
+ return ['month', Date.UTC(year, month - 1)];
125
210
  }
126
211
  if (expr.charAt(8) === 'W' || expr.charAt(8) === 'w') {
127
212
  const week = parseInt(expr.charAt(9), 10);
128
213
  if (isNaN(week) || week < 1 || week > 4) {
129
214
  throw new Error(`Unrecognized date: ${expr}`);
130
215
  }
131
- return { type: 'week', year, month, week };
216
+ return ['week', Date.UTC(year, month - 1, week * 7 - 6)];
132
217
  }
133
218
  let date;
134
219
  try {
@@ -137,7 +222,7 @@ function parseDateExpr(expr) {
137
222
  } catch (_) {
138
223
  throw new Error(`Unrecognized date: ${expr}`);
139
224
  }
140
- return { type: 'day', year, month, day: date.getUTCDate() };
225
+ return ['day', Date.UTC(year, month - 1, date.getUTCDate())];
141
226
  default:
142
227
  throw new Error(`Unrecognized date: ${expr}`);
143
228
  }
package/test/date.test.js CHANGED
@@ -2,30 +2,34 @@ import { test } from 'uvu';
2
2
  import * as assert from 'uvu/assert';
3
3
  import { startOfDate, endOfDate } from '../src/index.js';
4
4
 
5
+ function testDateFn(fn, input, output) {
6
+ assert.equal(new Date(fn(input)).toISOString(), output, `${fn.name}(${input})`);
7
+ }
8
+
5
9
  test('startOfDate', () => {
6
- assert.equal(startOfDate('2025'), Date.parse('2025-01-01T00:00:00Z'));
7
- assert.equal(startOfDate('2025-Q1'), Date.parse('2025-01-01T00:00:00Z'));
8
- assert.equal(startOfDate('2025-q2'), Date.parse('2025-04-01T00:00:00Z'));
9
- assert.equal(startOfDate('2025-Q3'), Date.parse('2025-07-01T00:00:00Z'));
10
- assert.equal(startOfDate('2025-q4'), Date.parse('2025-10-01T00:00:00Z'));
11
- assert.equal(startOfDate('2025-02'), Date.parse('2025-02-01T00:00:00Z'));
12
- assert.equal(startOfDate('2025-02-W1'), Date.parse('2025-02-01T00:00:00Z'));
13
- assert.equal(startOfDate('2025-02-w2'), Date.parse('2025-02-08T00:00:00Z'));
14
- assert.equal(startOfDate('2025-02-03'), Date.parse('2025-02-03T00:00:00Z'));
10
+ testDateFn(startOfDate, '2025', '2025-01-01T00:00:00.000Z');
11
+ testDateFn(startOfDate, '2025-Q1', '2025-01-01T00:00:00.000Z');
12
+ testDateFn(startOfDate, '2025-q2', '2025-04-01T00:00:00.000Z');
13
+ testDateFn(startOfDate, '2025-Q3', '2025-07-01T00:00:00.000Z');
14
+ testDateFn(startOfDate, '2025-q4', '2025-10-01T00:00:00.000Z');
15
+ testDateFn(startOfDate, '2025-02', '2025-02-01T00:00:00.000Z');
16
+ testDateFn(startOfDate, '2025-02-W1', '2025-02-01T00:00:00.000Z');
17
+ testDateFn(startOfDate, '2025-02-w2', '2025-02-08T00:00:00.000Z');
18
+ testDateFn(startOfDate, '2025-02-03', '2025-02-03T00:00:00.000Z');
15
19
  });
16
20
 
17
21
  test('endOfDate', () => {
18
- assert.equal(endOfDate('2025'), Date.parse('2025-12-31T23:59:59Z'));
19
- assert.equal(endOfDate('2025-Q1'), Date.parse('2025-03-31T23:59:59Z'));
20
- assert.equal(endOfDate('2025-q2'), Date.parse('2025-06-30T23:59:59Z'));
21
- assert.equal(endOfDate('2025-Q3'), Date.parse('2025-09-30T23:59:59Z'));
22
- assert.equal(endOfDate('2025-q4'), Date.parse('2025-12-31T23:59:59Z'));
23
- assert.equal(endOfDate('2025-02'), Date.parse('2025-02-28T23:59:59Z'));
24
- assert.equal(endOfDate('2025-03-W1'), Date.parse('2025-03-07T23:59:59Z'));
25
- assert.equal(endOfDate('2025-03-w2'), Date.parse('2025-03-14T23:59:59Z'));
26
- assert.equal(endOfDate('2025-03-W3'), Date.parse('2025-03-21T23:59:59Z'));
27
- assert.equal(endOfDate('2025-03-w4'), Date.parse('2025-03-31T23:59:59Z'));
28
- assert.equal(endOfDate('2025-03-03'), Date.parse('2025-03-03T23:59:59Z'));
22
+ testDateFn(endOfDate, '2025', '2025-12-31T23:59:59.000Z');
23
+ testDateFn(endOfDate, '2025-Q1', '2025-03-31T23:59:59.000Z');
24
+ testDateFn(endOfDate, '2025-q2', '2025-06-30T23:59:59.000Z');
25
+ testDateFn(endOfDate, '2025-Q3', '2025-09-30T23:59:59.000Z');
26
+ testDateFn(endOfDate, '2025-q4', '2025-12-31T23:59:59.000Z');
27
+ testDateFn(endOfDate, '2025-02', '2025-02-28T23:59:59.000Z');
28
+ testDateFn(endOfDate, '2025-03-W1', '2025-03-07T23:59:59.000Z');
29
+ testDateFn(endOfDate, '2025-03-w2', '2025-03-14T23:59:59.000Z');
30
+ testDateFn(endOfDate, '2025-03-W3', '2025-03-21T23:59:59.000Z');
31
+ testDateFn(endOfDate, '2025-03-w4', '2025-03-31T23:59:59.000Z');
32
+ testDateFn(endOfDate, '2025-03-03', '2025-03-03T23:59:59.000Z');
29
33
  });
30
34
 
31
35
  test.run();