shelving 1.83.4 → 1.84.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.83.4",
14
+ "version": "1.84.0",
15
15
  "repository": "https://github.com/dhoulb/shelving",
16
16
  "author": "Dave Houlbrooke <dave@shax.com>",
17
17
  "license": "0BSD",
@@ -1,6 +1,6 @@
1
1
  import { PossibleDate } from "../util/date.js";
2
2
  import { Schema, SchemaOptions } from "./Schema.js";
3
- /** Define a valid date, e.g. `2005-09-12` */
3
+ /** Define a valid date in YMD format, e.g. `2005-09-12` */
4
4
  export declare class DateSchema extends Schema<string> {
5
5
  readonly value: PossibleDate;
6
6
  readonly min: PossibleDate | null;
@@ -2,7 +2,7 @@ import { getOptionalDate, getYmd } from "../util/date.js";
2
2
  import { InvalidFeedback } from "../feedback/InvalidFeedback.js";
3
3
  import { Schema } from "./Schema.js";
4
4
  import { OPTIONAL } from "./OptionalSchema.js";
5
- /** Define a valid date, e.g. `2005-09-12` */
5
+ /** Define a valid date in YMD format, e.g. `2005-09-12` */
6
6
  export class DateSchema extends Schema {
7
7
  constructor({ value = "now", min = null, max = null, ...options }) {
8
8
  super(options);
@@ -0,0 +1,14 @@
1
+ import { PossibleTime } from "../util/time.js";
2
+ import { Schema, SchemaOptions } from "./Schema.js";
3
+ /** Define a valid time in 24h hh:mm:ss.fff format, e.g. `23:59` or `24:00 */
4
+ export declare class TimeSchema extends Schema<string> {
5
+ readonly value: PossibleTime;
6
+ constructor({ value, ...options }: SchemaOptions & {
7
+ readonly value?: PossibleTime;
8
+ });
9
+ validate(unsafeValue?: unknown): string;
10
+ }
11
+ /** Valid time, e.g. `2005-09-12` (required because falsy values are invalid). */
12
+ export declare const TIME: TimeSchema;
13
+ /** Valid time, e.g. `2005-09-12`, or `null` */
14
+ export declare const OPTIONAL_TIME: import("./OptionalSchema.js").OptionalSchema<string>;
@@ -0,0 +1,21 @@
1
+ import { InvalidFeedback } from "../feedback/InvalidFeedback.js";
2
+ import { getOptionalTime } from "../util/time.js";
3
+ import { OPTIONAL } from "./OptionalSchema.js";
4
+ import { Schema } from "./Schema.js";
5
+ /** Define a valid time in 24h hh:mm:ss.fff format, e.g. `23:59` or `24:00 */
6
+ export class TimeSchema extends Schema {
7
+ constructor({ value = "now", ...options }) {
8
+ super(options);
9
+ this.value = value;
10
+ }
11
+ validate(unsafeValue = this.value) {
12
+ const time = getOptionalTime(unsafeValue);
13
+ if (!time)
14
+ throw new InvalidFeedback(unsafeValue ? "Invalid time" : "Required", { value: unsafeValue });
15
+ return time.long;
16
+ }
17
+ }
18
+ /** Valid time, e.g. `2005-09-12` (required because falsy values are invalid). */
19
+ export const TIME = new TimeSchema({});
20
+ /** Valid time, e.g. `2005-09-12`, or `null` */
21
+ export const OPTIONAL_TIME = OPTIONAL(TIME);
package/schema/index.d.ts CHANGED
@@ -10,9 +10,10 @@ export * from "./EmailSchema.js";
10
10
  export * from "./KeySchema.js";
11
11
  export * from "./LinkSchema.js";
12
12
  export * from "./NumberSchema.js";
13
- export * from "./OptionalSchema.js";
14
13
  export * from "./PhoneSchema.js";
15
- export * from "./RequiredSchema.js";
16
14
  export * from "./SlugSchema.js";
17
15
  export * from "./StringSchema.js";
16
+ export * from "./TimeSchema.js";
18
17
  export * from "./ThroughSchema.js";
18
+ export * from "./OptionalSchema.js";
19
+ export * from "./RequiredSchema.js";
package/schema/index.js CHANGED
@@ -10,9 +10,10 @@ export * from "./EmailSchema.js";
10
10
  export * from "./KeySchema.js";
11
11
  export * from "./LinkSchema.js";
12
12
  export * from "./NumberSchema.js";
13
- export * from "./OptionalSchema.js";
14
13
  export * from "./PhoneSchema.js";
15
- export * from "./RequiredSchema.js";
16
14
  export * from "./SlugSchema.js";
17
15
  export * from "./StringSchema.js";
16
+ export * from "./TimeSchema.js";
18
17
  export * from "./ThroughSchema.js";
18
+ export * from "./OptionalSchema.js";
19
+ export * from "./RequiredSchema.js";
package/util/color.d.ts CHANGED
@@ -1,13 +1,17 @@
1
+ export declare const HEX3_REGEXP: RegExp;
2
+ export declare const HEX6_REGEXP: RegExp;
1
3
  /** Things that can be converted to a `Color` instance. */
2
4
  export declare type PossibleColor = Color | string;
3
5
  export declare type PossibleOptionalColor = PossibleColor | null;
4
6
  /** Represent a color. */
5
7
  export declare class Color {
8
+ /** Make a `Color` from a 3 or 6 or 8 byte hex string. */
9
+ static fromHex(str: string): Color | null;
6
10
  readonly r: number;
7
11
  readonly g: number;
8
12
  readonly b: number;
9
13
  readonly a: number;
10
- constructor(r?: number | string, g?: number | string, b?: number | string, a?: number | string);
14
+ constructor(r?: number, g?: number, b?: number, a?: number);
11
15
  /** Convert this color to a six or eight digit hex color. */
12
16
  get hex(): string;
13
17
  /** Convert this color to an `rgb()` string. */
@@ -16,19 +20,17 @@ export declare class Color {
16
20
  get rgba(): string;
17
21
  /** Get the sRGB luminance of this color. */
18
22
  get luminance(): number;
23
+ /** Is this color light. */
24
+ get isLight(): boolean;
25
+ /** Is this color dark. */
26
+ get isDark(): boolean;
19
27
  toString(): string;
20
28
  }
21
29
  /** Is an unknown value a `Color` instance. */
22
30
  export declare const isColor: (v: Color | unknown) => v is Color;
23
31
  /** Assert that an unknown value is a `Color` instance. */
24
32
  export declare function assertColor(v: Color | unknown): asserts v is Color;
25
- /** Convert a number or string to a color channel number that's within bounds (strings like `0a` or `ff` are parsed as hexadecimal). */
26
- export declare function getColorChannel(channel: number | string): number;
27
33
  /** Convert a possible color to a `Color` instance or `null` */
28
- export declare function getOptionalColor(possibleColor: unknown): Color | null;
34
+ export declare function getOptionalColor(possible: unknown): Color | null;
29
35
  /** Convert a possible color to a `Color` instance */
30
- export declare function getColor(possibleColor: PossibleColor): Color;
31
- /** Is a color light? */
32
- export declare const isLight: (input: PossibleColor) => boolean;
33
- /** Is a color dark? */
34
- export declare const isDark: (input: PossibleColor) => boolean;
36
+ export declare function getColor(possible: PossibleColor): Color;
package/util/color.js CHANGED
@@ -1,23 +1,29 @@
1
1
  import { AssertionError } from "../error/AssertionError.js";
2
- import { getBetween, roundNumber } from "./number.js";
2
+ import { boundNumber, roundNumber } from "./number.js";
3
3
  // Constants.
4
4
  const DARK = 140; // Anything with a luminance > 140 is considered light.
5
- const MIN = 0; // Maximum value of a channel.
6
- const MAX = 255; // Maximum value of a channel.
7
5
  // Regular expressions.
8
- const HEX3 = /^#?([0-F])([0-F])([0-F])$/i;
9
- const HEX6 = /^#?([0-F]{2})([0-F]{2})([0-F]{2})([0-F]{2})?$/i;
6
+ export const HEX3_REGEXP = /^#?([0-F])([0-F])([0-F])$/i;
7
+ export const HEX6_REGEXP = /^#?([0-F]{2})([0-F]{2})([0-F]{2})([0-F]{2})?$/i;
10
8
  /** Represent a color. */
11
9
  export class Color {
12
10
  constructor(r = 255, g = 255, b = 255, a = 255) {
13
- this.r = getColorChannel(r);
14
- this.g = getColorChannel(g);
15
- this.b = getColorChannel(b);
16
- this.a = getColorChannel(a);
11
+ this.r = boundNumber(r, 0, 255);
12
+ this.g = boundNumber(g, 0, 255);
13
+ this.b = boundNumber(b, 0, 255);
14
+ this.a = boundNumber(a, 0, 255);
15
+ }
16
+ /** Make a `Color` from a 3 or 6 or 8 byte hex string. */
17
+ static fromHex(str) {
18
+ const matches = (str.match(HEX3_REGEXP) || str.match(HEX6_REGEXP));
19
+ if (!matches)
20
+ return null;
21
+ const [, r, g, b, a] = matches;
22
+ return new Color(_parse(r), _parse(g), _parse(b), typeof a === "string" ? _parse(a) : undefined);
17
23
  }
18
24
  /** Convert this color to a six or eight digit hex color. */
19
25
  get hex() {
20
- return `#${_hex(this.r)}${_hex(this.g)}${_hex(this.b)}${this.a < MAX ? _hex(this.a) : ""}`;
26
+ return `#${_hex(this.r)}${_hex(this.g)}${_hex(this.b)}${this.a < 255 ? _hex(this.a) : ""}`;
21
27
  }
22
28
  /** Convert this color to an `rgb()` string. */
23
29
  get rgb() {
@@ -32,11 +38,19 @@ export class Color {
32
38
  // Green is the largest component of the luminence, etc.
33
39
  return Math.round(0.2126 * this.r + 0.7152 * this.g + 0.0722 * this.b);
34
40
  }
41
+ /** Is this color light. */
42
+ get isLight() {
43
+ return this.luminance > DARK;
44
+ }
45
+ /** Is this color dark. */
46
+ get isDark() {
47
+ return this.luminance <= DARK;
48
+ }
35
49
  toString() {
36
50
  return this.rgba;
37
51
  }
38
52
  }
39
- /** Convert number channel to a hex string (results will be unpredictable if number is outside 0-MAX). */
53
+ const _parse = (hex) => parseInt(hex.padStart(2, "00"), 16);
40
54
  const _hex = (channel) => channel.toString(16).padStart(2, "00");
41
55
  /** Is an unknown value a `Color` instance. */
42
56
  export const isColor = (v) => v instanceof Color;
@@ -45,35 +59,18 @@ export function assertColor(v) {
45
59
  if (!isColor(v))
46
60
  throw new AssertionError("Invalid color", v);
47
61
  }
48
- /** Convert a number or string to a color channel number that's within bounds (strings like `0a` or `ff` are parsed as hexadecimal). */
49
- export function getColorChannel(channel) {
50
- const num = typeof channel === "string" ? parseInt(channel.padStart(2, "00"), 16) : Math.round(channel);
51
- if (Number.isFinite(num))
52
- return getBetween(num, MIN, MAX);
53
- throw new AssertionError("Invalid color channel", channel);
54
- }
55
62
  /** Convert a possible color to a `Color` instance or `null` */
56
- export function getOptionalColor(possibleColor) {
57
- if (isColor(possibleColor))
58
- return possibleColor;
59
- if (typeof possibleColor === "string") {
60
- const hex3 = possibleColor.match(HEX3);
61
- if (hex3)
62
- return new Color(hex3[1], hex3[2], hex3[3]);
63
- const hex6 = possibleColor.match(HEX6);
64
- if (hex6)
65
- return new Color(hex6[1], hex6[2], hex6[3], hex6[4]);
66
- }
63
+ export function getOptionalColor(possible) {
64
+ if (isColor(possible))
65
+ return possible;
66
+ if (typeof possible === "string")
67
+ return Color.fromHex(possible);
67
68
  return null;
68
69
  }
69
70
  /** Convert a possible color to a `Color` instance */
70
- export function getColor(possibleColor) {
71
- const color = getOptionalColor(possibleColor);
71
+ export function getColor(possible) {
72
+ const color = getOptionalColor(possible);
72
73
  if (!color)
73
- throw new AssertionError("Invalid color", possibleColor);
74
+ throw new AssertionError("Invalid color", possible);
74
75
  return color;
75
76
  }
76
- /** Is a color light? */
77
- export const isLight = (input) => getColor(input).luminance > DARK;
78
- /** Is a color dark? */
79
- export const isDark = (input) => getColor(input).luminance <= DARK;
package/util/date.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** Things that converted to dates. */
2
- export declare type PossibleDate = Date | number | string | (() => PossibleDate);
2
+ export declare type PossibleDate = Date | number | string | (() => Date | number | string);
3
3
  /** Things that converted to dates or `null` */
4
- export declare type PossibleOptionalDate = Date | number | string | null | (() => PossibleDate | null);
4
+ export declare type PossibleOptionalDate = Date | number | string | null | (() => Date | number | string | null | null);
5
5
  /** Is a value a date? */
6
6
  export declare const isDate: (v: Date | unknown) => v is Date;
7
7
  /** Assert that a value is a `Date` instance. */
@@ -65,11 +65,13 @@ export declare const getDaysAgo: (target: PossibleDate, current?: PossibleDate)
65
65
  export declare const getWeeksUntil: (target: PossibleDate, current?: PossibleDate) => number;
66
66
  /** Count the number of weeks ago a date was. */
67
67
  export declare const getWeeksAgo: (target: PossibleDate, current?: PossibleDate) => number;
68
- /** Format a date in the browser locale. */
69
- export declare const formatDate: (date: PossibleDate) => string;
70
68
  /** Is a date in the past? */
71
69
  export declare const isPast: (target: PossibleDate, current?: PossibleDate) => boolean;
72
70
  /** Is a date in the future? */
73
71
  export declare const isFuture: (target: PossibleDate, current?: PossibleDate) => boolean;
74
72
  /** Is a date today (taking into account midnight). */
75
73
  export declare const isToday: (target: PossibleDate, current?: PossibleDate) => boolean;
74
+ /** Format a date in the browser locale. */
75
+ export declare const formatDate: (date: PossibleDate) => string;
76
+ /** Format an optional time as a string. */
77
+ export declare const formatOptionalDate: (date?: unknown) => string | null;
package/util/date.js CHANGED
@@ -58,8 +58,13 @@ export function getOptionalYmd(possibleDate) {
58
58
  }
59
59
  /** Convert a `Date` instance to a YMD string like "2015-09-12", or throw `AssertionError` if it couldn't be converted. */
60
60
  export function getYmd(possibleDate = "now") {
61
- return getDate(possibleDate).toISOString().slice(0, 10);
61
+ const date = getDate(possibleDate);
62
+ const y = _pad(date.getUTCFullYear(), 4);
63
+ const m = _pad(date.getUTCMonth() + 1, 2);
64
+ const d = _pad(date.getUTCDate(), 2);
65
+ return `${y}-${m}-${d}`;
62
66
  }
67
+ const _pad = (num, size) => num.toString(10).padStart(size, "0000");
63
68
  /** List of day-of-week strings. */
64
69
  export const days = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
65
70
  /** Convert a `Date` instance to a day-of-week string like "monday" */
@@ -67,10 +72,7 @@ export const getDay = (target) => days[getDate(target).getDay()];
67
72
  /** Get a Date representing exactly midnight of the specified date. */
68
73
  export function getMidnight(target) {
69
74
  const date = new Date(getDate(target)); // New instance, because we modify it.
70
- date.setHours(0);
71
- date.setMinutes(0);
72
- date.setSeconds(0);
73
- date.setMilliseconds(0);
75
+ date.setHours(0, 0, 0, 0);
74
76
  return date;
75
77
  }
76
78
  /** Get a Date representing midnight on Monday of the specified week. */
@@ -114,12 +116,13 @@ export const getDaysAgo = (target, current) => 0 - getDaysUntil(target, current)
114
116
  export const getWeeksUntil = (target, current) => Math.floor(getDaysUntil(target, current) / 7);
115
117
  /** Count the number of weeks ago a date was. */
116
118
  export const getWeeksAgo = (target, current) => 0 - getWeeksUntil(target, current);
117
- /** Format a date in the browser locale. */
118
- export const formatDate = (date) => _formatter.format(getDate(date));
119
- const _formatter = new Intl.DateTimeFormat(undefined, {});
120
119
  /** Is a date in the past? */
121
120
  export const isPast = (target, current) => getDate(target) < getDate(current);
122
121
  /** Is a date in the future? */
123
122
  export const isFuture = (target, current) => getDate(target) > getDate(current);
124
123
  /** Is a date today (taking into account midnight). */
125
124
  export const isToday = (target, current) => getMidnight(target) === getMidnight(current);
125
+ /** Format a date in the browser locale. */
126
+ export const formatDate = (date) => getDate(date).toLocaleDateString();
127
+ /** Format an optional time as a string. */
128
+ export const formatOptionalDate = (date) => { var _a; return ((_a = getOptionalDate(date)) === null || _a === void 0 ? void 0 : _a.toLocaleDateString()) || null; };
package/util/index.d.ts CHANGED
@@ -35,6 +35,7 @@ export * from "./sort.js";
35
35
  export * from "./source.js";
36
36
  export * from "./string.js";
37
37
  export * from "./template.js";
38
+ export * from "./time.js";
38
39
  export * from "./timeout.js";
39
40
  export * from "./transform.js";
40
41
  export * from "./undefined.js";
package/util/index.js CHANGED
@@ -35,6 +35,7 @@ export * from "./sort.js";
35
35
  export * from "./source.js";
36
36
  export * from "./string.js";
37
37
  export * from "./template.js";
38
+ export * from "./time.js";
38
39
  export * from "./timeout.js";
39
40
  export * from "./transform.js";
40
41
  export * from "./undefined.js";
package/util/number.d.ts CHANGED
@@ -3,6 +3,16 @@ export declare const isNumber: (v: unknown) => v is number;
3
3
  /** Assert that a value is a number. */
4
4
  export declare function assertNumber(v: number | unknown): asserts v is number;
5
5
  /** Assert that a value is a number greater than. */
6
+ export declare function assertFinite(v: number | unknown): asserts v is number;
7
+ /**
8
+ * Is a finite number within a specified range?
9
+ *
10
+ * @param num The number to test, e.g. `17`
11
+ * @param min The start of the range, e.g. `10`
12
+ * @param max The end of the range, e.g. `20`
13
+ */
14
+ export declare const isBetween: (num: number, min: number, max: number) => boolean;
15
+ /** Assert that a value is a number greater than. */
6
16
  export declare function assertBetween(v: number | unknown, min: number, max: number): asserts v is number;
7
17
  /** Assert that a value is a number greater than. */
8
18
  export declare function assertMax(v: number | unknown, max: number): asserts v is number;
@@ -54,20 +64,16 @@ export declare const roundNumber: (num: number, precision?: number) => number;
54
64
  * @returns The number truncated to the specified precision.
55
65
  */
56
66
  export declare const truncateNumber: (num: number, precision?: number) => number;
57
- /**
58
- * Format a number (based on the user's browser settings).
59
- *
60
- * @param num The number to format.
61
- * @param maxPrecision Maximum number of digits shown after the decimal point (defaults to 2).
62
- * @param minPrecision Minimum number of digits shown after the decimal point (defaults to 0).
63
- *
64
- * @returns The number formatted as a string in the browser's current locale.
65
- */
66
- export declare function formatNumber(num: number, maxPrecision?: number, minPrecision?: number): string;
67
+ /** Bound a number so it fits between two values. */
68
+ export declare function boundNumber(num: number, min: number, max: number): number;
69
+ /** Wrap a number so it fits between two values. */
70
+ export declare function wrapNumber(num: number, min: number, max: number): number;
71
+ /** Format a number (based on the user's browser language settings). */
72
+ export declare function formatNumber(num: number, precision?: number | null): string;
67
73
  /** Format a number with a short abbreviated suffix. */
68
- export declare const formatQuantity: (num: number, abbr: string, maxPrecision?: number, minPrecision?: number) => string;
74
+ export declare const formatQuantity: (num: number, abbr: string, precision?: number | null) => string;
69
75
  /** Format a number with a longer full-word suffix. */
70
- export declare function formatFullQuantity(num: number, singular: string, plural: string, maxPrecision?: number, minPrecision?: number): string;
76
+ export declare function formatFullQuantity(num: number, singular: string, plural: string, precision?: number | null): string;
71
77
  /**
72
78
  * Cram a large whole numbers into a space efficient format, e.g. `14.7M`
73
79
  * - Improves glanceability.
@@ -96,22 +102,6 @@ export declare function cramNumber(num: number): string;
96
102
  export declare const cramQuantity: (num: number, suffix: string) => string;
97
103
  /** Cram a number with a longer full-word suffix. */
98
104
  export declare function cramFullQuantity(num: number, singular: string, plural: string): string;
99
- /**
100
- * Is a number within a specified range?
101
- *
102
- * @param num The number to test, e.g. `17`
103
- * @param start The start of the range, e.g. `10`
104
- * @param end The end of the range, e.g. `20`
105
- */
106
- export declare const isBetween: (num: number, start: number, end: number) => boolean;
107
- /**
108
- * Apply a min/max to a number to return a number that's definitely in the specified range.
109
- *
110
- * @param num The number to apply the min/max to, e.g. `17`
111
- * @param start The start of the range, e.g. `10`
112
- * @param end The end of the range, e.g. `20`
113
- */
114
- export declare const getBetween: (num: number, start: number, end: number) => number;
115
105
  /**
116
106
  * Get a number as a percentage of another number.
117
107
  *
package/util/number.js CHANGED
@@ -8,9 +8,22 @@ export function assertNumber(v) {
8
8
  throw new AssertionError(`Must be number`, v);
9
9
  }
10
10
  /** Assert that a value is a number greater than. */
11
+ export function assertFinite(v) {
12
+ if (typeof v !== "number" || !Number.isFinite(v))
13
+ throw new AssertionError(`Must be finite number`, v);
14
+ }
15
+ /**
16
+ * Is a finite number within a specified range?
17
+ *
18
+ * @param num The number to test, e.g. `17`
19
+ * @param min The start of the range, e.g. `10`
20
+ * @param max The end of the range, e.g. `20`
21
+ */
22
+ export const isBetween = (num, min, max) => num >= min && num <= max;
23
+ /** Assert that a value is a number greater than. */
11
24
  export function assertBetween(v, min, max) {
12
- if (typeof v !== "number" || v < min || v > max)
13
- throw new AssertionError(`Must be number between ${min}-${max}`, v);
25
+ if (typeof v !== "number" || isBetween(v, min, max))
26
+ throw new AssertionError(`Must be number between ${min} and ${max}`, v);
14
27
  }
15
28
  /** Assert that a value is a number greater than. */
16
29
  export function assertMax(v, max) {
@@ -36,19 +49,19 @@ export function getOptionalNumber(value) {
36
49
  if (typeof value === "number")
37
50
  return !Number.isFinite(value) ? null : value === 0 ? 0 : value;
38
51
  else if (typeof value === "string")
39
- return getOptionalNumber(parseFloat(value.replace(NUMERIC, "")));
52
+ return getOptionalNumber(parseFloat(value.replace(NOT_NUMERIC_REGEXP, "")));
40
53
  else if (value instanceof Date)
41
- return value.getTime();
54
+ return getOptionalNumber(value.getTime());
42
55
  return null;
43
56
  }
44
- const NUMERIC = /[^0-9-.]/g;
57
+ const NOT_NUMERIC_REGEXP = /[^0-9-.]/g;
45
58
  /**
46
59
  * Assertively convert an unknown value to a finite number.
47
60
  * @throws `AssertionError` if the value cannot be converted.
48
61
  */
49
62
  export function getNumber(value) {
50
63
  const num = getOptionalNumber(value);
51
- assertNumber(num);
64
+ assertFinite(num);
52
65
  return num;
53
66
  }
54
67
  /**
@@ -81,25 +94,31 @@ export const roundNumber = (num, precision = 0) => Math.round(num * 10 ** precis
81
94
  * @returns The number truncated to the specified precision.
82
95
  */
83
96
  export const truncateNumber = (num, precision = 0) => Math.trunc(num * 10 ** precision) / 10 ** precision;
84
- /**
85
- * Format a number (based on the user's browser settings).
86
- *
87
- * @param num The number to format.
88
- * @param maxPrecision Maximum number of digits shown after the decimal point (defaults to 2).
89
- * @param minPrecision Minimum number of digits shown after the decimal point (defaults to 0).
90
- *
91
- * @returns The number formatted as a string in the browser's current locale.
92
- */
93
- export function formatNumber(num, maxPrecision = 4, minPrecision = 0) {
97
+ /** Bound a number so it fits between two values. */
98
+ export function boundNumber(num, min, max) {
99
+ assertMin(max, min); // Assert that max is more than min.
100
+ return Math.max(min, Math.min(max, num));
101
+ }
102
+ /** Wrap a number so it fits between two values. */
103
+ export function wrapNumber(num, min, max) {
104
+ assertMin(max, min); // Assert that max is more than min.
105
+ if (num >= max)
106
+ return ((num - max) % (max - min)) + min;
107
+ if (num < min)
108
+ return ((num - min) % (min - max)) + max;
109
+ return num;
110
+ }
111
+ /** Format a number (based on the user's browser language settings). */
112
+ export function formatNumber(num, precision = null) {
94
113
  if (!Number.isFinite(num))
95
114
  return Number.isNaN(num) ? "None" : "Infinity";
96
- return new Intl.NumberFormat(undefined, { maximumFractionDigits: maxPrecision, minimumFractionDigits: minPrecision }).format(num);
115
+ return new Intl.NumberFormat(undefined, { minimumFractionDigits: precision !== null && precision !== void 0 ? precision : undefined, maximumFractionDigits: precision !== null && precision !== void 0 ? precision : 20 }).format(num);
97
116
  }
98
117
  /** Format a number with a short abbreviated suffix. */
99
- export const formatQuantity = (num, abbr, maxPrecision, minPrecision) => `${formatNumber(num, maxPrecision, minPrecision)}${NNBSP}${abbr}`;
118
+ export const formatQuantity = (num, abbr, precision) => `${formatNumber(num, precision)}${NNBSP}${abbr}`;
100
119
  /** Format a number with a longer full-word suffix. */
101
- export function formatFullQuantity(num, singular, plural, maxPrecision, minPrecision) {
102
- const qty = formatNumber(num, maxPrecision, minPrecision);
120
+ export function formatFullQuantity(num, singular, plural, precision) {
121
+ const qty = formatNumber(num, precision);
103
122
  return `${qty} ${qty === "1" ? singular : plural}`;
104
123
  }
105
124
  /**
@@ -148,22 +167,6 @@ export function cramFullQuantity(num, singular, plural) {
148
167
  const qty = cramNumber(num);
149
168
  return `${qty} ${qty === "1" ? singular : plural}`;
150
169
  }
151
- /**
152
- * Is a number within a specified range?
153
- *
154
- * @param num The number to test, e.g. `17`
155
- * @param start The start of the range, e.g. `10`
156
- * @param end The end of the range, e.g. `20`
157
- */
158
- export const isBetween = (num, start, end) => num >= start && num <= end;
159
- /**
160
- * Apply a min/max to a number to return a number that's definitely in the specified range.
161
- *
162
- * @param num The number to apply the min/max to, e.g. `17`
163
- * @param start The start of the range, e.g. `10`
164
- * @param end The end of the range, e.g. `20`
165
- */
166
- export const getBetween = (num, start, end) => Math.max(start, Math.min(end, num));
167
170
  /**
168
171
  * Get a number as a percentage of another number.
169
172
  *
package/util/time.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ /** Class representing a time in the day in 24 hour format in the user's current locale. */
2
+ export declare class Time {
3
+ /** Make a new `Time` instance from a date (or any value that can be converted to a date). */
4
+ static fromDate(possible?: unknown): Time | null;
5
+ /** Make a new `Time` instance from a time string. */
6
+ static fromString(str: string): Time | null;
7
+ readonly time: number;
8
+ constructor(time: number);
9
+ /** Get the number of hours in this time. */
10
+ get h(): number;
11
+ /** Get the number of minutes in this time. */
12
+ get m(): number;
13
+ /** Get the number of seconds in this time. */
14
+ get s(): number;
15
+ /** Get the number of seconds in this time. */
16
+ get ms(): number;
17
+ /** Get the time as in `hh:mm` format (hours, minutes), e.g. `13.59` */
18
+ get short(): string;
19
+ /** Get the time in `hh:mm:ss` format (hours, minutes seconds), e.g. `13.16.19.123` */
20
+ get medium(): string;
21
+ /** Get this time in `hh:mm:ss.fff` format (ISO 8601 compatible, hours, minutes, seconds, milliseconds), e.g. `13:16:19.123` */
22
+ get long(): string;
23
+ /** Get a date corresponding to this time. */
24
+ get date(): Date;
25
+ /**
26
+ * Format this time using the browser locale settings with a specified amount of precision.
27
+ * @param precision Reveal additional parts of the time, e.g. `2` shows hours and minutes, `3` also shows seconds, and `4 | 5 | 6` show mlliseconds at increasing precision.
28
+ */
29
+ format(precision?: 2 | 3 | 4 | 5 | 6): string;
30
+ valueOf(): number;
31
+ toString(): string;
32
+ }
33
+ /** Regular expression that matches a time in ISO 8601 format. */
34
+ export declare const TIME_REGEXP: RegExp;
35
+ /** Things that converted to times. */
36
+ export declare type PossibleTime = Time | Date | number | string | (() => number | string);
37
+ /** Things that converted to times or `null` */
38
+ export declare type PossibleOptionalTime = Time | Date | number | string | null | (() => number | string | null);
39
+ /** Is an unknown value a `Time` instance. */
40
+ export declare const isTime: (v: Time | unknown) => v is Time;
41
+ /**
42
+ * Convert a value to a `Time` instance or `null`
43
+ * - Works with possible dates, e.g. `now` or `Date` or `2022-09-12 18:32` or `19827263567`
44
+ * - Works with time strings, e.g. `18:32` or `23:59:59.999`
45
+ */
46
+ export declare function getOptionalTime(possible: unknown): Time | null;
47
+ /** Convert a possible time to a `Time` instance, or throw `AssertionError` if it couldn't be converted. */
48
+ export declare function getTime(possible?: PossibleTime): Time;
49
+ /** Get the time as in `hh:mm` format (hours, minutes), e.g. `13.59` */
50
+ export declare const getShortTime: (time?: PossibleTime) => string;
51
+ /** Get the time in `hh:mm:ss` format (hours, minutes seconds), e.g. `13.16.19.123` */
52
+ export declare const getMediumTime: (time?: PossibleTime) => string;
53
+ /** Get this time in `hh:mm:ss.fff` format (ISO 8601 compatible, hours, minutes, seconds, milliseconds), e.g. `13:16:19.123` */
54
+ export declare const getLongTime: (time?: PossibleTime) => string;
55
+ /** Format a time as a string based on the browser locale settings. */
56
+ export declare const formatTime: (time?: PossibleTime, precision?: 2 | 3 | 4 | 5 | 6) => string;
57
+ /** Format an optional time as a string based on the browser locale settings. */
58
+ export declare const formatOptionalTime: (time?: unknown, precision?: 2 | 3 | 4 | 5 | 6) => string | null;
package/util/time.js ADDED
@@ -0,0 +1,111 @@
1
+ import { AssertionError } from "../error/AssertionError.js";
2
+ import { DAY, HOUR, MINUTE, SECOND } from "./constants.js";
3
+ import { getOptionalDate } from "./date.js";
4
+ import { wrapNumber } from "./number.js";
5
+ /** Class representing a time in the day in 24 hour format in the user's current locale. */
6
+ export class Time {
7
+ constructor(time) {
8
+ this.time = wrapNumber(Math.round(time), 0, DAY);
9
+ }
10
+ /** Make a new `Time` instance from a date (or any value that can be converted to a date). */
11
+ static fromDate(possible) {
12
+ const date = getOptionalDate(possible);
13
+ return date ? new Time(date.getHours() * HOUR + date.getMinutes() * MINUTE + date.getSeconds() * SECOND + date.getMilliseconds()) : null;
14
+ }
15
+ /** Make a new `Time` instance from a time string. */
16
+ static fromString(str) {
17
+ const matches = str.match(TIME_REGEXP);
18
+ if (!matches)
19
+ return null;
20
+ const [, h, m, s, ms] = matches;
21
+ return new Time(parseInt(h, 10) * HOUR + parseInt(m, 10) * MINUTE + (typeof s === "string" ? parseInt(s, 10) * SECOND : 0) + (typeof ms === "string" ? parseInt(ms, 10) : 0));
22
+ }
23
+ /** Get the number of hours in this time. */
24
+ get h() {
25
+ return Math.trunc(this.time / HOUR);
26
+ }
27
+ /** Get the number of minutes in this time. */
28
+ get m() {
29
+ return Math.trunc((this.time % HOUR) / MINUTE);
30
+ }
31
+ /** Get the number of seconds in this time. */
32
+ get s() {
33
+ return Math.trunc((this.time % MINUTE) / SECOND);
34
+ }
35
+ /** Get the number of seconds in this time. */
36
+ get ms() {
37
+ return this.time % SECOND;
38
+ }
39
+ /** Get the time as in `hh:mm` format (hours, minutes), e.g. `13.59` */
40
+ get short() {
41
+ return `${_pad(this.h, 2)}:${_pad(this.m, 2)}`;
42
+ }
43
+ /** Get the time in `hh:mm:ss` format (hours, minutes seconds), e.g. `13.16.19.123` */
44
+ get medium() {
45
+ return `${_pad(this.h, 2)}:${_pad(this.m, 2)}:${_pad(this.s, 2)}`;
46
+ }
47
+ /** Get this time in `hh:mm:ss.fff` format (ISO 8601 compatible, hours, minutes, seconds, milliseconds), e.g. `13:16:19.123` */
48
+ get long() {
49
+ return `${_pad(this.h, 2)}:${_pad(this.m, 2)}:${_pad(this.s, 2)}.${_pad(this.ms, 3)}`;
50
+ }
51
+ /** Get a date corresponding to this time. */
52
+ get date() {
53
+ const date = new Date();
54
+ date.setHours(0, 0, 0, 0);
55
+ return date;
56
+ }
57
+ /**
58
+ * Format this time using the browser locale settings with a specified amount of precision.
59
+ * @param precision Reveal additional parts of the time, e.g. `2` shows hours and minutes, `3` also shows seconds, and `4 | 5 | 6` show mlliseconds at increasing precision.
60
+ */
61
+ format(precision = 2) {
62
+ return this.date.toLocaleTimeString(undefined, {
63
+ hour: "2-digit",
64
+ minute: "2-digit",
65
+ second: precision >= 3 ? "2-digit" : undefined,
66
+ fractionalSecondDigits: precision >= 4 ? precision - 3 : undefined,
67
+ });
68
+ }
69
+ // Implement `valueOf()`
70
+ valueOf() {
71
+ return this.time;
72
+ }
73
+ // Implement `toString()`
74
+ toString() {
75
+ return this.long;
76
+ }
77
+ }
78
+ const _pad = (num, size) => num.toString(10).padStart(size, "0000");
79
+ /** Regular expression that matches a time in ISO 8601 format. */
80
+ export const TIME_REGEXP = /([0-9]+):([0-9]+)(?::([0-9]+)(?:.([0-9]+))?)?/;
81
+ /** Is an unknown value a `Time` instance. */
82
+ export const isTime = (v) => v instanceof Time;
83
+ /**
84
+ * Convert a value to a `Time` instance or `null`
85
+ * - Works with possible dates, e.g. `now` or `Date` or `2022-09-12 18:32` or `19827263567`
86
+ * - Works with time strings, e.g. `18:32` or `23:59:59.999`
87
+ */
88
+ export function getOptionalTime(possible) {
89
+ if (isTime(possible))
90
+ return possible;
91
+ if (typeof possible === "function")
92
+ return getOptionalTime(possible());
93
+ return (typeof possible === "string" && Time.fromString(possible)) || Time.fromDate(possible) || null;
94
+ }
95
+ /** Convert a possible time to a `Time` instance, or throw `AssertionError` if it couldn't be converted. */
96
+ export function getTime(possible = "now") {
97
+ const time = getOptionalTime(possible);
98
+ if (!time)
99
+ throw new AssertionError(`Must be time`, possible);
100
+ return time;
101
+ }
102
+ /** Get the time as in `hh:mm` format (hours, minutes), e.g. `13.59` */
103
+ export const getShortTime = (time) => getTime(time).long;
104
+ /** Get the time in `hh:mm:ss` format (hours, minutes seconds), e.g. `13.16.19.123` */
105
+ export const getMediumTime = (time) => getTime(time).long;
106
+ /** Get this time in `hh:mm:ss.fff` format (ISO 8601 compatible, hours, minutes, seconds, milliseconds), e.g. `13:16:19.123` */
107
+ export const getLongTime = (time) => getTime(time).long;
108
+ /** Format a time as a string based on the browser locale settings. */
109
+ export const formatTime = (time, precision = 2) => getTime(time).format(precision);
110
+ /** Format an optional time as a string based on the browser locale settings. */
111
+ export const formatOptionalTime = (time, precision = 2) => { var _a; return ((_a = getOptionalTime(time)) === null || _a === void 0 ? void 0 : _a.format(precision)) || null; };
package/util/units.d.ts CHANGED
@@ -14,6 +14,8 @@ declare type UnitProps<T extends string> = {
14
14
  readonly singular?: string;
15
15
  /** Plural name for this unit, e.g. `kilometers` (defaults to `id`). */
16
16
  readonly plural?: string;
17
+ /** Default precision for this unit (defaults to `null`). */
18
+ readonly precision?: number | null;
17
19
  /** Conversions to other units (typically needs at least the base conversion, unless it's already the base unit). */
18
20
  readonly to?: Conversions<T>;
19
21
  };
@@ -30,6 +32,8 @@ export declare class Unit<T extends string> {
30
32
  readonly singular: string;
31
33
  /** Plural name for this unit, e.g. `kilometers` (defaults to `singular` + "s"). */
32
34
  readonly plural: string;
35
+ /** Default precision for this unit (defaults to `null`). */
36
+ readonly precision: number | null;
33
37
  /** Title for this unit (uses format `abbr (plural)`, e.g. `fl oz (US fluid ounces)`) */
34
38
  get title(): string;
35
39
  constructor(
@@ -38,7 +42,7 @@ export declare class Unit<T extends string> {
38
42
  /** Key for this unit, e.g. `kilometer` */
39
43
  id: T,
40
44
  /** Props to configure this unit. */
41
- { abbr, singular, plural, to }: UnitProps<T>);
45
+ { abbr, singular, plural, precision, to }: UnitProps<T>);
42
46
  /** Convert an amount from this unit to another unit. */
43
47
  to(amount: number, id?: T | Unit<T>): number;
44
48
  /** Convert an amount from another unit to this unit. */
@@ -46,9 +50,9 @@ export declare class Unit<T extends string> {
46
50
  /** Convert an amount from this unit to another unit (must specify another `Unit` instance). */
47
51
  private _toUnit;
48
52
  /** Format a number with a given unit of measure, e.g. `12 kg` or `29.5 l` */
49
- format(amount: number, maxPrecision?: number, minPrecision?: number): string;
53
+ format(amount: number, precision?: number | null): string;
50
54
  /** Format a number with a given unit of measure, e.g. `12 kilograms` or `29.5 liters` or `1 degree` */
51
- formatFull(amount: number, maxPrecision?: number, minPrecision?: number): string;
55
+ formatFull(amount: number, precision?: number | null): string;
52
56
  }
53
57
  /** Represent a list of units. */
54
58
  export declare class UnitList<T extends string> extends ImmutableRequiredMap<T, Unit<T>> {
@@ -86,11 +90,11 @@ export declare type VolumeUnitIdentifier = MapKey<typeof VOLUME_UNITS>;
86
90
  export declare const TEMPERATURE_UNITS: UnitList<"celsius" | "fahrenheit" | "kelvin">;
87
91
  export declare type TemperatureUnitIdentifier = MapKey<typeof TEMPERATURE_UNITS>;
88
92
  /** Format a percentage (combines `getPercent()` and `formatUnits()` for convenience). */
89
- export declare const formatPercent: (numerator: number, denumerator: number, maxPrecision?: number, minPrecision?: number) => string;
93
+ export declare const formatPercent: (numerator: number, denumerator: number, precision?: number) => string;
90
94
  /** Format a full format of a duration of time using the most reasonable units e.g. `5 years` or `1 week` or `4 minutes` or `12 milliseconds`. */
91
- export declare function formatFullDuration(ms: number, maxPrecision?: number, minPrecision?: number): string;
95
+ export declare function formatFullDuration(ms: number, precision?: number): string;
92
96
  /** Format a description of a duration of time using the most reasonable units e.g. `5y` or `4m` or `12ms`. */
93
- export declare function formatDuration(ms: number, maxPrecision?: number, minPrecision?: number): string;
97
+ export declare function formatDuration(ms: number, precision?: number): string;
94
98
  /** Full when a date happens/happened, e.g. `in 10 days` or `2 hours ago` */
95
99
  export declare const formatFullWhen: (target: PossibleDate, current?: PossibleDate) => string;
96
100
  /** Compact when a date happens/happened, e.g. `in 10d` or `2h ago` or `in 1w` */
package/util/units.js CHANGED
@@ -14,12 +14,13 @@ export class Unit {
14
14
  /** Key for this unit, e.g. `kilometer` */
15
15
  id,
16
16
  /** Props to configure this unit. */
17
- { abbr = id.slice(0, 1), singular = id.replace(/-/, " "), plural = `${singular}s`, to }) {
17
+ { abbr = id.slice(0, 1), singular = id.replace(/-/, " "), plural = `${singular}s`, precision = null, to }) {
18
18
  this.list = list;
19
19
  this.id = id;
20
20
  this.abbr = abbr;
21
21
  this.singular = singular;
22
22
  this.plural = plural;
23
+ this.precision = precision;
23
24
  this._to = to;
24
25
  }
25
26
  /** Title for this unit (uses format `abbr (plural)`, e.g. `fl oz (US fluid ounces)`) */
@@ -57,12 +58,12 @@ export class Unit {
57
58
  throw new ConditionError(`Cannot convert "${this.id}" to "${unit.id}"`);
58
59
  }
59
60
  /** Format a number with a given unit of measure, e.g. `12 kg` or `29.5 l` */
60
- format(amount, maxPrecision, minPrecision) {
61
- return formatQuantity(amount, this.abbr, maxPrecision, minPrecision);
61
+ format(amount, precision = this.precision) {
62
+ return formatQuantity(amount, this.abbr, precision);
62
63
  }
63
64
  /** Format a number with a given unit of measure, e.g. `12 kilograms` or `29.5 liters` or `1 degree` */
64
- formatFull(amount, maxPrecision, minPrecision) {
65
- return formatFullQuantity(amount, this.singular, this.plural, maxPrecision, minPrecision);
65
+ formatFull(amount, precision = this.precision) {
66
+ return formatFullQuantity(amount, this.singular, this.plural, precision);
66
67
  }
67
68
  }
68
69
  const _getUnit = (list, id) => (typeof id === "string" ? list.get(id) : id);
@@ -206,7 +207,7 @@ export const TEMPERATURE_UNITS = new UnitList({
206
207
  kelvin: { abbr: "°K", singular: "degree Kelvin", plural: "degrees Kelvin", to: { celsius: n => n - 273.15 } },
207
208
  });
208
209
  /** Format a percentage (combines `getPercent()` and `formatUnits()` for convenience). */
209
- export const formatPercent = (numerator, denumerator, maxPrecision, minPrecision) => formatQuantity(getPercent(numerator, denumerator), "%", maxPrecision, minPrecision);
210
+ export const formatPercent = (numerator, denumerator, precision) => formatQuantity(getPercent(numerator, denumerator), "%", precision);
210
211
  /** Get the ID for a time unit based on the amount in milliseconds. */
211
212
  function _getTimeUnitIdentifier(ms) {
212
213
  const abs = Math.abs(ms);
@@ -227,14 +228,14 @@ function _getTimeUnitIdentifier(ms) {
227
228
  return "millisecond";
228
229
  }
229
230
  /** Format a full format of a duration of time using the most reasonable units e.g. `5 years` or `1 week` or `4 minutes` or `12 milliseconds`. */
230
- export function formatFullDuration(ms, maxPrecision, minPrecision) {
231
+ export function formatFullDuration(ms, precision) {
231
232
  const unit = TIME_UNITS.get(_getTimeUnitIdentifier(ms));
232
- return unit.formatFull(unit.from(ms), maxPrecision, minPrecision);
233
+ return unit.formatFull(unit.from(ms), precision);
233
234
  }
234
235
  /** Format a description of a duration of time using the most reasonable units e.g. `5y` or `4m` or `12ms`. */
235
- export function formatDuration(ms, maxPrecision, minPrecision) {
236
+ export function formatDuration(ms, precision) {
236
237
  const unit = TIME_UNITS.get(_getTimeUnitIdentifier(ms));
237
- return unit.format(unit.from(ms), maxPrecision, minPrecision);
238
+ return unit.format(unit.from(ms), precision);
238
239
  }
239
240
  /** format when a data happens/happened. */
240
241
  function _formatWhen(formatter, target, current) {