Skip to content

Commit 3115b69

Browse files
committed
new date helpers and validators
1 parent 9936e81 commit 3115b69

14 files changed

+483
-8
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# deverything
22

3+
## 4.1.0
4+
5+
### Minor Changes
6+
7+
- new date helpers and validators:
8+
- `groupByDateBucket()` - group items into date buckets based on their dates and specified time intervals
9+
- `getDateSeriesRange()` - get the smallest and biggest dates from an array of dates
10+
- `isBetweenDates()` - check if date falls between two other dates (left inclusive)
11+
- Enhanced `isFutureDate()` and `isPastDate()` with optional `referenceDate` parameter
12+
- Deprecated `bucketSortedDates()` in favor of `groupByDateBucket()`
13+
314
## 4.0.0
415

516
### Major Changes

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ Contributions always welcome!
3030
- `isFile()` if it's a file
3131
- `isFunction()`
3232
- `isJsDate()` if it's a **valid** javascript's Date
33-
- `isFutureDate()`
34-
- `isPastDate()`
33+
- `isBetweenDates()` check if date falls between two other dates (left inclusive: includes start, excludes end)
34+
- `isFutureDate()` check if date is in the future, optionally against a reference date
35+
- `isPastDate()` check if date is in the past, optionally against a reference date
3536
- `isStringDate()` also checks if the string passed is a **valid** date
3637
- `isKey()` is a real key of an object
3738
- `isLastIndex()` is the index is the last item of array
@@ -61,7 +62,9 @@ Contributions always welcome!
6162
### Dates
6263

6364
- `getDateRangeSeries()` generate a series of dates within a range
64-
- `isOver18()`
65+
- `getDateSeriesRange()` get the smallest and biggest dates from an array of dates
66+
- `groupByDateBucket()` group items into date buckets based on their dates and specified time intervals
67+
- `isOver18()` check if someone is over 18 years old
6568
- `startOfDay()` get the start of a specific day
6669
- `startOfNextMonth()`
6770
- `startOfNextWeek()`

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "deverything",
3-
"version": "4.0.0",
3+
"version": "4.1.0",
44
"description": "Everything you need for Dev",
55
"main": "./dist/index.js",
66
"module": "./dist/index.mjs",

src/dates/bucketSortedDates.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ISODate } from "../types";
33

