@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.
@@ -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
+ );