@f-o-t/datetime 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/CHANGELOG.md +13 -0
- package/README.md +726 -0
- package/biome.json +39 -0
- package/bunup.config.ts +14 -0
- package/examples/plugins-demo.ts +85 -0
- package/fot.config.ts +5 -0
- package/package.json +47 -0
- package/src/core/datetime.test.ts +1498 -0
- package/src/core/datetime.ts +694 -0
- package/src/core/factory.test.ts +167 -0
- package/src/core/factory.ts +32 -0
- package/src/errors.ts +82 -0
- package/src/index.ts +20 -0
- package/src/plugins/business-days/business-days.test.ts +225 -0
- package/src/plugins/business-days/index.ts +126 -0
- package/src/plugins/format/format.test.ts +173 -0
- package/src/plugins/format/index.ts +78 -0
- package/src/plugins/format/tokens.ts +153 -0
- package/src/plugins/index.ts +15 -0
- package/src/plugins/plugin-base.test.ts +211 -0
- package/src/plugins/plugin-base.ts +104 -0
- package/src/plugins/relative-time/index.ts +169 -0
- package/src/plugins/relative-time/relative-time.test.ts +164 -0
- package/src/plugins/timezone/index.ts +152 -0
- package/src/plugins/timezone/timezone.test.ts +135 -0
- package/src/schemas.test.ts +283 -0
- package/src/schemas.ts +104 -0
- package/src/types.ts +122 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { InvalidDateError } from "../errors";
|
|
3
|
+
import type { DateInput } from "../types";
|
|
4
|
+
import { DateTime } from "./datetime";
|
|
5
|
+
import { datetime } from "./factory";
|
|
6
|
+
|
|
7
|
+
describe("datetime factory function", () => {
|
|
8
|
+
describe("basic usage", () => {
|
|
9
|
+
test("creates DateTime instance without arguments (current time)", () => {
|
|
10
|
+
const dt = datetime();
|
|
11
|
+
expect(dt).toBeInstanceOf(DateTime);
|
|
12
|
+
expect(dt.isValid()).toBe(true);
|
|
13
|
+
|
|
14
|
+
// Should be close to current time (within 100ms)
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
expect(Math.abs(dt.valueOf() - now)).toBeLessThan(100);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("creates DateTime from Date object", () => {
|
|
20
|
+
const date = new Date("2024-01-15T10:30:00Z");
|
|
21
|
+
const dt = datetime(date);
|
|
22
|
+
|
|
23
|
+
expect(dt).toBeInstanceOf(DateTime);
|
|
24
|
+
expect(dt.isValid()).toBe(true);
|
|
25
|
+
expect(dt.toISO()).toBe("2024-01-15T10:30:00.000Z");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("creates DateTime from ISO string", () => {
|
|
29
|
+
const dt = datetime("2024-01-15T10:30:00Z");
|
|
30
|
+
|
|
31
|
+
expect(dt).toBeInstanceOf(DateTime);
|
|
32
|
+
expect(dt.isValid()).toBe(true);
|
|
33
|
+
expect(dt.toISO()).toBe("2024-01-15T10:30:00.000Z");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("creates DateTime from timestamp", () => {
|
|
37
|
+
const timestamp = 1705315800000; // 2024-01-15T10:30:00Z
|
|
38
|
+
const dt = datetime(timestamp);
|
|
39
|
+
|
|
40
|
+
expect(dt).toBeInstanceOf(DateTime);
|
|
41
|
+
expect(dt.isValid()).toBe(true);
|
|
42
|
+
expect(dt.valueOf()).toBe(timestamp);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("creates DateTime from another DateTime", () => {
|
|
46
|
+
const dt1 = datetime("2024-01-15T10:30:00Z");
|
|
47
|
+
const dt2 = datetime(dt1);
|
|
48
|
+
|
|
49
|
+
expect(dt2).toBeInstanceOf(DateTime);
|
|
50
|
+
expect(dt2.isValid()).toBe(true);
|
|
51
|
+
expect(dt2.toISO()).toBe(dt1.toISO());
|
|
52
|
+
expect(dt2.valueOf()).toBe(dt1.valueOf());
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("edge cases", () => {
|
|
57
|
+
test("creates invalid DateTime from invalid date string", () => {
|
|
58
|
+
const dt = datetime("invalid");
|
|
59
|
+
|
|
60
|
+
expect(dt).toBeInstanceOf(DateTime);
|
|
61
|
+
expect(dt.isValid()).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("creates invalid DateTime from NaN", () => {
|
|
65
|
+
const dt = datetime(NaN);
|
|
66
|
+
|
|
67
|
+
expect(dt).toBeInstanceOf(DateTime);
|
|
68
|
+
expect(dt.isValid()).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("creates invalid DateTime from invalid Date object", () => {
|
|
72
|
+
const dt = datetime(new Date("invalid"));
|
|
73
|
+
|
|
74
|
+
expect(dt).toBeInstanceOf(DateTime);
|
|
75
|
+
expect(dt.isValid()).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("throws InvalidDateError for non-DateInput types", () => {
|
|
79
|
+
expect(() => datetime({} as any)).toThrow(InvalidDateError);
|
|
80
|
+
expect(() => datetime([] as any)).toThrow(InvalidDateError);
|
|
81
|
+
expect(() => datetime(true as any)).toThrow(InvalidDateError);
|
|
82
|
+
expect(() => datetime(null as any)).toThrow(InvalidDateError);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("immutability", () => {
|
|
87
|
+
test("creates independent instances", () => {
|
|
88
|
+
const dt1 = datetime("2024-01-15T10:30:00Z");
|
|
89
|
+
const dt2 = datetime(dt1);
|
|
90
|
+
const dt3 = dt2.addDays(1);
|
|
91
|
+
|
|
92
|
+
expect(dt1.toISO()).toBe("2024-01-15T10:30:00.000Z");
|
|
93
|
+
expect(dt2.toISO()).toBe("2024-01-15T10:30:00.000Z");
|
|
94
|
+
expect(dt3.toISO()).toBe("2024-01-16T10:30:00.000Z");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("does not mutate input Date object", () => {
|
|
98
|
+
const date = new Date("2024-01-15T10:30:00Z");
|
|
99
|
+
const originalTime = date.getTime();
|
|
100
|
+
const dt = datetime(date);
|
|
101
|
+
|
|
102
|
+
dt.addDays(1);
|
|
103
|
+
|
|
104
|
+
expect(date.getTime()).toBe(originalTime);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("equivalence to constructor", () => {
|
|
109
|
+
test("produces same result as constructor", () => {
|
|
110
|
+
const inputs: Array<DateInput | undefined> = [
|
|
111
|
+
undefined,
|
|
112
|
+
new Date("2024-01-15T10:30:00Z"),
|
|
113
|
+
"2024-01-15T10:30:00Z",
|
|
114
|
+
1705315800000,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
for (const input of inputs) {
|
|
118
|
+
const factory = datetime(input);
|
|
119
|
+
const constructor = new DateTime(input);
|
|
120
|
+
|
|
121
|
+
expect(factory.valueOf()).toBe(constructor.valueOf());
|
|
122
|
+
expect(factory.isValid()).toBe(constructor.isValid());
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("throws same errors as constructor", () => {
|
|
127
|
+
const invalidInputs = [{}, [], true, null];
|
|
128
|
+
|
|
129
|
+
for (const input of invalidInputs) {
|
|
130
|
+
expect(() => datetime(input as any)).toThrow(InvalidDateError);
|
|
131
|
+
expect(() => new DateTime(input as any)).toThrow(InvalidDateError);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("chaining", () => {
|
|
137
|
+
test("can chain operations after factory creation", () => {
|
|
138
|
+
const dt = datetime("2024-01-15T10:30:00Z")
|
|
139
|
+
.addDays(5)
|
|
140
|
+
.addHours(3)
|
|
141
|
+
.subtractMinutes(15);
|
|
142
|
+
|
|
143
|
+
expect(dt.toISO()).toBe("2024-01-20T13:15:00.000Z");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("can use comparison methods", () => {
|
|
147
|
+
const dt1 = datetime("2024-01-15T10:30:00Z");
|
|
148
|
+
const dt2 = datetime("2024-01-16T10:30:00Z");
|
|
149
|
+
|
|
150
|
+
expect(dt1.isBefore(dt2)).toBe(true);
|
|
151
|
+
expect(dt2.isAfter(dt1)).toBe(true);
|
|
152
|
+
expect(dt1.isSame(dt1)).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("can use getter methods", () => {
|
|
156
|
+
const dt = datetime("2024-01-15T10:30:45.123Z");
|
|
157
|
+
|
|
158
|
+
expect(dt.year()).toBe(2024);
|
|
159
|
+
expect(dt.month()).toBe(0); // January is 0
|
|
160
|
+
expect(dt.date()).toBe(15);
|
|
161
|
+
expect(dt.hour()).toBe(10);
|
|
162
|
+
expect(dt.minute()).toBe(30);
|
|
163
|
+
expect(dt.second()).toBe(45);
|
|
164
|
+
expect(dt.millisecond()).toBe(123);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DateInput } from "../types.ts";
|
|
2
|
+
import { DateTime } from "./datetime.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Factory function to create a DateTime instance
|
|
6
|
+
* Provides a convenient alternative to using the constructor
|
|
7
|
+
*
|
|
8
|
+
* @param input - Date input (Date, ISO string, timestamp, DateTime, or undefined for current time)
|
|
9
|
+
* @returns A new DateTime instance
|
|
10
|
+
* @throws {InvalidDateError} When input fails validation
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // Create with current time
|
|
15
|
+
* const now = datetime();
|
|
16
|
+
*
|
|
17
|
+
* // Create from Date object
|
|
18
|
+
* const dt1 = datetime(new Date());
|
|
19
|
+
*
|
|
20
|
+
* // Create from ISO string
|
|
21
|
+
* const dt2 = datetime("2024-01-15T10:30:00Z");
|
|
22
|
+
*
|
|
23
|
+
* // Create from timestamp
|
|
24
|
+
* const dt3 = datetime(1705315800000);
|
|
25
|
+
*
|
|
26
|
+
* // Create from another DateTime
|
|
27
|
+
* const dt4 = datetime(dt1);
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function datetime(input?: DateInput): DateTime {
|
|
31
|
+
return new DateTime(input);
|
|
32
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all DateTime-related errors
|
|
3
|
+
*/
|
|
4
|
+
export class DateTimeError extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "DateTimeError";
|
|
8
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
9
|
+
if (Error.captureStackTrace) {
|
|
10
|
+
Error.captureStackTrace(this, this.constructor);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Error thrown when an invalid date is provided
|
|
17
|
+
*/
|
|
18
|
+
export class InvalidDateError extends DateTimeError {
|
|
19
|
+
constructor(
|
|
20
|
+
message = "Invalid date",
|
|
21
|
+
public readonly input?: unknown,
|
|
22
|
+
) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "InvalidDateError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Error thrown when a date string doesn't match the expected format
|
|
30
|
+
*/
|
|
31
|
+
export class InvalidFormatError extends DateTimeError {
|
|
32
|
+
constructor(
|
|
33
|
+
message: string,
|
|
34
|
+
public readonly input?: string,
|
|
35
|
+
public readonly expectedFormat?: string,
|
|
36
|
+
) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = "InvalidFormatError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Error thrown when an invalid timezone is specified
|
|
44
|
+
*/
|
|
45
|
+
export class InvalidTimezoneError extends DateTimeError {
|
|
46
|
+
constructor(
|
|
47
|
+
message: string,
|
|
48
|
+
public readonly timezone?: string,
|
|
49
|
+
) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "InvalidTimezoneError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Error thrown when a plugin operation fails
|
|
57
|
+
*/
|
|
58
|
+
export class PluginError extends DateTimeError {
|
|
59
|
+
constructor(
|
|
60
|
+
message: string,
|
|
61
|
+
public readonly pluginName?: string,
|
|
62
|
+
) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "PluginError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Error thrown when attempting to use functionality that requires a plugin that isn't installed
|
|
70
|
+
*/
|
|
71
|
+
export class MissingPluginError extends PluginError {
|
|
72
|
+
constructor(
|
|
73
|
+
public readonly requiredPlugin: string,
|
|
74
|
+
message?: string,
|
|
75
|
+
) {
|
|
76
|
+
super(
|
|
77
|
+
message || `Missing required plugin: ${requiredPlugin}`,
|
|
78
|
+
requiredPlugin,
|
|
79
|
+
);
|
|
80
|
+
this.name = "MissingPluginError";
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Core exports
|
|
2
|
+
export { DateTime } from "./core/datetime.ts";
|
|
3
|
+
export { datetime } from "./core/factory.ts";
|
|
4
|
+
// Error exports
|
|
5
|
+
export { InvalidDateError, PluginError } from "./errors.ts";
|
|
6
|
+
// Plugin utilities
|
|
7
|
+
export { createPlugin, isPlugin, isValidPluginName } from "./plugins/index.ts";
|
|
8
|
+
|
|
9
|
+
// Schema exports
|
|
10
|
+
export { DateInputSchema } from "./schemas.ts";
|
|
11
|
+
// Type exports
|
|
12
|
+
export type {
|
|
13
|
+
DateInput,
|
|
14
|
+
DateTimeClass,
|
|
15
|
+
DateTimeConfig,
|
|
16
|
+
DateTimePlugin,
|
|
17
|
+
FormatOptions,
|
|
18
|
+
ParseOptions,
|
|
19
|
+
TimeUnit,
|
|
20
|
+
} from "./types.ts";
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { DateTime } from "../../core/datetime";
|
|
3
|
+
import { businessDaysPlugin } from "./index";
|
|
4
|
+
|
|
5
|
+
describe("Business Days Plugin", () => {
|
|
6
|
+
// Install plugin before running tests
|
|
7
|
+
DateTime.extend(businessDaysPlugin);
|
|
8
|
+
|
|
9
|
+
describe("isWeekday()", () => {
|
|
10
|
+
it("should return true for Monday", () => {
|
|
11
|
+
// 2024-01-15 is a Monday
|
|
12
|
+
const dt = new DateTime("2024-01-15T12:00:00.000Z");
|
|
13
|
+
expect((dt as any).isWeekday()).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should return true for Tuesday", () => {
|
|
17
|
+
// 2024-01-16 is a Tuesday
|
|
18
|
+
const dt = new DateTime("2024-01-16T12:00:00.000Z");
|
|
19
|
+
expect((dt as any).isWeekday()).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should return true for Wednesday", () => {
|
|
23
|
+
// 2024-01-17 is a Wednesday
|
|
24
|
+
const dt = new DateTime("2024-01-17T12:00:00.000Z");
|
|
25
|
+
expect((dt as any).isWeekday()).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return true for Thursday", () => {
|
|
29
|
+
// 2024-01-18 is a Thursday
|
|
30
|
+
const dt = new DateTime("2024-01-18T12:00:00.000Z");
|
|
31
|
+
expect((dt as any).isWeekday()).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should return true for Friday", () => {
|
|
35
|
+
// 2024-01-19 is a Friday
|
|
36
|
+
const dt = new DateTime("2024-01-19T12:00:00.000Z");
|
|
37
|
+
expect((dt as any).isWeekday()).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should return false for Saturday", () => {
|
|
41
|
+
// 2024-01-20 is a Saturday
|
|
42
|
+
const dt = new DateTime("2024-01-20T12:00:00.000Z");
|
|
43
|
+
expect((dt as any).isWeekday()).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should return false for Sunday", () => {
|
|
47
|
+
// 2024-01-21 is a Sunday
|
|
48
|
+
const dt = new DateTime("2024-01-21T12:00:00.000Z");
|
|
49
|
+
expect((dt as any).isWeekday()).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("isWeekend()", () => {
|
|
54
|
+
it("should return false for Monday", () => {
|
|
55
|
+
const dt = new DateTime("2024-01-15T12:00:00.000Z");
|
|
56
|
+
expect((dt as any).isWeekend()).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should return false for Friday", () => {
|
|
60
|
+
const dt = new DateTime("2024-01-19T12:00:00.000Z");
|
|
61
|
+
expect((dt as any).isWeekend()).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should return true for Saturday", () => {
|
|
65
|
+
const dt = new DateTime("2024-01-20T12:00:00.000Z");
|
|
66
|
+
expect((dt as any).isWeekend()).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should return true for Sunday", () => {
|
|
70
|
+
const dt = new DateTime("2024-01-21T12:00:00.000Z");
|
|
71
|
+
expect((dt as any).isWeekend()).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("addBusinessDays()", () => {
|
|
76
|
+
it("should add business days correctly", () => {
|
|
77
|
+
// Monday + 1 business day = Tuesday
|
|
78
|
+
const monday = new DateTime("2024-01-15T12:00:00.000Z");
|
|
79
|
+
const tuesday = (monday as any).addBusinessDays(1);
|
|
80
|
+
expect(tuesday.date()).toBe(16);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should skip weekends when adding business days", () => {
|
|
84
|
+
// Friday + 1 business day = Monday
|
|
85
|
+
const friday = new DateTime("2024-01-19T12:00:00.000Z");
|
|
86
|
+
const monday = (friday as any).addBusinessDays(1);
|
|
87
|
+
expect(monday.date()).toBe(22);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should handle multiple business days across weekends", () => {
|
|
91
|
+
// Friday + 3 business days = Wednesday (skip Sat, Sun, Mon, Tue, Wed)
|
|
92
|
+
const friday = new DateTime("2024-01-19T12:00:00.000Z");
|
|
93
|
+
const wednesday = (friday as any).addBusinessDays(3);
|
|
94
|
+
expect(wednesday.date()).toBe(24);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should handle adding from Saturday", () => {
|
|
98
|
+
// Saturday + 1 business day = Monday
|
|
99
|
+
const saturday = new DateTime("2024-01-20T12:00:00.000Z");
|
|
100
|
+
const monday = (saturday as any).addBusinessDays(1);
|
|
101
|
+
expect(monday.date()).toBe(22);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should handle adding from Sunday", () => {
|
|
105
|
+
// Sunday + 1 business day = Monday
|
|
106
|
+
const sunday = new DateTime("2024-01-21T12:00:00.000Z");
|
|
107
|
+
const monday = (sunday as any).addBusinessDays(1);
|
|
108
|
+
expect(monday.date()).toBe(22);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should handle adding 0 business days", () => {
|
|
112
|
+
const dt = new DateTime("2024-01-15T12:00:00.000Z");
|
|
113
|
+
const result = (dt as any).addBusinessDays(0);
|
|
114
|
+
expect(result.valueOf()).toBe(dt.valueOf());
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should preserve time when adding business days", () => {
|
|
118
|
+
const dt = new DateTime("2024-01-15T14:30:45.123Z");
|
|
119
|
+
const result = (dt as any).addBusinessDays(1);
|
|
120
|
+
expect(result.hour()).toBe(14);
|
|
121
|
+
expect(result.minute()).toBe(30);
|
|
122
|
+
expect(result.second()).toBe(45);
|
|
123
|
+
expect(result.millisecond()).toBe(123);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("subtractBusinessDays()", () => {
|
|
128
|
+
it("should subtract business days correctly", () => {
|
|
129
|
+
// Tuesday - 1 business day = Monday
|
|
130
|
+
const tuesday = new DateTime("2024-01-16T12:00:00.000Z");
|
|
131
|
+
const monday = (tuesday as any).subtractBusinessDays(1);
|
|
132
|
+
expect(monday.date()).toBe(15);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should skip weekends when subtracting business days", () => {
|
|
136
|
+
// Monday - 1 business day = Friday
|
|
137
|
+
const monday = new DateTime("2024-01-22T12:00:00.000Z");
|
|
138
|
+
const friday = (monday as any).subtractBusinessDays(1);
|
|
139
|
+
expect(friday.date()).toBe(19);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should handle multiple business days across weekends", () => {
|
|
143
|
+
// Wednesday - 3 business days = Friday (previous week)
|
|
144
|
+
const wednesday = new DateTime("2024-01-24T12:00:00.000Z");
|
|
145
|
+
const friday = (wednesday as any).subtractBusinessDays(3);
|
|
146
|
+
expect(friday.date()).toBe(19);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should handle subtracting from Saturday", () => {
|
|
150
|
+
// Saturday - 1 business day = Friday
|
|
151
|
+
const saturday = new DateTime("2024-01-20T12:00:00.000Z");
|
|
152
|
+
const friday = (saturday as any).subtractBusinessDays(1);
|
|
153
|
+
expect(friday.date()).toBe(19);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should handle subtracting from Sunday", () => {
|
|
157
|
+
// Sunday - 1 business day = Friday
|
|
158
|
+
const sunday = new DateTime("2024-01-21T12:00:00.000Z");
|
|
159
|
+
const friday = (sunday as any).subtractBusinessDays(1);
|
|
160
|
+
expect(friday.date()).toBe(19);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should handle subtracting 0 business days", () => {
|
|
164
|
+
const dt = new DateTime("2024-01-15T12:00:00.000Z");
|
|
165
|
+
const result = (dt as any).subtractBusinessDays(0);
|
|
166
|
+
expect(result.valueOf()).toBe(dt.valueOf());
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("diffBusinessDays()", () => {
|
|
171
|
+
it("should calculate business days between two weekdays", () => {
|
|
172
|
+
// Monday to Friday = 4 business days
|
|
173
|
+
const monday = new DateTime("2024-01-15T12:00:00.000Z");
|
|
174
|
+
const friday = new DateTime("2024-01-19T12:00:00.000Z");
|
|
175
|
+
expect((monday as any).diffBusinessDays(friday)).toBe(4);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should exclude weekends in calculation", () => {
|
|
179
|
+
// Friday to next Monday = 1 business day
|
|
180
|
+
const friday = new DateTime("2024-01-19T12:00:00.000Z");
|
|
181
|
+
const monday = new DateTime("2024-01-22T12:00:00.000Z");
|
|
182
|
+
expect((friday as any).diffBusinessDays(monday)).toBe(1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should return negative values when going backwards", () => {
|
|
186
|
+
const friday = new DateTime("2024-01-19T12:00:00.000Z");
|
|
187
|
+
const monday = new DateTime("2024-01-15T12:00:00.000Z");
|
|
188
|
+
expect((friday as any).diffBusinessDays(monday)).toBe(-4);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should return 0 for same day", () => {
|
|
192
|
+
const dt1 = new DateTime("2024-01-15T12:00:00.000Z");
|
|
193
|
+
const dt2 = new DateTime("2024-01-15T18:00:00.000Z");
|
|
194
|
+
expect((dt1 as any).diffBusinessDays(dt2)).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should handle weekend dates", () => {
|
|
198
|
+
// Saturday to Sunday = 0 business days
|
|
199
|
+
const saturday = new DateTime("2024-01-20T12:00:00.000Z");
|
|
200
|
+
const sunday = new DateTime("2024-01-21T12:00:00.000Z");
|
|
201
|
+
expect((saturday as any).diffBusinessDays(sunday)).toBe(0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should handle crossing multiple weeks", () => {
|
|
205
|
+
// Monday to Monday (2 weeks) = 10 business days
|
|
206
|
+
const monday1 = new DateTime("2024-01-15T12:00:00.000Z");
|
|
207
|
+
const monday2 = new DateTime("2024-01-29T12:00:00.000Z");
|
|
208
|
+
expect((monday1 as any).diffBusinessDays(monday2)).toBe(10);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("Chaining", () => {
|
|
213
|
+
it("should chain business day methods with other DateTime methods", () => {
|
|
214
|
+
const dt = new DateTime("2024-01-15T12:00:00.000Z");
|
|
215
|
+
const result = (dt as any).addBusinessDays(5).addHours(2);
|
|
216
|
+
expect(result).toBeInstanceOf(DateTime);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should work with arithmetic operations", () => {
|
|
220
|
+
const dt = new DateTime("2024-01-15T12:00:00.000Z");
|
|
221
|
+
const result = (dt as any).addBusinessDays(3).subtractBusinessDays(1);
|
|
222
|
+
expect((dt as any).diffBusinessDays(result)).toBe(2);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { DateTime } from "../../core/datetime";
|
|
2
|
+
import type { DateTimeClass } from "../../types";
|
|
3
|
+
import { createPlugin } from "../plugin-base";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks if a given day of week is a weekday (Monday-Friday)
|
|
7
|
+
* @param dayOfWeek - Day of week (0=Sunday, 6=Saturday)
|
|
8
|
+
* @returns true if weekday, false otherwise
|
|
9
|
+
*/
|
|
10
|
+
function isWeekdayDay(dayOfWeek: number): boolean {
|
|
11
|
+
return dayOfWeek >= 1 && dayOfWeek <= 5;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extended DateTime interface with business days methods
|
|
16
|
+
*/
|
|
17
|
+
declare module "../../core/datetime" {
|
|
18
|
+
interface DateTime {
|
|
19
|
+
/**
|
|
20
|
+
* Checks if this date is a weekday (Monday-Friday)
|
|
21
|
+
* @returns true if weekday, false otherwise
|
|
22
|
+
*/
|
|
23
|
+
isWeekday(): boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Checks if this date is a weekend (Saturday-Sunday)
|
|
27
|
+
* @returns true if weekend, false otherwise
|
|
28
|
+
*/
|
|
29
|
+
isWeekend(): boolean;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Adds business days to this date
|
|
33
|
+
* Skips weekends (Saturday and Sunday)
|
|
34
|
+
* @param n - Number of business days to add
|
|
35
|
+
* @returns New DateTime instance with business days added
|
|
36
|
+
*/
|
|
37
|
+
addBusinessDays(n: number): DateTime;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Subtracts business days from this date
|
|
41
|
+
* Skips weekends (Saturday and Sunday)
|
|
42
|
+
* @param n - Number of business days to subtract
|
|
43
|
+
* @returns New DateTime instance with business days subtracted
|
|
44
|
+
*/
|
|
45
|
+
subtractBusinessDays(n: number): DateTime;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Calculates the number of business days between this date and another
|
|
49
|
+
* Excludes weekends from the calculation
|
|
50
|
+
* @param other - DateTime instance to compare against
|
|
51
|
+
* @returns Number of business days (positive if other is after, negative if before)
|
|
52
|
+
*/
|
|
53
|
+
diffBusinessDays(other: DateTime): number;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Business Days plugin for DateTime
|
|
59
|
+
* Adds methods for working with business days (weekdays only)
|
|
60
|
+
* Week is defined as Monday-Friday (weekdays), Saturday-Sunday (weekend)
|
|
61
|
+
*/
|
|
62
|
+
export const businessDaysPlugin = createPlugin(
|
|
63
|
+
"business-days",
|
|
64
|
+
(DateTimeClass: DateTimeClass) => {
|
|
65
|
+
// Add instance methods
|
|
66
|
+
DateTimeClass.prototype.isWeekday = function (): boolean {
|
|
67
|
+
const dayOfWeek = this.day(); // 0=Sunday, 6=Saturday
|
|
68
|
+
return isWeekdayDay(dayOfWeek);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
DateTimeClass.prototype.isWeekend = function (): boolean {
|
|
72
|
+
const dayOfWeek = this.day();
|
|
73
|
+
return dayOfWeek === 0 || dayOfWeek === 6;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
DateTimeClass.prototype.addBusinessDays = function (n: number): DateTime {
|
|
77
|
+
if (n === 0) {
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let current = new DateTimeClass(this.valueOf());
|
|
82
|
+
let daysToAdd = Math.abs(n);
|
|
83
|
+
const direction = n > 0 ? 1 : -1;
|
|
84
|
+
|
|
85
|
+
while (daysToAdd > 0) {
|
|
86
|
+
// Move one day in the specified direction
|
|
87
|
+
current = current.addDays(direction);
|
|
88
|
+
|
|
89
|
+
// Only count weekdays
|
|
90
|
+
if ((current as any).isWeekday()) {
|
|
91
|
+
daysToAdd--;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return current;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
DateTimeClass.prototype.subtractBusinessDays = function (
|
|
99
|
+
n: number,
|
|
100
|
+
): DateTime {
|
|
101
|
+
return (this as any).addBusinessDays(-n);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
DateTimeClass.prototype.diffBusinessDays = function (
|
|
105
|
+
other: DateTime,
|
|
106
|
+
): number {
|
|
107
|
+
// Get start and end dates (normalized to start of day for consistency)
|
|
108
|
+
const start = this.valueOf() < other.valueOf() ? this : other;
|
|
109
|
+
const end = this.valueOf() < other.valueOf() ? other : this;
|
|
110
|
+
const isReversed = this.valueOf() > other.valueOf();
|
|
111
|
+
|
|
112
|
+
let businessDays = 0;
|
|
113
|
+
let current = new DateTimeClass(start.valueOf());
|
|
114
|
+
|
|
115
|
+
// Iterate through each day and count weekdays
|
|
116
|
+
while (current.startOfDay().valueOf() < end.startOfDay().valueOf()) {
|
|
117
|
+
if ((current as any).isWeekday()) {
|
|
118
|
+
businessDays++;
|
|
119
|
+
}
|
|
120
|
+
current = current.addDays(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return isReversed ? -businessDays : businessDays;
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
);
|