44
/**
55
* Buckets dates into groups based on a date series
6+
* @deprecated Use `groupByDateBucket` instead. This function will be removed in the next major version.
67
* @param dateSeries Array of dates that define the buckets, must be sorted in ascending order
78
* @param dates Array of dates to be bucketed, must be sorted in ascending order
89
* @param unit The time unit to use for bucketing ('day', 'hour', 'minute', 'second')
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { describe, it, expect } from "vitest";
2+
import { groupByDateBucket } from "./groupByDateBucket";
3+
import { ISODate } from "../types";
4+
5+
describe("groupByDateBucket", () => {
6+
const dateSeries: ISODate[] = [
7+
"2023-01-01T00:00:00.000Z",
8+
"2023-01-02T00:00:00.000Z",
9+
"2023-01-03T00:00:00.000Z",
10+
];
11+
12+
describe("with date arrays (original functionality)", () => {
13+
it("should bucket dates correctly", () => {
14+
const dates: ISODate[] = [
15+
"2023-01-01T12:00:00.000Z",
16+
"2023-01-02T06:00:00.000Z",
17+
"2023-01-02T18:00:00.000Z",
18+
"2023-01-03T10:00:00.000Z",
19+
];
20+
21+
const result = groupByDateBucket({
22+
dateBuckets: dateSeries,
23+
items: dates,
24+
unit: "day",
25+
});
26+
27+
expect(result).toEqual({
28+
"2023-01-01T00:00:00.000Z": ["2023-01-01T12:00:00.000Z"],
29+
"2023-01-02T00:00:00.000Z": [
30+
"2023-01-02T06:00:00.000Z",
31+
"2023-01-02T18:00:00.000Z",
32+
],
33+
"2023-01-03T00:00:00.000Z": ["2023-01-03T10:00:00.000Z"],
34+
});
35+
});
36+
});
37+
38+
describe("with object arrays and accessor", () => {
39+
type Event = {
40+
id: string;
41+
timestamp: ISODate;
42+
name: string;
43+
};
44+
45+
it("should bucket objects by date property using string accessor", () => {
46+
const events: Event[] = [
47+
{ id: "1", timestamp: "2023-01-01T12:00:00.000Z", name: "Event 1" },
48+
{ id: "2", timestamp: "2023-01-02T06:00:00.000Z", name: "Event 2" },
49+
{ id: "3", timestamp: "2023-01-02T18:00:00.000Z", name: "Event 3" },
50+
{ id: "4", timestamp: "2023-01-03T10:00:00.000Z", name: "Event 4" },
51+
];
52+
53+
const result = groupByDateBucket({
54+
dateBuckets: dateSeries,
55+
items: events,
56+
unit: "day",
57+
accessor: "timestamp",
58+
});
59+
60+
expect(result).toEqual({
61+
"2023-01-01T00:00:00.000Z": [
62+
{ id: "1", timestamp: "2023-01-01T12:00:00.000Z", name: "Event 1" },
63+
],
64+
"2023-01-02T00:00:00.000Z": [
65+
{ id: "2", timestamp: "2023-01-02T06:00:00.000Z", name: "Event 2" },
66+
{ id: "3", timestamp: "2023-01-02T18:00:00.000Z", name: "Event 3" },
67+
],
68+
"2023-01-03T00:00:00.000Z": [
69+
{ id: "4", timestamp: "2023-01-03T10:00:00.000Z", name: "Event 4" },
70+
],
71+
});
72+
});
73+
74+
it("should bucket objects by date property using function accessor", () => {
75+
const events: Event[] = [
76+
{ id: "1", timestamp: "2023-01-01T12:00:00.000Z", name: "Event 1" },
77+
{ id: "2", timestamp: "2023-01-02T06:00:00.000Z", name: "Event 2" },
78+
];
79+
80+
const result = groupByDateBucket({
81+
dateBuckets: dateSeries,
82+
items: events,
83+
unit: "day",
84+
accessor: (event: Event) => event.timestamp,
85+
});
86+
87+
expect(result).toEqual({
88+
"2023-01-01T00:00:00.000Z": [
89+
{ id: "1", timestamp: "2023-01-01T12:00:00.000Z", name: "Event 1" },
90+
],
91+
"2023-01-02T00:00:00.000Z": [
92+
{ id: "2", timestamp: "2023-01-02T06:00:00.000Z", name: "Event 2" },
93+
],
94+
"2023-01-03T00:00:00.000Z": [],
95+
});
96+
});
97+
98+
it("should handle nested date properties", () => {
99+
type ComplexEvent = {
100+
id: string;
101+
meta: {
102+
createdAt: ISODate;
103+
};
104+
name: string;
105+
};
106+
107+
const events: ComplexEvent[] = [
108+
{
109+
id: "1",
110+
meta: { createdAt: "2023-01-01T12:00:00.000Z" },
111+
name: "Event 1",
112+
},
113+
{
114+
id: "2",
115+
meta: { createdAt: "2023-01-02T06:00:00.000Z" },
116+
name: "Event 2",
117+
},
118+
];
119+
120+
const result = groupByDateBucket({
121+
dateBuckets: dateSeries,
122+
items: events,
123+
unit: "day",
124+
accessor: (event: ComplexEvent) => event.meta.createdAt,
125+
});
126+
127+
expect(result).toEqual({
128+
"2023-01-01T00:00:00.000Z": [
129+
{
130+
id: "1",
131+
meta: { createdAt: "2023-01-01T12:00:00.000Z" },
132+
name: "Event 1",
133+
},
134+
],
135+
"2023-01-02T00:00:00.000Z": [
136+
{
137+
id: "2",
138+
meta: { createdAt: "2023-01-02T06:00:00.000Z" },
139+
name: "Event 2",
140+
},
141+
],
142+
"2023-01-03T00:00:00.000Z": [],
143+
});
144+
});
145+
});
146+
});

src/dates/groupByDateBucket.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { parseDate } from "../helpers/parseDate";
2+
import { ISODate } from "../types";
3+
import { PropertyAccessor } from "../_internals/getProp";
4+
import { getProp } from "../_internals/getProp";
5+
6+
/**
7+
* Groups items into date buckets based on their dates and specified time intervals
8+
* @param dateBuckets Array of ISO date strings that define the bucket boundaries, must be sorted in ascending order
9+
* @param items Array of items to be grouped into buckets, should be sorted in ascending order by the date property for optimal performance
10+
* @param unit The time unit to use for bucket intervals ('day', 'hour', 'minute', 'second')
11+
* @param accessor Optional property accessor for extracting dates from items. If not provided, assumes items are ISO date strings
12+
* @returns Record where keys are ISO date strings from dateBuckets and values are arrays of items that fall within each bucket's time range
13+
*/
14+
15+
export function groupByDateBucket<T>({
16+
dateBuckets,
17+
items,
18+
unit,
19+
accessor,
20+
}: {
21+
dateBuckets: ISODate[];
22+
items: T[];
23+
unit: "day" | "hour" | "minute" | "second";
24+
accessor?: PropertyAccessor<T>;
25+
}): Record<ISODate, T[]> {
26+
// Calculate interval based on unit for virtual right edge
27+
const getIntervalMs = (
28+
unit: "day" | "hour" | "minute" | "second"
29+
): number => {
30+
switch (unit) {
31+
case "day":
32+
return 24 * 60 * 60 * 1000;
33+
case "hour":
34+
return 60 * 60 * 1000;
35+
case "minute":
36+
return 60 * 1000;
37+
case "second":
38+
return 1000;
39+
}
40+
};
41+
42+
const intervalMs = getIntervalMs(unit);
43+
44+
// Pre-compute timestamps and normalized keys for dateBuckets
45+
const bucketData = dateBuckets.map((date) => {
46+
const dateObj = parseDate(date);
47+
if (!dateObj)
48+
throw new Error(`[groupByDateBucket] Invalid dateBucket: ${date}`);
49+
return {
50+
timestamp: dateObj.getTime(),
51+
normalizedKey: dateObj.toISOString(),
52+
};
53+
});
54+
55+
const bucketedDates: Record<ISODate, T[]> = {};
56+
// Initialize each bucket with an empty array
57+
bucketData.forEach(({ normalizedKey }) => {
58+
bucketedDates[normalizedKey] = [];
59+
});
60+
61+
// Helper function to extract date from item
62+
const extractDate = (item: T): ISODate | null => {
63+
if (accessor) {
64+
return getProp(item as any, accessor as any);
65+
}
66+
// If no accessor, assume T is ISODate
67+
return item as unknown as ISODate;
68+
};
69+
70+
// Single-pass algorithm assuming both arrays are sorted
71+
let bucketIndex = 0;
72+
73+
items.forEach((item) => {
74+
const dateValue = extractDate(item);
75+
if (!dateValue) return;
76+
77+
const dateObj = parseDate(dateValue);
78+
if (!dateObj) return;
79+
80+
const dateTimestamp = dateObj.getTime();
81+
82+
// Find the appropriate bucket for this date
83+
// Since both arrays are sorted, we can advance the bucket index as needed
84+
while (bucketIndex < bucketData.length) {
85+
const currentBucketTimestamp = bucketData[bucketIndex].timestamp;
86+
const nextBucketTimestamp =
87+
bucketIndex < bucketData.length - 1
88+
? bucketData[bucketIndex + 1].timestamp
89+
: currentBucketTimestamp + intervalMs;
90+
91+
// If date falls within current bucket's range, add it and break
92+
if (
93+
dateTimestamp >= currentBucketTimestamp &&
94+
dateTimestamp < nextBucketTimestamp
95+
) {
96+
bucketedDates[bucketData[bucketIndex].normalizedKey].push(item);
97+
break;
98+
}
99+
100+
// If date is before current bucket, it means dates array is not properly sorted
101+
// or there's a gap. Skip to next bucket.
102+
if (dateTimestamp < currentBucketTimestamp) {
103+
break;
104+
}
105+
106+
// Date is after current bucket, move to next bucket
107+
bucketIndex++;
108+
}
109+
});
110+
111+
return bucketedDates;
112+
}

src/dates/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from "./bucketSortedDates";
22
export * from "./getDateRangeSeries";
33
export * from "./getDateSeriesRange";
4+
export * from "./groupByDateBucket";
45
export * from "./isOver18";
56
export * from "./startOfDay";
67
export * from "./startOfNextMonth";

src/validators/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./isArray";
22
export * from "./isArrayIncluded";
3+
export * from "./isBetweenDates";
34
export * from "./isBoolean";
45
export * from "./isBrowser";
56
export * from "./isBuffer";

0 commit comments

Comments
 (0)