@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,104 @@
|
|
|
1
|
+
import type { DateTimeClass, DateTimePlugin } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper function to create a DateTime plugin
|
|
5
|
+
*
|
|
6
|
+
* @param name - Unique plugin name
|
|
7
|
+
* @param install - Installation function that extends the DateTime class
|
|
8
|
+
* @returns A DateTimePlugin object
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const myPlugin = createPlugin("myPlugin", (DateTimeClass, options) => {
|
|
13
|
+
* // Extend DateTime prototype
|
|
14
|
+
* DateTimeClass.prototype.myMethod = function() {
|
|
15
|
+
* return this.valueOf();
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* // Add static methods
|
|
19
|
+
* DateTimeClass.myStaticMethod = function() {
|
|
20
|
+
* return new DateTimeClass();
|
|
21
|
+
* };
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Register the plugin
|
|
25
|
+
* DateTime.extend(myPlugin);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function createPlugin(
|
|
29
|
+
name: string,
|
|
30
|
+
install: (
|
|
31
|
+
DateTimeClass: DateTimeClass,
|
|
32
|
+
options?: Record<string, unknown>,
|
|
33
|
+
) => void,
|
|
34
|
+
): DateTimePlugin {
|
|
35
|
+
if (!name || typeof name !== "string") {
|
|
36
|
+
throw new Error("Plugin name must be a non-empty string");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof install !== "function") {
|
|
40
|
+
throw new Error("Plugin install must be a function");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
name,
|
|
45
|
+
install,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Type guard to check if an object is a valid DateTimePlugin
|
|
51
|
+
*
|
|
52
|
+
* @param obj - Object to check
|
|
53
|
+
* @returns true if object is a valid DateTimePlugin
|
|
54
|
+
*/
|
|
55
|
+
export function isPlugin(obj: unknown): obj is DateTimePlugin {
|
|
56
|
+
if (!obj || typeof obj !== "object") {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const plugin = obj as Partial<DateTimePlugin>;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
typeof plugin.name === "string" &&
|
|
64
|
+
plugin.name.length > 0 &&
|
|
65
|
+
typeof plugin.install === "function"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validates that a plugin name follows naming conventions
|
|
71
|
+
*
|
|
72
|
+
* @param name - Plugin name to validate
|
|
73
|
+
* @returns true if name is valid, false otherwise
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* Valid plugin names:
|
|
77
|
+
* - Must be a non-empty string
|
|
78
|
+
* - Should use kebab-case or camelCase
|
|
79
|
+
* - Should not contain spaces or special characters (except hyphens)
|
|
80
|
+
* - Should be descriptive and unique
|
|
81
|
+
*/
|
|
82
|
+
export function isValidPluginName(name: string): boolean {
|
|
83
|
+
if (!name || typeof name !== "string") {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check for valid characters (alphanumeric, hyphens, underscores)
|
|
88
|
+
const validNamePattern = /^[a-zA-Z0-9-_]+$/;
|
|
89
|
+
if (!validNamePattern.test(name)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check minimum length
|
|
94
|
+
if (name.length < 2) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Should not start or end with special characters
|
|
99
|
+
if (/^[-_]|[-_]$/.test(name)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { DateTime } from "../../core/datetime";
|
|
2
|
+
import type { DateTimeClass } from "../../types";
|
|
3
|
+
import { createPlugin } from "../plugin-base";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Time thresholds for relative time formatting (in seconds)
|
|
7
|
+
*/
|
|
8
|
+
const THRESHOLDS = {
|
|
9
|
+
second: 45, // 0-45 seconds
|
|
10
|
+
minute: 90, // 45-90 seconds
|
|
11
|
+
hour: 2700, // 45 minutes - 1.5 hours
|
|
12
|
+
day: 75600, // 21 hours threshold for switching to days
|
|
13
|
+
month: 2592000, // ~30 days
|
|
14
|
+
year: 31536000, // ~365 days
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Formats a relative time string
|
|
19
|
+
* @param diff - Time difference in seconds
|
|
20
|
+
* @param isPast - Whether the time is in the past
|
|
21
|
+
* @returns Human-readable relative time string
|
|
22
|
+
*/
|
|
23
|
+
function formatRelativeTime(diff: number, isPast: boolean): string {
|
|
24
|
+
const absDiff = Math.abs(diff);
|
|
25
|
+
|
|
26
|
+
// Handle zero difference
|
|
27
|
+
if (absDiff === 0 || absDiff < THRESHOLDS.second) {
|
|
28
|
+
return isPast || diff === 0 ? "a few seconds ago" : "in a few seconds";
|
|
29
|
+
} else if (absDiff < THRESHOLDS.minute) {
|
|
30
|
+
return isPast ? "a minute ago" : "in a minute";
|
|
31
|
+
} else if (absDiff < THRESHOLDS.hour) {
|
|
32
|
+
const value = Math.round(absDiff / 60);
|
|
33
|
+
const unit = value === 1 ? "minute" : "minutes";
|
|
34
|
+
return isPast ? `${value} ${unit} ago` : `in ${value} ${unit}`;
|
|
35
|
+
} else if (absDiff < THRESHOLDS.day) {
|
|
36
|
+
const value = Math.round(absDiff / 3600);
|
|
37
|
+
if (value === 1) {
|
|
38
|
+
return isPast ? "an hour ago" : "in an hour";
|
|
39
|
+
}
|
|
40
|
+
const unit = "hours";
|
|
41
|
+
return isPast ? `${value} ${unit} ago` : `in ${value} ${unit}`;
|
|
42
|
+
} else if (absDiff < THRESHOLDS.month) {
|
|
43
|
+
const value = Math.round(absDiff / 86400);
|
|
44
|
+
if (value === 1) {
|
|
45
|
+
return isPast ? "a day ago" : "in a day";
|
|
46
|
+
}
|
|
47
|
+
const unit = "days";
|
|
48
|
+
return isPast ? `${value} ${unit} ago` : `in ${value} ${unit}`;
|
|
49
|
+
} else if (absDiff < THRESHOLDS.year) {
|
|
50
|
+
const value = Math.round(absDiff / 2592000);
|
|
51
|
+
if (value === 1) {
|
|
52
|
+
return isPast ? "a month ago" : "in a month";
|
|
53
|
+
}
|
|
54
|
+
const unit = "months";
|
|
55
|
+
return isPast ? `${value} ${unit} ago` : `in ${value} ${unit}`;
|
|
56
|
+
} else {
|
|
57
|
+
const value = Math.round(absDiff / 31536000);
|
|
58
|
+
if (value === 1) {
|
|
59
|
+
return isPast ? "a year ago" : "in a year";
|
|
60
|
+
}
|
|
61
|
+
const unit = "years";
|
|
62
|
+
return isPast ? `${value} ${unit} ago` : `in ${value} ${unit}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extended DateTime interface with relative time methods
|
|
68
|
+
*/
|
|
69
|
+
declare module "../../core/datetime" {
|
|
70
|
+
interface DateTime {
|
|
71
|
+
/**
|
|
72
|
+
* Returns a human-readable string representing the time from now
|
|
73
|
+
* @returns Relative time string (e.g., "2 hours ago", "in 3 days")
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* dt.fromNow() // "2 hours ago"
|
|
78
|
+
* dt.fromNow() // "in 3 days"
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
fromNow(): string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns a human-readable string representing the time to now
|
|
85
|
+
* Opposite of fromNow()
|
|
86
|
+
* @returns Relative time string
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* dt.toNow() // "in 2 hours"
|
|
91
|
+
* dt.toNow() // "3 days ago"
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
toNow(): string;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns a human-readable string representing the time from another DateTime
|
|
98
|
+
* @param other - DateTime instance to compare against
|
|
99
|
+
* @returns Relative time string
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* dt1.from(dt2) // "2 hours ago"
|
|
104
|
+
* dt1.from(dt2) // "in 3 days"
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
from(other: DateTime): string;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns a human-readable string representing the time to another DateTime
|
|
111
|
+
* Opposite of from()
|
|
112
|
+
* @param other - DateTime instance to compare against
|
|
113
|
+
* @returns Relative time string
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* dt1.to(dt2) // "in 2 hours"
|
|
118
|
+
* dt1.to(dt2) // "3 days ago"
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
to(other: DateTime): string;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Relative Time plugin for DateTime
|
|
127
|
+
* Adds methods for human-readable relative time formatting
|
|
128
|
+
*/
|
|
129
|
+
export const relativeTimePlugin = createPlugin(
|
|
130
|
+
"relative-time",
|
|
131
|
+
(DateTimeClass: DateTimeClass) => {
|
|
132
|
+
// Add instance methods
|
|
133
|
+
DateTimeClass.prototype.fromNow = function (): string {
|
|
134
|
+
const now = new DateTimeClass();
|
|
135
|
+
const diffMs = this.valueOf() - now.valueOf();
|
|
136
|
+
const diffSeconds = diffMs / 1000;
|
|
137
|
+
const isPast = diffSeconds < 0;
|
|
138
|
+
|
|
139
|
+
return formatRelativeTime(diffSeconds, isPast);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
DateTimeClass.prototype.toNow = function (): string {
|
|
143
|
+
const now = new DateTimeClass();
|
|
144
|
+
const diffMs = this.valueOf() - now.valueOf();
|
|
145
|
+
const diffSeconds = diffMs / 1000;
|
|
146
|
+
const isPast = diffSeconds < 0;
|
|
147
|
+
|
|
148
|
+
// Reverse the direction for toNow
|
|
149
|
+
return formatRelativeTime(diffSeconds, !isPast);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
DateTimeClass.prototype.from = function (other: DateTime): string {
|
|
153
|
+
const diffMs = this.valueOf() - other.valueOf();
|
|
154
|
+
const diffSeconds = diffMs / 1000;
|
|
155
|
+
const isPast = diffSeconds < 0;
|
|
156
|
+
|
|
157
|
+
return formatRelativeTime(diffSeconds, isPast);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
DateTimeClass.prototype.to = function (other: DateTime): string {
|
|
161
|
+
const diffMs = this.valueOf() - other.valueOf();
|
|
162
|
+
const diffSeconds = diffMs / 1000;
|
|
163
|
+
const isPast = diffSeconds < 0;
|
|
164
|
+
|
|
165
|
+
// Reverse the direction for to
|
|
166
|
+
return formatRelativeTime(diffSeconds, !isPast);
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { DateTime } from "../../core/datetime";
|
|
3
|
+
import { relativeTimePlugin } from "./index";
|
|
4
|
+
|
|
5
|
+
describe("Relative Time Plugin", () => {
|
|
6
|
+
// Install plugin before running tests
|
|
7
|
+
DateTime.extend(relativeTimePlugin);
|
|
8
|
+
|
|
9
|
+
// Use a fixed reference time for testing
|
|
10
|
+
const referenceTime = new DateTime("2024-01-15T12:00:00.000Z");
|
|
11
|
+
|
|
12
|
+
describe("from()", () => {
|
|
13
|
+
it("should return 'a few seconds ago' for recent past", () => {
|
|
14
|
+
const dt = new DateTime("2024-01-15T11:59:55.000Z"); // 5 seconds before
|
|
15
|
+
expect((dt as any).from(referenceTime)).toBe("a few seconds ago");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should return 'a minute ago' for 1 minute", () => {
|
|
19
|
+
const dt = new DateTime("2024-01-15T11:59:00.000Z");
|
|
20
|
+
expect((dt as any).from(referenceTime)).toBe("a minute ago");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should return 'X minutes ago' for multiple minutes", () => {
|
|
24
|
+
const dt = new DateTime("2024-01-15T11:45:00.000Z"); // 15 minutes before
|
|
25
|
+
expect((dt as any).from(referenceTime)).toBe("15 minutes ago");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return 'an hour ago' for 1 hour", () => {
|
|
29
|
+
const dt = new DateTime("2024-01-15T11:00:00.000Z");
|
|
30
|
+
expect((dt as any).from(referenceTime)).toBe("an hour ago");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should return 'X hours ago' for multiple hours", () => {
|
|
34
|
+
const dt = new DateTime("2024-01-15T09:00:00.000Z"); // 3 hours before
|
|
35
|
+
expect((dt as any).from(referenceTime)).toBe("3 hours ago");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should return 'a day ago' for 1 day", () => {
|
|
39
|
+
const dt = new DateTime("2024-01-14T12:00:00.000Z");
|
|
40
|
+
expect((dt as any).from(referenceTime)).toBe("a day ago");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should return 'X days ago' for multiple days", () => {
|
|
44
|
+
const dt = new DateTime("2024-01-10T12:00:00.000Z"); // 5 days before
|
|
45
|
+
expect((dt as any).from(referenceTime)).toBe("5 days ago");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should return 'a month ago' for 1 month", () => {
|
|
49
|
+
const dt = new DateTime("2023-12-15T12:00:00.000Z");
|
|
50
|
+
expect((dt as any).from(referenceTime)).toBe("a month ago");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should return 'X months ago' for multiple months", () => {
|
|
54
|
+
const dt = new DateTime("2023-10-15T12:00:00.000Z"); // 3 months before
|
|
55
|
+
expect((dt as any).from(referenceTime)).toBe("3 months ago");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should return 'a year ago' for 1 year", () => {
|
|
59
|
+
const dt = new DateTime("2023-01-15T12:00:00.000Z");
|
|
60
|
+
expect((dt as any).from(referenceTime)).toBe("a year ago");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should return 'X years ago' for multiple years", () => {
|
|
64
|
+
const dt = new DateTime("2021-01-15T12:00:00.000Z"); // 3 years before
|
|
65
|
+
expect((dt as any).from(referenceTime)).toBe("3 years ago");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should return 'in a few seconds' for near future", () => {
|
|
69
|
+
const dt = new DateTime("2024-01-15T12:00:05.000Z"); // 5 seconds after
|
|
70
|
+
expect((dt as any).from(referenceTime)).toBe("in a few seconds");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should return 'in a minute' for 1 minute future", () => {
|
|
74
|
+
const dt = new DateTime("2024-01-15T12:01:00.000Z");
|
|
75
|
+
expect((dt as any).from(referenceTime)).toBe("in a minute");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should return 'in X minutes' for multiple minutes future", () => {
|
|
79
|
+
const dt = new DateTime("2024-01-15T12:15:00.000Z"); // 15 minutes after
|
|
80
|
+
expect((dt as any).from(referenceTime)).toBe("in 15 minutes");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should return 'in an hour' for 1 hour future", () => {
|
|
84
|
+
const dt = new DateTime("2024-01-15T13:00:00.000Z");
|
|
85
|
+
expect((dt as any).from(referenceTime)).toBe("in an hour");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should return 'in X hours' for multiple hours future", () => {
|
|
89
|
+
const dt = new DateTime("2024-01-15T15:00:00.000Z"); // 3 hours after
|
|
90
|
+
expect((dt as any).from(referenceTime)).toBe("in 3 hours");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should return 'in a day' for 1 day future", () => {
|
|
94
|
+
const dt = new DateTime("2024-01-16T12:00:00.000Z");
|
|
95
|
+
expect((dt as any).from(referenceTime)).toBe("in a day");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should return 'in X days' for multiple days future", () => {
|
|
99
|
+
const dt = new DateTime("2024-01-20T12:00:00.000Z"); // 5 days after
|
|
100
|
+
expect((dt as any).from(referenceTime)).toBe("in 5 days");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should handle same time", () => {
|
|
104
|
+
const dt1 = new DateTime("2024-01-15T12:00:00.000Z");
|
|
105
|
+
const dt2 = new DateTime("2024-01-15T12:00:00.000Z");
|
|
106
|
+
expect((dt1 as any).from(dt2)).toBe("a few seconds ago");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should handle days calculation", () => {
|
|
110
|
+
const dt1 = new DateTime("2024-01-15T12:00:00.000Z");
|
|
111
|
+
const dt2 = new DateTime("2024-01-10T12:00:00.000Z");
|
|
112
|
+
expect((dt2 as any).from(dt1)).toBe("5 days ago");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("to()", () => {
|
|
117
|
+
it("should return opposite of from() for past", () => {
|
|
118
|
+
const dt = new DateTime("2024-01-15T11:00:00.000Z"); // 1 hour before
|
|
119
|
+
expect((dt as any).to(referenceTime)).toBe("in an hour");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should return opposite of from() for future", () => {
|
|
123
|
+
const dt = new DateTime("2024-01-15T13:00:00.000Z"); // 1 hour after
|
|
124
|
+
expect((dt as any).to(referenceTime)).toBe("an hour ago");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("fromNow() and toNow()", () => {
|
|
129
|
+
it("should return a string for fromNow()", () => {
|
|
130
|
+
const dt = new DateTime("2024-01-01T12:00:00.000Z");
|
|
131
|
+
const result = (dt as any).fromNow();
|
|
132
|
+
expect(typeof result).toBe("string");
|
|
133
|
+
expect(result.length).toBeGreaterThan(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should return a string for toNow()", () => {
|
|
137
|
+
const dt = new DateTime("2024-01-01T12:00:00.000Z");
|
|
138
|
+
const result = (dt as any).toNow();
|
|
139
|
+
expect(typeof result).toBe("string");
|
|
140
|
+
expect(result.length).toBeGreaterThan(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("fromNow and toNow should be opposites", () => {
|
|
144
|
+
const dt = new DateTime("2024-01-01T12:00:00.000Z");
|
|
145
|
+
const fromResult = (dt as any).fromNow();
|
|
146
|
+
const toResult = (dt as any).toNow();
|
|
147
|
+
|
|
148
|
+
// One should contain "ago" and the other "in", or both could be "a few seconds"
|
|
149
|
+
const hasAgo = fromResult.includes("ago");
|
|
150
|
+
const hasIn = toResult.includes("in");
|
|
151
|
+
expect(hasAgo || hasIn || fromResult.includes("few seconds")).toBe(
|
|
152
|
+
true,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("Chaining", () => {
|
|
158
|
+
it("should work with other DateTime methods", () => {
|
|
159
|
+
const dt = new DateTime("2024-01-15T11:00:00.000Z");
|
|
160
|
+
const relative = (dt.addHours(1) as any).from(referenceTime);
|
|
161
|
+
expect(relative).toBe("a few seconds ago");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { DateTime } from "../../core/datetime";
|
|
2
|
+
import type { DateTimeClass } from "../../types";
|
|
3
|
+
import { createPlugin } from "../plugin-base";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates a timezone string using Intl.DateTimeFormat
|
|
7
|
+
* @param timezone - IANA timezone string to validate
|
|
8
|
+
* @returns true if valid, false otherwise
|
|
9
|
+
*/
|
|
10
|
+
function isValidTimezone(timezone: string): boolean {
|
|
11
|
+
if (!timezone || typeof timezone !== "string") {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Use Intl.DateTimeFormat to validate timezone
|
|
17
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone });
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets the system's local timezone
|
|
26
|
+
* @returns IANA timezone string for local timezone
|
|
27
|
+
*/
|
|
28
|
+
function getLocalTimezone(): string {
|
|
29
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extended DateTime interface with timezone methods
|
|
34
|
+
*/
|
|
35
|
+
declare module "../../core/datetime" {
|
|
36
|
+
interface DateTime {
|
|
37
|
+
/**
|
|
38
|
+
* Sets the timezone for this DateTime instance
|
|
39
|
+
* @param timezone - IANA timezone string (e.g., "America/New_York")
|
|
40
|
+
* @returns New DateTime instance with timezone set
|
|
41
|
+
*/
|
|
42
|
+
tz(timezone: string): DateTime;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Converts this DateTime to a different timezone
|
|
46
|
+
* @param timezone - IANA timezone string
|
|
47
|
+
* @returns New DateTime instance in the specified timezone
|
|
48
|
+
*/
|
|
49
|
+
toTimezone(timezone: string): DateTime;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Converts this DateTime to UTC timezone
|
|
53
|
+
* @returns New DateTime instance in UTC
|
|
54
|
+
*/
|
|
55
|
+
utc(): DateTime;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Converts this DateTime to local system timezone
|
|
59
|
+
* @returns New DateTime instance in local timezone
|
|
60
|
+
*/
|
|
61
|
+
local(): DateTime;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Gets the current timezone of this DateTime instance
|
|
65
|
+
* @returns IANA timezone string
|
|
66
|
+
*/
|
|
67
|
+
getTimezone(): string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Internal timezone storage
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
_timezone?: string;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Extended DateTimeClass interface with static timezone method
|
|
79
|
+
*/
|
|
80
|
+
declare module "../../types" {
|
|
81
|
+
interface DateTimeClass {
|
|
82
|
+
/**
|
|
83
|
+
* Creates a DateTime instance in a specific timezone
|
|
84
|
+
* @param input - Date input
|
|
85
|
+
* @param timezone - IANA timezone string
|
|
86
|
+
* @returns DateTime instance in the specified timezone
|
|
87
|
+
*/
|
|
88
|
+
tz(input: any, timezone: string): DateTime;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Timezone plugin for DateTime
|
|
94
|
+
* Adds timezone support using IANA timezone strings
|
|
95
|
+
*/
|
|
96
|
+
export const timezonePlugin = createPlugin(
|
|
97
|
+
"timezone",
|
|
98
|
+
(DateTimeClass: DateTimeClass) => {
|
|
99
|
+
// Add instance methods
|
|
100
|
+
DateTimeClass.prototype.tz = function (timezone: string): DateTime {
|
|
101
|
+
if (!isValidTimezone(timezone)) {
|
|
102
|
+
throw new Error(`Invalid timezone: ${timezone}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Create new instance with same timestamp but different timezone
|
|
106
|
+
const newInstance = new DateTimeClass(this.valueOf());
|
|
107
|
+
(newInstance as any)._timezone = timezone;
|
|
108
|
+
return newInstance;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
DateTimeClass.prototype.toTimezone = function (
|
|
112
|
+
timezone: string,
|
|
113
|
+
): DateTime {
|
|
114
|
+
if (!isValidTimezone(timezone)) {
|
|
115
|
+
throw new Error(`Invalid timezone: ${timezone}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Convert to the new timezone (same as tz for now, as we preserve moment in time)
|
|
119
|
+
const newInstance = new DateTimeClass(this.valueOf());
|
|
120
|
+
(newInstance as any)._timezone = timezone;
|
|
121
|
+
return newInstance;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
DateTimeClass.prototype.utc = function (): DateTime {
|
|
125
|
+
const newInstance = new DateTimeClass(this.valueOf());
|
|
126
|
+
(newInstance as any)._timezone = "UTC";
|
|
127
|
+
return newInstance;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
DateTimeClass.prototype.local = function (): DateTime {
|
|
131
|
+
const localTz = getLocalTimezone();
|
|
132
|
+
const newInstance = new DateTimeClass(this.valueOf());
|
|
133
|
+
(newInstance as any)._timezone = localTz;
|
|
134
|
+
return newInstance;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
DateTimeClass.prototype.getTimezone = function (): string {
|
|
138
|
+
return (this as any)._timezone || "UTC";
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Add static method
|
|
142
|
+
(DateTimeClass as any).tz = (input: any, timezone: string): DateTime => {
|
|
143
|
+
if (!isValidTimezone(timezone)) {
|
|
144
|
+
throw new Error(`Invalid timezone: ${timezone}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const instance = new DateTimeClass(input);
|
|
148
|
+
(instance as any)._timezone = timezone;
|
|
149
|
+
return instance;
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
);
|