Skip to content

Commit 9e8a51b

Browse files
committed
feat: introduce chunkMonthExtended util function
The `chunkMonthExtended` function allows for efficient splitting of a given `DateRange` into arrays of `DateTime` objects, each representing a week's worth of dates. This utility is particularly valuable for calendar implementations, where breaking down a DateRange into weekly chunks is often necessary for displaying data. The commit includes the implementation of `chunkMonthExtended` and a test suite. chore: add node types to tsconfig
1 parent 5161f3b commit 9e8a51b

File tree

5 files changed

+272
-2
lines changed

5 files changed

+272
-2
lines changed

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
import { RANGE_TYPE, WEEKDAY } from "./constants";
12
import { DateRange } from "./dateRange";
2-
export { DateRange };
3+
import { chunkMonthExtended } from "./utils/chunkMonthExtended";
4+
export { DateRange, RANGE_TYPE, WEEKDAY, chunkMonthExtended };

src/utils/chunkMonthExtended.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { RANGE_TYPE } from "../constants";
2+
import { DateRange } from "../dateRange";
3+
import { InvalidParameterError, MissingArgumentError } from "../errors";
4+
import { DateTime } from "luxon";
5+
6+
/**
7+
* Options for the `chunkMonthExtended` function.
8+
*/
9+
interface ChunkMonthExtendedOptions {
10+
/**
11+
* If set to `true`, the resulting chunks will have `null` values for dates
12+
* that do not belong to the same month as the reference date.
13+
* If set to `false` or not provided, the resulting chunks will only contain
14+
* valid `DateTime` objects and no `null` values.
15+
*/
16+
nullNextPrevDates?: boolean;
17+
}
18+
19+
/**
20+
* Splits the given `DateRange` into chunks of `DateTime` arrays, each containing
21+
* a week's worth of dates. Optionally, the chunks can be extended with `null`
22+
* values for dates that do not belong to the same month as the reference date.
23+
*
24+
* @param dateRange - The `DateRange` to be split into chunks.
25+
* @param options - (Optional) The {@link ChunkMonthExtendedOptions} to customize the chunking behavior.
26+
* @returns An array of arrays containing the chunks of `DateTime` objects.
27+
* If `options.nullNextPrevDates` is `true`, the chunks may contain `null` values for dates
28+
* that do not belong to the same month as the reference date.
29+
* If `options.nullNextPrevDates` is `false` or not provided, the chunks will only contain
30+
* valid `DateTime` objects and no `null` values.
31+
*
32+
* @throws {MissingArgumentError}
33+
* When `dateRange` is not provided.
34+
* @throws {Error}
35+
* When `dateRange` is not an instance of `DateRange` or has an invalid range type.
36+
* @throws {InvalidParameterError}
37+
* When `options.nullNextPrevDates` is provided but not a boolean value.
38+
*
39+
* @example
40+
* // Example: Splitting the current month into chunks with `null` values for dates outside the month.
41+
* const dateRange = new DateRange().getMonthExtended();
42+
* // dateRange is a `DateRange` for the current month, extended to full weeks.
43+
*
44+
* const result = chunkMonthExtended(dateRange, { nullNextPrevDates: true });
45+
* // result will be an array of arrays, each containing a week's worth of dates,
46+
* // with `null` values for dates that do not belong to the same month as the reference date.
47+
*/
48+
export function chunkMonthExtended(
49+
dateRange: DateRange,
50+
options?: ChunkMonthExtendedOptions & { nullNextPrevDates: false },
51+
): DateTime[][];
52+
53+
// Function overload for the main function signature
54+
export function chunkMonthExtended(
55+
dateRange: DateRange,
56+
options: ChunkMonthExtendedOptions & { nullNextPrevDates: true },
57+
): (DateTime | null)[][];
58+
59+
// Function overload for the main function signature
60+
export function chunkMonthExtended(
61+
dateRange: DateRange,
62+
options?: ChunkMonthExtendedOptions,
63+
): (DateTime | null)[][] | DateTime[][];
64+
65+
// Function implementation
66+
export function chunkMonthExtended(
67+
dateRange: DateRange,
68+
options?: ChunkMonthExtendedOptions,
69+
): (DateTime | null)[][] | DateTime[][] {
70+
if (dateRange === undefined) {
71+
throw new MissingArgumentError("dateRange", "chunkMonthExtended");
72+
}
73+
if (
74+
!(dateRange instanceof DateRange) ||
75+
dateRange.rangeType !== RANGE_TYPE.MonthExtended
76+
) {
77+
throw new Error(
78+
'The dateRange argument must be an instance of the DateRange class, with a generated range of type: "MONTH-EXTENDED"',
79+
);
80+
}
81+
82+
const nullNextPrevDates = options?.nullNextPrevDates ?? false;
83+
84+
if (typeof nullNextPrevDates !== "boolean") {
85+
throw new InvalidParameterError(
86+
"nullNextPrevDates",
87+
nullNextPrevDates,
88+
"boolean",
89+
);
90+
}
91+
92+
const dateTimes = [...dateRange.toDateTimes()];
93+
94+
const result: DateTime[][] = [];
95+
96+
for (let i = 0; i < dateTimes.length; i += 7) {
97+
const chunk = dateTimes.slice(i, i + 7);
98+
result.push(chunk);
99+
}
100+
101+
if (nullNextPrevDates) {
102+
const resultWithNulls: (DateTime | null)[][] = [...result];
103+
104+
const { month } = dateRange.refDate;
105+
// handle first chunk
106+
result[0].forEach((dateTime, index) => {
107+
if (dateTime.month !== month) {
108+
resultWithNulls[0][index] = null;
109+
}
110+
});
111+
112+
// handle last chunk
113+
result[result.length - 1].forEach((dateTime, index) => {
114+
if (dateTime.month !== month) {
115+
resultWithNulls[resultWithNulls.length - 1][index] = null;
116+
}
117+
});
118+
119+
return resultWithNulls;
120+
}
121+
return result;
122+
}

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export { isValidWeekday } from "./isValidWeekday";
1111
export type { PropertiesMap } from "./types";
1212
export { getRandomDateTime } from "./getRandomDateTime";
1313
export { getRandomWeekday } from "./getRandomWeekday";
14+
export { chunkMonthExtended } from "./chunkMonthExtended";
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { DateRange } from "../../src/dateRange";
2+
import { chunkMonthExtended } from "../../src/utils";
3+
import { DateTime } from "luxon";
4+
import { describe, expect, expectTypeOf, it, test } from "vitest";
5+
6+
describe("chunkMonthExtended", () => {
7+
describe("input validation", () => {
8+
test("throws an error if no arguments are specified", () => {
9+
// @ts-expect-error: testing invalid input
10+
expect(() => chunkMonthExtended()).toThrowError();
11+
});
12+
test("throws an error if dateRange argument isn't instance of DateRage", () => {
13+
// @ts-expect-error: testing invalid input
14+
expect(() => chunkMonthExtended("invalid")).toThrowError();
15+
});
16+
test("throws an error if dateRange argument is valid DateRange instance but generated range is not type MONTH-EXTENDED", () => {
17+
expect(() =>
18+
chunkMonthExtended(new DateRange().getMonthExact()),
19+
).toThrowError();
20+
});
21+
test.each(["invalid", 1, {}, []])(
22+
"throws an error if the second argument is not valid. Tested value: %j",
23+
(value) => {
24+
expect(() =>
25+
chunkMonthExtended(new DateRange().getMonthExtended(), {
26+
// @ts-expect-error: testing invalid input
27+
nullNextPrevDates: value,
28+
}),
29+
).toThrowError();
30+
},
31+
);
32+
});
33+
34+
describe("functionality", () => {
35+
const dateStrings = [
36+
"2023-01-01",
37+
"1990-05-24",
38+
"2015-12-31",
39+
"2008-07-14",
40+
"2021-04-26",
41+
"2003-09-11",
42+
"2019-10-30",
43+
"2006-02-28",
44+
"2014-06-15",
45+
"2020-08-08",
46+
];
47+
48+
describe("With nullExtendedDates option set to false (default)", () => {
49+
describe.each(dateStrings)("with refDate: %s", (dateString) => {
50+
test("returns an array of DateTime arrays, each with a length of 7", () => {
51+
const chunk = chunkMonthExtended(
52+
new DateRange().getMonthExtended({
53+
refDate: new Date(dateString),
54+
}),
55+
);
56+
// check if the result is an array
57+
expectTypeOf(chunk).toBeArray();
58+
// check if each item in the chunk is an array of 7 DateTime objects
59+
chunk.forEach((dates) => {
60+
expectTypeOf(dates).toBeArray();
61+
expect(dates.length).toBe(7);
62+
dates.forEach((date) => {
63+
expect(date).toBeInstanceOf(DateTime);
64+
});
65+
});
66+
});
67+
68+
test("chunk contains all dateTimes from the passed dateRange", () => {
69+
const dateRange = new DateRange().getMonthExtended({
70+
refDate: new Date(dateString),
71+
});
72+
const dateTimes = dateRange.toDateTimes();
73+
const chunk = chunkMonthExtended(dateRange);
74+
75+
let index = 0;
76+
// loop through the chunk array and compare each element with the corresponding element in the dateTimes array
77+
chunk.forEach((dates) => {
78+
dates.forEach((date) => {
79+
expect(date).toEqual(dateTimes[index]);
80+
index++;
81+
});
82+
});
83+
84+
// check if chunk has the same number of elements as the dateTimes
85+
expect(index).toBe(dateTimes.length);
86+
});
87+
});
88+
});
89+
90+
describe("with nullExtendedDates option set to true", () => {
91+
describe.each(dateStrings)("with refDate: %s", (dateString) => {
92+
test("returns an array of DateTime or null arrays, each with a length of 7", () => {
93+
const chunk = chunkMonthExtended(
94+
new DateRange().getMonthExtended({ refDate: new Date(dateString) }),
95+
{ nullNextPrevDates: true },
96+
);
97+
// check if the result is an array
98+
expectTypeOf(chunk).toBeArray();
99+
// check if each item in the chunk is an array of 7 elements (DateTime or null)
100+
chunk.forEach((dates) => {
101+
expectTypeOf(dates).toBeArray();
102+
expect(dates.length).toBe(7);
103+
dates.forEach((date) => {
104+
date === null
105+
? expectTypeOf(date).toBeNull()
106+
: expect(date).toBeInstanceOf(DateTime);
107+
});
108+
});
109+
});
110+
111+
test("chunk contains only current month dates, previous and next dates are null", () => {
112+
const dateRange = new DateRange().getMonthExtended();
113+
const currentMonth = dateRange.refDate.month;
114+
const dateTimes = dateRange.toDateTimes();
115+
const chunk = chunkMonthExtended(dateRange, {
116+
nullNextPrevDates: true,
117+
});
118+
119+
// get the indexes of the dates that are not in the current month
120+
const getNextPrevIndexes = dateRange.dateTimes
121+
.map((dateTime, index) => {
122+
if (dateTime.month !== currentMonth) return index;
123+
})
124+
.filter((index) => index !== undefined);
125+
126+
let index = 0;
127+
// loop through the chunk array and compare each element with the corresponding element in the dateTimes array
128+
chunk.forEach((dates) => {
129+
dates.forEach((date) => {
130+
if (getNextPrevIndexes.includes(index)) {
131+
expect(date).toBeNull();
132+
} else {
133+
expect(date).toEqual(dateTimes[index]);
134+
}
135+
index++;
136+
});
137+
});
138+
139+
// check if chunk has the same number of elements as the dateTimes
140+
expect(index).toBe(dateTimes.length);
141+
});
142+
});
143+
});
144+
});
145+
});

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"esModuleInterop": true,
1111
"skipLibCheck": true,
1212
"experimentalDecorators": true,
13-
"types": ["vite/client"],
13+
"types": ["vite/client", "node"],
1414
"baseUrl": ".",
1515
"paths": {
1616
"@/*": ["src/*"]

0 commit comments

Comments
 (0)