shelving 1.163.0 → 1.165.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/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.163.0",
14
+ "version": "1.165.0",
15
15
  "repository": "https://github.com/dhoulb/shelving",
16
16
  "author": "Dave Houlbrooke <dave@shax.com>",
17
17
  "license": "0BSD",
@@ -1,5 +1,6 @@
1
1
  import type { Data, Database, PartialData } from "../util/data.js";
2
2
  import type { Identifier, Item } from "../util/item.js";
3
+ import { type Key } from "../util/object.js";
3
4
  import type { NullableSchema } from "./NullableSchema.js";
4
5
  import type { SchemaOptions, Schemas } from "./Schema.js";
5
6
  import { Schema } from "./Schema.js";
@@ -15,6 +16,8 @@ export declare class DataSchema<T extends Data> extends Schema<unknown> {
15
16
  readonly props: Schemas<T>;
16
17
  constructor({ one, title, props, value: partialValue, ...options }: DataSchemaOptions<T>);
17
18
  validate(unsafeValue?: unknown): T;
19
+ pick<K extends Key<T>>(...keys: K[]): DataSchema<Pick<T, K>>;
20
+ omit<K extends Key<T>>(...keys: K[]): DataSchema<Omit<T, K>>;
18
21
  }
19
22
  /** Set of named data schemas. */
20
23
  export type DataSchemas<T extends Database> = {
@@ -1,5 +1,6 @@
1
1
  import { ValueFeedback } from "../feedback/Feedback.js";
2
2
  import { isData } from "../util/data.js";
3
+ import { omitProps, pickProps } from "../util/object.js";
3
4
  import { mapProps } from "../util/transform.js";
4
5
  import { validateData } from "../util/validate.js";
5
6
  import { NULLABLE } from "./NullableSchema.js";
@@ -19,6 +20,12 @@ export class DataSchema extends Schema {
19
20
  throw new ValueFeedback("Must be object", unsafeValue);
20
21
  return validateData(unsafeValue, this.props);
21
22
  }
23
+ pick(...keys) {
24
+ return new DataSchema({ ...this, props: pickProps(this.props, ...keys) });
25
+ }
26
+ omit(...keys) {
27
+ return new DataSchema({ ...this, props: omitProps(this.props, ...keys) });
28
+ }
22
29
  }
23
30
  function _getSchemaValue([, { value }]) {
24
31
  return value;
package/util/date.d.ts CHANGED
@@ -35,6 +35,12 @@ export declare function getYesterday(): Date;
35
35
  export declare function getToday(): Date;
36
36
  /** Get a date representing midnight of the next day. */
37
37
  export declare function getTomorrow(): Date;
38
+ /** Get a Date representing exactly midnight of the specified date. */
39
+ export declare function getMidnight(target?: PossibleDate, caller?: AnyCaller): Date;
40
+ /** Get a Date representing midnight on Monday of the specified week. */
41
+ export declare function getMonday(target?: PossibleDate, caller?: AnyCaller): Date;
42
+ /** Get a Date representing the first day of the specified month. */
43
+ export declare function getMonthStart(target?: PossibleDate, caller?: AnyCaller): Date;
38
44
  /**
39
45
  * Convert a possible date to a `Date` instance, or throw `RequiredError` if it couldn't be converted.
40
46
  * @param value Any value that we want to parse as a valid date (defaults to `"now"`).
@@ -57,47 +63,31 @@ export declare function getTimeString(value?: unknown): string | undefined;
57
63
  /** Convert a possible `Date` instance to local time string like "18:32:00", or throw `RequiredError` if it couldn't be converted. */
58
64
  export declare function requireTimeString(value?: PossibleDate, caller?: AnyCaller): string;
59
65
  /** List of day-of-week strings. */
60
- export declare const DAYS: readonly ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
66
+ export declare const DAYS: readonly ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
61
67
  /** Type listing day-of-week strings. */
62
68
  export type Day = (typeof DAYS)[number];
63
- /** Convert a `Date` instance to a day-of-week string like "monday" */
69
+ /** Convert a `Date` instance to a day-of-week string like "Monday" */
64
70
  export declare function getDay(target?: PossibleDate): Day;
65
- /** Get a Date representing exactly midnight of the specified date. */
66
- export declare function getMidnight(target?: PossibleDate, caller?: AnyCaller): Date;
67
- /** Get a Date representing midnight on Monday of the specified week. */
68
- export declare function getMonday(target?: PossibleDate, caller?: AnyCaller): Date;
69
- /** Return a new date that increase or decreases the number of days based on an input date. */
70
- export declare function addDays(change: number, target?: PossibleDate): Date;
71
- /** Return a new date that increase or decreases the number of hours based on an input date. */
72
- export declare function addHours(change: number, target?: PossibleDate): Date;
73
71
  /**
74
- * Get the duration (in milliseconds) between two dates.
75
- *
76
- * @param target The date when the thing will happen or did happen.
77
- * @param current Today's date (or a different date to measure from).
72
+ * Return a new date that increase or decreases the month based on an input date.
73
+ * - February 29th is a special cased and is _rounded down_ to February 28th on non-leap years.
74
+ */
75
+ export declare function addYears(change: number, target?: PossibleDate, caller?: AnyCaller): Date;
76
+ /**
77
+ * Return a new date that increase or decreases the month based on an input date.
78
+ * - Note that with Javascript "rollover" semantics, adding a month when we're on e.g. 31st of August would normally roll _past_ September and return 1st October.
79
+ * - To avoid this we clamp the date to the end of the month if rollover happens.
78
80
  */
79
- export declare function getMillisecondsUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
80
- /** Count the number of seconds until a date. */
81
- export declare function getSecondsUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
82
- /** Count the number of days ago a date was. */
83
- export declare function getSecondsAgo(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
84
- /** Count the number of days until a date. */
85
- export declare function getDaysUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
86
- /** Count the number of days ago a date was. */
87
- export declare function getDaysAgo(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
88
- /** Count the number of weeks until a date. */
89
- export declare function getWeeksUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
90
- /** Count the number of weeks ago a date was. */
91
- export declare function getWeeksAgo(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
92
- /** Is a date in the past? */
93
- export declare function isPast(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): boolean;
94
- /** Is a date in the future? */
95
- export declare function isFuture(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): boolean;
96
- /** Is a date today (taking into account midnight). */
97
- export declare function isToday(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): boolean;
98
- /** Compact when a date happens/happened, e.g. `in 10d` or `2h ago` or `in 1w` or `just now` */
99
- export declare function formatWhen(target: PossibleDate, current?: PossibleDate, options?: Intl.NumberFormatOptions): string;
100
- /** Compact when a date happens, e.g. `10d` or `2h` or `-1w` */
101
- export declare function formatUntil(target: PossibleDate, current?: PossibleDate, options?: Intl.NumberFormatOptions): string;
102
- /** Compact when a date will happen, e.g. `10d` or `2h` or `-1w` */
103
- export declare function formatAgo(target: PossibleDate, current?: PossibleDate, options?: Intl.NumberFormatOptions): string;
81
+ export declare function addMonths(change: number, target?: PossibleDate, caller?: AnyCaller): Date;
82
+ /** Return a new date that increase or decreases the week based on an input date. */
83
+ export declare function addWeeks(change: number, target?: PossibleDate, caller?: AnyCaller): Date;
84
+ /** Return a new date that increase or decreases the day based on an input date. */
85
+ export declare function addDays(change: number, target?: PossibleDate, caller?: AnyCaller): Date;
86
+ /** Return a new date that increase or decreases the hour based on an input date. */
87
+ export declare function addHours(change: number, target?: PossibleDate, caller?: AnyCaller): Date;
88
+ /** Return a new date that increase or decreases the minute based on an input date. */
89
+ export declare function addMinutes(change: number, target?: PossibleDate, caller?: AnyCaller): Date;
90
+ /** Return a new date that increase or decreases the minute based on an input date. */
91
+ export declare function addSeconds(change: number, target?: PossibleDate, caller?: AnyCaller): Date;
92
+ /** Return a new date that increase or decreases the minute based on an input date. */
93
+ export declare function addMilliseconds(change: number, target?: PossibleDate, caller?: AnyCaller): Date;
package/util/date.js CHANGED
@@ -1,6 +1,4 @@
1
1
  import { RequiredError } from "../error/RequiredError.js";
2
- import { DAY, HOUR, MONTH, SECOND, WEEK } from "./constants.js";
3
- import { TIME_UNITS } from "./units.js";
4
2
  /**
5
3
  * Is a value a valid date?
6
4
  * - Note: `Date` instances can be invalid (e.g. `new Date("blah blah").getTime()` returns `NaN`). These are detected and will always return `false`
@@ -75,6 +73,28 @@ export function getTomorrow() {
75
73
  date.setDate(date.getDate() + 1);
76
74
  return date;
77
75
  }
76
+ /** Get a Date representing exactly midnight of the specified date. */
77
+ export function getMidnight(target, caller = getMidnight) {
78
+ const date = new Date(requireDate(target, caller));
79
+ date.setHours(0, 0, 0, 0);
80
+ return date;
81
+ }
82
+ /** Get a Date representing midnight on Monday of the specified week. */
83
+ export function getMonday(target, caller = getMonday) {
84
+ const date = getMidnight(target, caller);
85
+ const day = date.getDay();
86
+ if (day === 0)
87
+ date.setDate(date.getDate() - 6);
88
+ else if (day !== 1)
89
+ date.setDate(date.getDate() - (day - 1));
90
+ return date;
91
+ }
92
+ /** Get a Date representing the first day of the specified month. */
93
+ export function getMonthStart(target, caller = getMonthStart) {
94
+ const date = getMidnight(target, caller);
95
+ date.setDate(1);
96
+ return date;
97
+ }
78
98
  /**
79
99
  * Convert a possible date to a `Date` instance, or throw `RequiredError` if it couldn't be converted.
80
100
  * @param value Any value that we want to parse as a valid date (defaults to `"now"`).
@@ -136,121 +156,69 @@ export function requireTimeString(value, caller = requireTimeString) {
136
156
  return _time(requireDate(value, caller));
137
157
  }
138
158
  /** List of day-of-week strings. */
139
- export const DAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
140
- /** Convert a `Date` instance to a day-of-week string like "monday" */
159
+ export const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
160
+ /** Convert a `Date` instance to a day-of-week string like "Monday" */
141
161
  export function getDay(target) {
142
162
  return DAYS[requireDate(target, getDay).getDay()];
143
163
  }
144
- /** Get a Date representing exactly midnight of the specified date. */
145
- export function getMidnight(target, caller = getMidnight) {
146
- const date = new Date(requireDate(target, caller)); // New instance, because we modify it.
147
- date.setHours(0, 0, 0, 0);
148
- return date;
164
+ /**
165
+ * Return a new date that increase or decreases the month based on an input date.
166
+ * - February 29th is a special cased and is _rounded down_ to February 28th on non-leap years.
167
+ */
168
+ export function addYears(change, target, caller = addYears) {
169
+ const input = requireDate(target, caller);
170
+ const output = new Date(input);
171
+ output.setFullYear(output.getFullYear() + change);
172
+ if (input.getMonth() !== output.getMonth())
173
+ output.setDate(0); // Handle February 29th case.
174
+ return output;
149
175
  }
150
- /** Get a Date representing midnight on Monday of the specified week. */
151
- export function getMonday(target, caller = getMonday) {
152
- const date = getMidnight(target, caller); // New instance, because we modify it.
153
- const day = date.getDay();
154
- if (day === 0)
155
- date.setDate(date.getDate() - 6);
156
- else if (day !== 1)
157
- date.setDate(date.getDate() - (day - 1));
176
+ /**
177
+ * Return a new date that increase or decreases the month based on an input date.
178
+ * - Note that with Javascript "rollover" semantics, adding a month when we're on e.g. 31st of August would normally roll _past_ September and return 1st October.
179
+ * - To avoid this we clamp the date to the end of the month if rollover happens.
180
+ */
181
+ export function addMonths(change, target, caller = addMonths) {
182
+ const input = requireDate(target, caller);
183
+ const output = new Date(input);
184
+ output.setMonth(output.getMonth() + change);
185
+ if (input.getMonth() !== output.getMonth() + change)
186
+ output.setDate(0); // Handle 31st rollover case.
187
+ return output;
188
+ }
189
+ /** Return a new date that increase or decreases the week based on an input date. */
190
+ export function addWeeks(change, target, caller = addWeeks) {
191
+ const date = new Date(requireDate(target, caller));
192
+ date.setDate(date.getDate() + change * 7);
158
193
  return date;
159
194
  }
160
- /** Return a new date that increase or decreases the number of days based on an input date. */
161
- export function addDays(change, target) {
162
- const date = new Date(requireDate(target, addDays)); // New instance, because we modify it.
195
+ /** Return a new date that increase or decreases the day based on an input date. */
196
+ export function addDays(change, target, caller = addDays) {
197
+ const date = new Date(requireDate(target, caller));
163
198
  date.setDate(date.getDate() + change);
164
199
  return date;
165
200
  }
166
- /** Return a new date that increase or decreases the number of hours based on an input date. */
167
- export function addHours(change, target) {
168
- const date = new Date(requireDate(target, addHours)); // New instance, because we modify it.
201
+ /** Return a new date that increase or decreases the hour based on an input date. */
202
+ export function addHours(change, target, caller = addHours) {
203
+ const date = new Date(requireDate(target, caller));
169
204
  date.setHours(date.getHours() + change);
170
205
  return date;
171
206
  }
172
- /**
173
- * Get the duration (in milliseconds) between two dates.
174
- *
175
- * @param target The date when the thing will happen or did happen.
176
- * @param current Today's date (or a different date to measure from).
177
- */
178
- export function getMillisecondsUntil(target, current, caller = getMillisecondsUntil) {
179
- return requireDate(target, caller).getTime() - requireDate(current, caller).getTime();
180
- }
181
- /** Count the number of seconds until a date. */
182
- export function getSecondsUntil(target, current, caller = getSecondsUntil) {
183
- return getMillisecondsUntil(target, current, caller) / SECOND;
184
- }
185
- /** Count the number of days ago a date was. */
186
- export function getSecondsAgo(target, current, caller = getSecondsAgo) {
187
- return 0 - getSecondsUntil(target, current, caller);
188
- }
189
- /** Count the number of days until a date. */
190
- export function getDaysUntil(target, current, caller = getDaysUntil) {
191
- return Math.round((requireDate(target, caller).getTime() - requireDate(current, caller).getTime()) / DAY);
192
- }
193
- /** Count the number of days ago a date was. */
194
- export function getDaysAgo(target, current, caller = getDaysAgo) {
195
- return 0 - getDaysUntil(target, current, caller);
196
- }
197
- /** Count the number of weeks until a date. */
198
- export function getWeeksUntil(target, current, caller = getWeeksUntil) {
199
- return Math.floor(getDaysUntil(target, current, caller) / 7);
200
- }
201
- /** Count the number of weeks ago a date was. */
202
- export function getWeeksAgo(target, current, caller = getWeeksAgo) {
203
- return 0 - getWeeksUntil(target, current, caller);
204
- }
205
- /** Is a date in the past? */
206
- export function isPast(target, current, caller = isPast) {
207
- return getMillisecondsUntil(target, current, caller) < 0;
208
- }
209
- /** Is a date in the future? */
210
- export function isFuture(target, current, caller = isFuture) {
211
- return getMillisecondsUntil(target, current, caller) > 0;
212
- }
213
- /** Is a date today (taking into account midnight). */
214
- export function isToday(target, current, caller = isToday) {
215
- return getDaysUntil(target, current, caller) === 0;
216
- }
217
- /** Get an appropriate time unit based on an amount in milliseconds. */
218
- function getBestTimeUnit(ms) {
219
- const abs = Math.abs(ms);
220
- if (abs > 18 * MONTH)
221
- return TIME_UNITS.require("year");
222
- if (abs > 10 * WEEK)
223
- return TIME_UNITS.require("month");
224
- if (abs > 2 * WEEK)
225
- return TIME_UNITS.require("week");
226
- if (abs > DAY)
227
- return TIME_UNITS.require("day");
228
- if (abs > HOUR)
229
- return TIME_UNITS.require("hour");
230
- if (abs > 9949)
231
- return TIME_UNITS.require("minute");
232
- if (abs > SECOND)
233
- return TIME_UNITS.require("second");
234
- return TIME_UNITS.require("millisecond");
235
- }
236
- /** Compact when a date happens/happened, e.g. `in 10d` or `2h ago` or `in 1w` or `just now` */
237
- export function formatWhen(target, current, options) {
238
- const ms = getMillisecondsUntil(target, current, formatWhen);
239
- const abs = Math.abs(ms);
240
- if (abs < 30 * SECOND)
241
- return "just now";
242
- const unit = getBestTimeUnit(ms);
243
- return ms > 0 ? `in ${unit.format(unit.from(abs), options)}` : `${unit.format(unit.from(abs), options)} ago`;
244
- }
245
- /** Compact when a date happens, e.g. `10d` or `2h` or `-1w` */
246
- export function formatUntil(target, current, options) {
247
- const ms = getMillisecondsUntil(target, current, formatUntil);
248
- const unit = getBestTimeUnit(ms);
249
- return unit.format(unit.from(ms), options);
250
- }
251
- /** Compact when a date will happen, e.g. `10d` or `2h` or `-1w` */
252
- export function formatAgo(target, current, options) {
253
- const ms = 0 - getMillisecondsUntil(target, current, formatAgo);
254
- const unit = getBestTimeUnit(ms);
255
- return unit.format(unit.from(ms), options);
207
+ /** Return a new date that increase or decreases the minute based on an input date. */
208
+ export function addMinutes(change, target, caller = addMinutes) {
209
+ const date = new Date(requireDate(target, caller));
210
+ date.setMinutes(date.getMinutes() + change);
211
+ return date;
212
+ }
213
+ /** Return a new date that increase or decreases the minute based on an input date. */
214
+ export function addSeconds(change, target, caller = addSeconds) {
215
+ const date = new Date(requireDate(target, caller));
216
+ date.setSeconds(date.getSeconds() + change);
217
+ return date;
218
+ }
219
+ /** Return a new date that increase or decreases the minute based on an input date. */
220
+ export function addMilliseconds(change, target, caller = addMilliseconds) {
221
+ const date = new Date(requireDate(target, caller));
222
+ date.setMilliseconds(date.getMilliseconds() + change);
223
+ return date;
256
224
  }
@@ -0,0 +1,120 @@
1
+ import { type PossibleDate } from "./date.js";
2
+ import type { AnyCaller } from "./function.js";
3
+ import { type TimeUnitKey, type Unit } from "./units.js";
4
+ /**
5
+ * Duration object.
6
+ * - This should be compatible with `Intl.DurationFormat` when that is available.
7
+ */
8
+ export type Duration = {
9
+ readonly years?: number | undefined;
10
+ readonly months?: number | undefined;
11
+ readonly weeks?: number | undefined;
12
+ readonly days?: number | undefined;
13
+ readonly hours?: number | undefined;
14
+ readonly minutes?: number | undefined;
15
+ readonly seconds?: number | undefined;
16
+ readonly milliseconds?: number | undefined;
17
+ readonly microseconds?: number | undefined;
18
+ readonly nanoseconds?: number | undefined;
19
+ };
20
+ /** Get the millisecond difference between two dates. */
21
+ export declare function getMilliseconds(from?: PossibleDate, to?: PossibleDate, caller?: AnyCaller): number;
22
+ /** Count the various time units between two dates and return a `Duration` format. */
23
+ export declare function getDuration(from?: PossibleDate, to?: PossibleDate, caller?: AnyCaller): Duration;
24
+ /** Get the various time units until a certain date. */
25
+ export declare function getUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): Duration;
26
+ /** Get the various time units since a certain date. */
27
+ export declare function getAgo(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): Duration;
28
+ /** Count the milliseconds until a date. */
29
+ export declare function getMillisecondsUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
30
+ /** Count the milliseconds since a date. */
31
+ export declare function getMillisecondsAgo(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
32
+ /**
33
+ * Count the whole seconds until a date.
34
+ * - Rounds to the nearest whole second, i.e. `1 second 499 ms` returns `1`
35
+ */
36
+ export declare function getSecondsUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
37
+ /**
38
+ * Count the whole seconds since a date.
39
+ * - Rounds to the nearest whole second, i.e. `1 second 499 ms` returns `1`
40
+ */
41
+ export declare function getSecondsAgo(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
42
+ /**
43
+ * Count the whole minutes until a date.
44
+ * - Rounds to the nearest whole minute, i.e. `1 min 29 seconds` returns `1`
45
+ */
46
+ export declare function getMinutesUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
47
+ /**
48
+ * Count the whole minutes since a date.
49
+ * - Rounds to the nearest whole minute, i.e. `1 min 29 seconds` returns `1`
50
+ */
51
+ export declare function getMinutesAgo(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
52
+ /**
53
+ * Count the whole hours until a date.
54
+ * - Rounds to the nearest whole hour, i.e. `1 hour 29 minutes` returns `1`
55
+ */
56
+ export declare function getHoursUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
57
+ /**
58
+ * Count the whole hours since a date.
59
+ * - Rounds to the nearest whole hour, i.e. `1 hour 29 minutes` returns `1`
60
+ */
61
+ export declare function getHoursAgo(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
62
+ /**
63
+ * Count the calendar days until a date.
64
+ * - e.g. from 23:59 to 00:01 is 1 day, even though it's only 1 minutes.
65
+ */
66
+ export declare function getDaysUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
67
+ /**
68
+ * Count the calendar days since a date.
69
+ * - Rounds to the nearest whole days.
70
+ */
71
+ export declare function getDaysAgo(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
72
+ /**
73
+ * Count the whole weeks until a date.
74
+ * - Rounds down to the nearest whole week, i.e.
75
+ */
76
+ export declare function getWeeksUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
77
+ /**
78
+ * Count the whole weeks since a date.
79
+ * - Rounds to the nearest whole week.
80
+ */
81
+ export declare function getWeeksAgo(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
82
+ /**
83
+ * Count the calendar months until a date.
84
+ * - e.g. from March 31st to April 1st is 1 month, even though it's only 1 day.
85
+ */
86
+ export declare function getMonthsUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
87
+ /**
88
+ * Count the calendar months since a date.
89
+ * - e.g. from March 31st to April 1st is 1 month, even though it's only 1 day.
90
+ */
91
+ export declare function getMonthsAgo(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
92
+ /**
93
+ * Count the calendar years until a date.
94
+ * - e.g. from December 31st to January 1st is 1 year, even though it's only 1 day.
95
+ */
96
+ export declare function getYearsUntil(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
97
+ /**
98
+ * Count the calendar years since a date.
99
+ * - Note this counts calendar years, not 365-day periods.
100
+ * - e.g. from December 31st to January 1st is -1 years, even though it's only 1 day.
101
+ */
102
+ export declare function getYearsAgo(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): number;
103
+ /** Is a date in the past? */
104
+ export declare function isPast(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): boolean;
105
+ /** Is a date in the future? */
106
+ export declare function isFuture(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): boolean;
107
+ /** Is a date today (taking into account midnight). */
108
+ export declare function isToday(target: PossibleDate, current?: PossibleDate, caller?: AnyCaller): boolean;
109
+ /**
110
+ * Get a best-fit time unit based on an amount in milliseconds.
111
+ * - Makes a sensible choice about the best time unit to use.
112
+ * - Years will be used for anything 18 months or more, e.g. `in 2 years`
113
+ * - Months will be used for anything 10 weeks or more, e.g. `in 14 months`
114
+ * - Weeks will be used for anything 2 weeks or more, e.g. `in 9 weeks`
115
+ * - Days will be used for anything 24 hours or more, e.g. `in 13 days`
116
+ * - Hours will be used for anything 90 minutes or more, e.g. `in 23 hours`
117
+ * - Minutes will be used for anything 1 second or more, e.g. `1 minute ago` or `in 59 minutes`
118
+ * - Seconds will be used for anything 1000 milliseconds or more, e.g. `in 59 seconds`
119
+ */
120
+ export declare function getBestTimeUnit(ms: number): Unit<TimeUnitKey>;
@@ -0,0 +1,179 @@
1
+ import { DAY, HOUR, MINUTE, MONTH, SECOND, WEEK, YEAR } from "./constants.js";
2
+ import { getMidnight, requireDate } from "./date.js";
3
+ import { TIME_UNITS } from "./units.js";
4
+ /** Get the millisecond difference between two dates. */
5
+ export function getMilliseconds(from, to, caller = getMilliseconds) {
6
+ return requireDate(to, caller).getTime() - requireDate(from, caller).getTime();
7
+ }
8
+ /** Count the various time units between two dates and return a `Duration` format. */
9
+ export function getDuration(from, to, caller = getDuration) {
10
+ const ms = getMilliseconds(from, to, caller);
11
+ return {
12
+ years: Math.trunc(ms / YEAR),
13
+ months: Math.trunc((ms % YEAR) / MONTH),
14
+ weeks: Math.trunc((ms % MONTH) / WEEK),
15
+ days: Math.trunc((ms % WEEK) / DAY),
16
+ hours: Math.trunc((ms % DAY) / HOUR),
17
+ minutes: Math.trunc((ms % HOUR) / MINUTE),
18
+ seconds: Math.trunc((ms % MINUTE) / SECOND),
19
+ milliseconds: Math.trunc((ms % SECOND) / 1),
20
+ };
21
+ }
22
+ /** Get the various time units until a certain date. */
23
+ export function getUntil(target, current = "now", caller = getUntil) {
24
+ return getDuration(current, target, caller);
25
+ }
26
+ /** Get the various time units since a certain date. */
27
+ export function getAgo(target, current = "now", caller = getAgo) {
28
+ return getDuration(target, current, caller);
29
+ }
30
+ /** Count the milliseconds until a date. */
31
+ export function getMillisecondsUntil(target, current, caller = getMillisecondsUntil) {
32
+ return getMilliseconds(current, target, caller);
33
+ }
34
+ /** Count the milliseconds since a date. */
35
+ export function getMillisecondsAgo(target, current, caller = getMillisecondsAgo) {
36
+ return 0 - getMillisecondsUntil(target, current, caller);
37
+ }
38
+ /**
39
+ * Count the whole seconds until a date.
40
+ * - Rounds to the nearest whole second, i.e. `1 second 499 ms` returns `1`
41
+ */
42
+ export function getSecondsUntil(target, current, caller = getSecondsUntil) {
43
+ return Math.round(getMilliseconds(current, target, caller) / SECOND);
44
+ }
45
+ /**
46
+ * Count the whole seconds since a date.
47
+ * - Rounds to the nearest whole second, i.e. `1 second 499 ms` returns `1`
48
+ */
49
+ export function getSecondsAgo(target, current, caller = getSecondsAgo) {
50
+ return 0 - getSecondsUntil(target, current, caller);
51
+ }
52
+ /**
53
+ * Count the whole minutes until a date.
54
+ * - Rounds to the nearest whole minute, i.e. `1 min 29 seconds` returns `1`
55
+ */
56
+ export function getMinutesUntil(target, current, caller = getMinutesUntil) {
57
+ return Math.round(getMilliseconds(current, target, caller) / MINUTE);
58
+ }
59
+ /**
60
+ * Count the whole minutes since a date.
61
+ * - Rounds to the nearest whole minute, i.e. `1 min 29 seconds` returns `1`
62
+ */
63
+ export function getMinutesAgo(target, current, caller = getMinutesAgo) {
64
+ return 0 - getMinutesUntil(target, current, caller);
65
+ }
66
+ /**
67
+ * Count the whole hours until a date.
68
+ * - Rounds to the nearest whole hour, i.e. `1 hour 29 minutes` returns `1`
69
+ */
70
+ export function getHoursUntil(target, current, caller = getHoursUntil) {
71
+ return Math.round(getMilliseconds(current, target, caller) / HOUR);
72
+ }
73
+ /**
74
+ * Count the whole hours since a date.
75
+ * - Rounds to the nearest whole hour, i.e. `1 hour 29 minutes` returns `1`
76
+ */
77
+ export function getHoursAgo(target, current, caller = getHoursAgo) {
78
+ return 0 - getHoursUntil(target, current, caller);
79
+ }
80
+ /**
81
+ * Count the calendar days until a date.
82
+ * - e.g. from 23:59 to 00:01 is 1 day, even though it's only 1 minutes.
83
+ */
84
+ export function getDaysUntil(target, current, caller = getDaysUntil) {
85
+ return Math.round((getMidnight(target, caller).getTime() - getMidnight(current, caller).getTime()) / DAY);
86
+ }
87
+ /**
88
+ * Count the calendar days since a date.
89
+ * - Rounds to the nearest whole days.
90
+ */
91
+ export function getDaysAgo(target, current, caller = getDaysAgo) {
92
+ return 0 - getDaysUntil(target, current, caller);
93
+ }
94
+ /**
95
+ * Count the whole weeks until a date.
96
+ * - Rounds down to the nearest whole week, i.e.
97
+ */
98
+ export function getWeeksUntil(target, current, caller = getWeeksUntil) {
99
+ return Math.trunc(getDaysUntil(target, current, caller) / 7);
100
+ }
101
+ /**
102
+ * Count the whole weeks since a date.
103
+ * - Rounds to the nearest whole week.
104
+ */
105
+ export function getWeeksAgo(target, current, caller = getWeeksAgo) {
106
+ return 0 - getWeeksUntil(target, current, caller);
107
+ }
108
+ /**
109
+ * Count the calendar months until a date.
110
+ * - e.g. from March 31st to April 1st is 1 month, even though it's only 1 day.
111
+ */
112
+ export function getMonthsUntil(target, current, caller = getMonthsUntil) {
113
+ const t = requireDate(target, caller);
114
+ const c = requireDate(current, caller);
115
+ const years = t.getFullYear() - c.getFullYear();
116
+ const months = t.getMonth() - c.getMonth();
117
+ return years * 12 + months;
118
+ }
119
+ /**
120
+ * Count the calendar months since a date.
121
+ * - e.g. from March 31st to April 1st is 1 month, even though it's only 1 day.
122
+ */
123
+ export function getMonthsAgo(target, current, caller = getMonthsAgo) {
124
+ return 0 - getMonthsUntil(target, current, caller);
125
+ }
126
+ /**
127
+ * Count the calendar years until a date.
128
+ * - e.g. from December 31st to January 1st is 1 year, even though it's only 1 day.
129
+ */
130
+ export function getYearsUntil(target, current, caller = getYearsUntil) {
131
+ return requireDate(target, caller).getFullYear() - requireDate(current, caller).getFullYear();
132
+ }
133
+ /**
134
+ * Count the calendar years since a date.
135
+ * - Note this counts calendar years, not 365-day periods.
136
+ * - e.g. from December 31st to January 1st is -1 years, even though it's only 1 day.
137
+ */
138
+ export function getYearsAgo(target, current, caller = getYearsAgo) {
139
+ return 0 - getYearsUntil(target, current, caller);
140
+ }
141
+ /** Is a date in the past? */
142
+ export function isPast(target, current, caller = isPast) {
143
+ return getMilliseconds(current, target, caller) < 0;
144
+ }
145
+ /** Is a date in the future? */
146
+ export function isFuture(target, current, caller = isFuture) {
147
+ return getMilliseconds(current, target, caller) > 0;
148
+ }
149
+ /** Is a date today (taking into account midnight). */
150
+ export function isToday(target, current, caller = isToday) {
151
+ return getDaysUntil(target, current, caller) === 0;
152
+ }
153
+ /**
154
+ * Get a best-fit time unit based on an amount in milliseconds.
155
+ * - Makes a sensible choice about the best time unit to use.
156
+ * - Years will be used for anything 18 months or more, e.g. `in 2 years`
157
+ * - Months will be used for anything 10 weeks or more, e.g. `in 14 months`
158
+ * - Weeks will be used for anything 2 weeks or more, e.g. `in 9 weeks`
159
+ * - Days will be used for anything 24 hours or more, e.g. `in 13 days`
160
+ * - Hours will be used for anything 90 minutes or more, e.g. `in 23 hours`
161
+ * - Minutes will be used for anything 1 second or more, e.g. `1 minute ago` or `in 59 minutes`
162
+ * - Seconds will be used for anything 1000 milliseconds or more, e.g. `in 59 seconds`
163
+ */
164
+ export function getBestTimeUnit(ms) {
165
+ const abs = Math.abs(ms);
166
+ if (abs > 18 * MONTH)
167
+ return TIME_UNITS.require("year");
168
+ if (abs > 10 * WEEK)
169
+ return TIME_UNITS.require("month");
170
+ if (abs > DAY)
171
+ return TIME_UNITS.require("day");
172
+ if (abs > HOUR)
173
+ return TIME_UNITS.require("hour");
174
+ if (abs > MINUTE * 90)
175
+ return TIME_UNITS.require("minute");
176
+ if (abs > SECOND)
177
+ return TIME_UNITS.require("second");
178
+ return TIME_UNITS.require("millisecond");
179
+ }
package/util/format.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type ImmutableArray } from "./array.js";
2
2
  import { type PossibleDate } from "./date.js";
3
+ import { type Duration } from "./duration.js";
3
4
  import { type ImmutableObject } from "./object.js";
4
5
  import { type PossibleURL } from "./url.js";
5
6
  /** Options we use for number formatting. */
@@ -87,3 +88,27 @@ export declare function formatURL(possible: PossibleURL, base?: PossibleURL): st
87
88
  * - Everything else returns `"Unknown"`
88
89
  */
89
90
  export declare function formatValue(value: unknown): string;
91
+ /**
92
+ * Compact best-fit when a date happens/happened, e.g. `in 10d` or `2h ago` or `in 1w` or `just now`
93
+ * - See `getBestTimeUnit()` for details on how the best-fit unit is chosen.
94
+ * - But: anything under 30 seconds will show `just now`, which makes more sense in most UIs.
95
+ */
96
+ export declare function formatWhen(target: PossibleDate, current?: PossibleDate, options?: Intl.NumberFormatOptions): string;
97
+ /** Compact when a date happens, e.g. `10d` or `2h` or `-1w` */
98
+ export declare function formatUntil(target: PossibleDate, current?: PossibleDate, options?: Intl.NumberFormatOptions): string;
99
+ /** Compact when a date will happen, e.g. `10d` or `2h` or `-1w` */
100
+ export declare function formatAgo(target: PossibleDate, current?: PossibleDate, options?: Intl.NumberFormatOptions): string;
101
+ /**
102
+ * This roughly corresponds to `Intl.DurationFormatOptions`
103
+ * @todo Use `Intl.DurationFormatOptions` instead it's available in TS lib.
104
+ */
105
+ interface DurationFormatOptions {
106
+ style?: "short" | "long" | "narrow";
107
+ }
108
+ /**
109
+ * Format a duration as a string, e.g. `1 year, 2 months, 3 days` or `1y 2m 3d`
110
+ * @todo Use `Intl.DurationFormat().format()` instead it's more widely supported and is available in TS lib.
111
+ */
112
+ export declare function formatDuration(duration: Duration, options?: DurationFormatOptions): string;
113
+ export declare function _getDurationStrings(duration: Duration, options?: Intl.NumberFormatOptions): Iterable<string>;
114
+ export {};
package/util/format.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { isArray } from "./array.js";
2
+ import { SECOND } from "./constants.js";
2
3
  import { isDate, requireDate } from "./date.js";
4
+ import { getBestTimeUnit, getMilliseconds } from "./duration.js";
3
5
  import { getPercent } from "./number.js";
4
6
  import { isObject } from "./object.js";
7
+ import { TIME_UNITS } from "./units.js";
5
8
  import { requireURL } from "./url.js";
6
9
  /** Format a number (based on the user's browser language settings). */
7
10
  export function formatNumber(num, options) {
@@ -145,3 +148,46 @@ export function formatValue(value) {
145
148
  return formatObject(value);
146
149
  return "Unknown";
147
150
  }
151
+ /**
152
+ * Compact best-fit when a date happens/happened, e.g. `in 10d` or `2h ago` or `in 1w` or `just now`
153
+ * - See `getBestTimeUnit()` for details on how the best-fit unit is chosen.
154
+ * - But: anything under 30 seconds will show `just now`, which makes more sense in most UIs.
155
+ */
156
+ export function formatWhen(target, current, options) {
157
+ const ms = getMilliseconds(current, target, formatWhen);
158
+ const abs = Math.abs(ms);
159
+ if (abs < 30 * SECOND)
160
+ return "just now";
161
+ const unit = getBestTimeUnit(ms);
162
+ return ms > 0 ? `in ${unit.format(unit.from(abs), options)}` : `${unit.format(unit.from(abs), options)} ago`;
163
+ }
164
+ /** Compact when a date happens, e.g. `10d` or `2h` or `-1w` */
165
+ export function formatUntil(target, current, options) {
166
+ const ms = getMilliseconds(current, target, formatUntil);
167
+ const unit = getBestTimeUnit(ms);
168
+ return unit.format(unit.from(ms), options);
169
+ }
170
+ /** Compact when a date will happen, e.g. `10d` or `2h` or `-1w` */
171
+ export function formatAgo(target, current, options) {
172
+ const ms = getMilliseconds(target, current, formatAgo);
173
+ const unit = getBestTimeUnit(ms);
174
+ return unit.format(unit.from(ms), options);
175
+ }
176
+ /**
177
+ * Format a duration as a string, e.g. `1 year, 2 months, 3 days` or `1y 2m 3d`
178
+ * @todo Use `Intl.DurationFormat().format()` instead it's more widely supported and is available in TS lib.
179
+ */
180
+ export function formatDuration(duration, options) {
181
+ // Map `DurationFormatOptions` to `NumberFormatOptions`
182
+ const style = options?.style ?? "short";
183
+ return new Intl.ListFormat(undefined, { style, type: "unit" }).format(_getDurationStrings(duration, { ...options, style: "unit", unitDisplay: style }));
184
+ }
185
+ export function* _getDurationStrings(duration, options) {
186
+ for (const key of TIME_KEYS) {
187
+ const value = duration[`${key}s`];
188
+ if (typeof value === "number" && value !== 0)
189
+ yield TIME_UNITS.require(key)?.format(value, options);
190
+ }
191
+ }
192
+ // Keys we loop through in the right order.
193
+ const TIME_KEYS = ["year", "month", "week", "day", "hour", "minute", "second", "millisecond"];
package/util/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export * from "./debug.js";
16
16
  export * from "./dictionary.js";
17
17
  export * from "./diff.js";
18
18
  export * from "./dispose.js";
19
+ export * from "./duration.js";
19
20
  export * from "./entity.js";
20
21
  export * from "./entry.js";
21
22
  export * from "./equal.js";
package/util/index.js CHANGED
@@ -16,6 +16,7 @@ export * from "./debug.js";
16
16
  export * from "./dictionary.js";
17
17
  export * from "./diff.js";
18
18
  export * from "./dispose.js";
19
+ export * from "./duration.js";
19
20
  export * from "./entity.js";
20
21
  export * from "./entry.js";
21
22
  export * from "./equal.js";
@@ -26,6 +26,9 @@ export declare function getPlaceholders(template: string): readonly string[];
26
26
  *
27
27
  * @param templates Either a single template string, or an iterator that returns multiple template template strings.
28
28
  * - Template strings can include placeholders (e.g. `:name-${country}/{city}`).
29
+ * - Template strings do not match the `/` character.
30
+ * - The `**` double splat placeholder _can_ match multiple path segments (e.g. `a/b/c`).
31
+ *
29
32
  * @param target The string containing values, e.g. `Dave-UK/Manchester`
30
33
  *
31
34
  * @return An object containing values, e.g. `{ name: "Dave", country: "UK", city: "Manchester" }`, or undefined if target didn't match the template.
package/util/template.js CHANGED
@@ -5,7 +5,7 @@ import { setMapItem } from "./map.js";
5
5
  import { isObject } from "./object.js";
6
6
  import { getString } from "./string.js";
7
7
  // RegExp to find named variables in several formats e.g. `:a`, `${b}`, `{{c}}` or `{d}`
8
- const R_PLACEHOLDERS = /(\*|:[a-z][a-z0-9]*|\$\{[a-z][a-z0-9]*\}|\{\{[a-z][a-z0-9]*\}\}|\{[a-z][a-z0-9]*\})/i;
8
+ const R_PLACEHOLDERS = /(\*\*?|:[a-z][a-z0-9]*|\$\{[a-z][a-z0-9]*\}|\{\{[a-z][a-z0-9]*\}\}|\{[a-z][a-z0-9]*\})/i;
9
9
  // Find actual name within template placeholder e.g. `${name}` → `name`
10
10
  const R_NAME = /[a-z0-9]+/i;
11
11
  /**
@@ -26,7 +26,7 @@ function _splitTemplate(template, caller) {
26
26
  const post = matches[i + 1];
27
27
  if (i > 1 && !pre.length)
28
28
  throw new ValueError("Template placeholders must be separated by at least one character", { received: template, caller });
29
- const name = placeholder === "*" ? String(asterisks++) : R_NAME.exec(placeholder)?.[0] || "";
29
+ const name = placeholder[0] === "*" ? String(asterisks++) : R_NAME.exec(placeholder)?.[0] || "";
30
30
  chunks.push({ pre, placeholder, name, post });
31
31
  }
32
32
  return chunks;
@@ -53,6 +53,9 @@ function _getPlaceholder({ name }) {
53
53
  *
54
54
  * @param templates Either a single template string, or an iterator that returns multiple template template strings.
55
55
  * - Template strings can include placeholders (e.g. `:name-${country}/{city}`).
56
+ * - Template strings do not match the `/` character.
57
+ * - The `**` double splat placeholder _can_ match multiple path segments (e.g. `a/b/c`).
58
+ *
56
59
  * @param target The string containing values, e.g. `Dave-UK/Manchester`
57
60
  *
58
61
  * @return An object containing values, e.g. `{ name: "Dave", country: "UK", city: "Manchester" }`, or undefined if target didn't match the template.
@@ -70,13 +73,15 @@ export function matchTemplate(template, target, caller = matchTemplate) {
70
73
  // Loop through the placeholders (placeholders are at all the even-numbered positions in `chunks`).
71
74
  let startIndex = firstChunk.pre.length;
72
75
  const values = {};
73
- for (const { name, post } of chunks) {
76
+ for (const { name, post, placeholder } of chunks) {
74
77
  const stopIndex = !post ? Number.POSITIVE_INFINITY : target.indexOf(post, startIndex);
75
78
  if (stopIndex < 0)
76
79
  return undefined; // Target doesn't match template because chunk post wasn't found.
77
80
  const value = target.slice(startIndex, stopIndex);
78
81
  if (!value.length)
79
82
  return undefined; // Target doesn't match template because chunk value was missing.
83
+ if (placeholder !== "**" && value.includes("/"))
84
+ return undefined; // Placeholders can't consume multiple path segments.
80
85
  values[name] = value;
81
86
  startIndex = stopIndex + post.length;
82
87
  }
package/util/url.js CHANGED
@@ -4,6 +4,17 @@ import { isData } from "./data.js";
4
4
  import { notNullish } from "./null.js";
5
5
  import { getProps } from "./object.js";
6
6
  import { getString, isString } from "./string.js";
7
+ function parseURL(value, base) {
8
+ const ctor = URL;
9
+ if (typeof ctor.parse === "function")
10
+ return ctor.parse(value, base);
11
+ try {
12
+ return new URL(value, base);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
7
18
  /** Is an unknown value a URL object? */
8
19
  export function isURL(value) {
9
20
  return value instanceof URL;
@@ -16,7 +27,7 @@ export function assertURL(value, caller = assertURL) {
16
27
  /** Convert a possible URL to a URL, or return `undefined` if conversion fails. */
17
28
  export function getURL(possible, base = _BASE) {
18
29
  if (notNullish(possible))
19
- return isURL(possible) ? possible : URL.parse(possible, base) || undefined;
30
+ return isURL(possible) ? possible : parseURL(possible, base) || undefined;
20
31
  }
21
32
  const _BASE = typeof document === "object" ? document.baseURI : undefined;
22
33
  /** Convert a possible URL to a URL, or throw `RequiredError` if conversion fails. */