shelving 1.162.1 → 1.163.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.162.1",
14
+ "version": "1.163.0",
15
15
  "repository": "https://github.com/dhoulb/shelving",
16
16
  "author": "Dave Houlbrooke <dave@shax.com>",
17
17
  "license": "0BSD",
package/util/format.d.ts CHANGED
@@ -2,23 +2,60 @@ import { type ImmutableArray } from "./array.js";
2
2
  import { type PossibleDate } from "./date.js";
3
3
  import { type ImmutableObject } from "./object.js";
4
4
  import { type PossibleURL } from "./url.js";
5
- /** Format a number range (based on the user's browser language settings). */
6
- export declare function formatRange(min: number, max: number, options?: Intl.NumberFormatOptions): string;
7
- /** Format a number with a short suffix, e.g. `1,000 kg` */
8
- export declare function formatQuantity(num: number, suffix: string, options?: Intl.NumberFormatOptions): string;
5
+ /** Options we use for number formatting. */
6
+ export type NumberOptions = Omit<Intl.NumberFormatOptions, "style" | "unit" | "unitDisplay" | "currency" | "currencyDisplay" | "currencySign">;
9
7
  /** Format a number (based on the user's browser language settings). */
10
- export declare function formatNumber(num: number, options?: Intl.NumberFormatOptions): string;
11
- /** Format a number with a longer full-word suffix. */
12
- export declare function pluralizeQuantity(num: number, one: string, many: string, options?: Intl.NumberFormatOptions): string;
8
+ export declare function formatNumber(num: number, options?: NumberOptions): string;
9
+ /** Format a number range (based on the user's browser language settings). */
10
+ export declare function formatRange(from: number, to: number, options?: NumberOptions): string;
11
+ /** Options for quantity formatting. */
12
+ export interface QuantityOptions extends Omit<Intl.NumberFormatOptions, "style" | "unit" | "currency" | "currencyDisplay" | "currencySign"> {
13
+ /**
14
+ * String for one of this thing, e.g. `product` or `item` or `sheep`
15
+ * - Used for `unitDisplay: "long"` formatting.
16
+ * - Defaults to unit reference, e.g. "minute"
17
+ */
18
+ readonly one?: string;
19
+ /**
20
+ * String for several of this thing, e.g. `products` or `items` or `sheep`
21
+ * - Used for `unitDisplay: "long"` formatting.
22
+ * - Defaults to `one + "s"`
23
+ */
24
+ readonly many?: string;
25
+ /**
26
+ * Abbreviation for this thing, e.g. `products` or `items` or `sheep` (defaults to `one` + "s").
27
+ * - Used for `unitDisplay: "narrow"` formatting.
28
+ * - Defaults to unit reference, e.g. "minute"
29
+ */
30
+ readonly abbr?: string;
31
+ }
32
+ /**
33
+ * Format a quantity of a given unit.
34
+ *
35
+ * - Javascript has built-in support for formatting a number of different units.
36
+ * - Unfortunately the list of supported units changes in different browsers.
37
+ * - Ideally we want to format units using the built-in formatting so things like translation and internationalisation are covered.
38
+ * - But we want provide fallback formatting for unsupported units, and do something _good enough_ job in most cases.
39
+ */
40
+ export declare function formatUnit(num: number, unit: string, options?: QuantityOptions): string;
41
+ /** Options we use for currency formatting. */
42
+ export type CurrencyOptions = Omit<Intl.NumberFormatOptions, "style" | "unit" | "unitDisplay" | "currency">;
43
+ /**
44
+ * Format a currency amount (based on the user's browser language settings).
45
+ */
46
+ export declare function formatCurrency(amount: number, currency: string, options?: CurrencyOptions): string;
47
+ /** Options we use for percent formatting. */
48
+ export type PercentOptions = Omit<Intl.NumberFormatOptions, "style" | "unit" | "unitDisplay" | "currency" | "currencyDisplay" | "currencySign">;
13
49
  /**
14
50
  * Format a percentage (combines `getPercent()` and `formatQuantity()` for convenience).
15
51
  * - Defaults to showing no decimal places.
16
52
  * - Defaults to rounding closer to zero (so that 99.99% is shown as 99%).
53
+ * - Javascript's built-in percent formatting works on the `0` zero to `1` range. This uses `getPercent()` which works on `0` to `100` for convenience.
17
54
  *
18
- * @param numerator Number representing the amount of progress.
19
- * @param denumerator The number representing the whole amount.
55
+ * @param numerator Number representing the amount of progress (e.g. `50`).
56
+ * @param denumerator The number representing the whole amount (defaults to 100).
20
57
  */
21
- export declare function formatPercent(numerator: number, denumerator: number, options?: Intl.NumberFormatOptions): string;
58
+ export declare function formatPercent(numerator: number, denumerator?: number, options?: PercentOptions): string;
22
59
  /**
23
60
  * Format an unknown object as a string.
24
61
  * - Use the custom `.toString()` function if it exists (don't use built in `Object.prototype.toString` because it's useless.
package/util/format.js CHANGED
@@ -1,49 +1,61 @@
1
1
  import { isArray } from "./array.js";
2
- import { NNBSP } from "./constants.js";
3
2
  import { isDate, requireDate } from "./date.js";
4
3
  import { getPercent } from "./number.js";
5
4
  import { isObject } from "./object.js";
6
5
  import { requireURL } from "./url.js";
7
- /** Format a number range (based on the user's browser language settings). */
8
- export function formatRange(min, max, options) {
9
- return `${formatNumber(min, options)}${NNBSP}–${NNBSP}${formatNumber(max, options)}`;
10
- }
11
- /** Format a number with a short suffix, e.g. `1,000 kg` */
12
- export function formatQuantity(num, suffix, options) {
13
- const o = { unitDisplay: "short", ...options, style: "decimal" };
14
- const str = formatNumber(num, o);
15
- const sep = o.unitDisplay === "narrow" ? "" : NNBSP;
16
- return `${str}${sep}${suffix}`;
17
- }
18
6
  /** Format a number (based on the user's browser language settings). */
19
7
  export function formatNumber(num, options) {
20
- if (!Number.isFinite(num))
21
- return Number.isNaN(num) ? "-" : "∞";
22
- return new Intl.NumberFormat(undefined, options).format(num).replace(/ /, NNBSP);
8
+ return Intl.NumberFormat(undefined, options).format(num);
9
+ }
10
+ /** Format a number range (based on the user's browser language settings). */
11
+ export function formatRange(from, to, options) {
12
+ return Intl.NumberFormat(undefined, options).formatRange(from, to);
23
13
  }
24
- /** Format a number with a longer full-word suffix. */
25
- export function pluralizeQuantity(num, one, many, options) {
26
- const qty = formatNumber(num, {
14
+ /**
15
+ * Format a quantity of a given unit.
16
+ *
17
+ * - Javascript has built-in support for formatting a number of different units.
18
+ * - Unfortunately the list of supported units changes in different browsers.
19
+ * - Ideally we want to format units using the built-in formatting so things like translation and internationalisation are covered.
20
+ * - But we want provide fallback formatting for unsupported units, and do something _good enough_ job in most cases.
21
+ */
22
+ export function formatUnit(num, unit, options) {
23
+ // Check if the unit is supported by the browser.
24
+ if (Intl.supportedValuesOf("unit").includes(unit))
25
+ return Intl.NumberFormat(undefined, { ...options, style: "unit", unit }).format(num);
26
+ // Otherwise, use the default number format.
27
+ const str = Intl.NumberFormat(undefined, { ...options, style: "decimal" }).format(num);
28
+ const { unitDisplay, abbr = unit, one = unit, many = `${one}s` } = options ?? {};
29
+ if (unitDisplay === "long")
30
+ return `${str} ${str === "1" ? one : many}`;
31
+ return `${str}${unitDisplay === "narrow" ? "" : " "}${abbr}`; // "short" is the default.
32
+ }
33
+ /**
34
+ * Format a currency amount (based on the user's browser language settings).
35
+ */
36
+ export function formatCurrency(amount, currency, options) {
37
+ return Intl.NumberFormat(undefined, {
38
+ style: "currency",
39
+ currency,
27
40
  ...options,
28
- style: "decimal",
29
- });
30
- return `${qty}${NNBSP}${num === 1 ? one : many}`;
41
+ }).format(amount);
31
42
  }
32
43
  /**
33
44
  * Format a percentage (combines `getPercent()` and `formatQuantity()` for convenience).
34
45
  * - Defaults to showing no decimal places.
35
46
  * - Defaults to rounding closer to zero (so that 99.99% is shown as 99%).
47
+ * - Javascript's built-in percent formatting works on the `0` zero to `1` range. This uses `getPercent()` which works on `0` to `100` for convenience.
36
48
  *
37
- * @param numerator Number representing the amount of progress.
38
- * @param denumerator The number representing the whole amount.
49
+ * @param numerator Number representing the amount of progress (e.g. `50`).
50
+ * @param denumerator The number representing the whole amount (defaults to 100).
39
51
  */
40
52
  export function formatPercent(numerator, denumerator, options) {
41
- return formatNumber(getPercent(numerator, denumerator), {
53
+ return Intl.NumberFormat(undefined, {
54
+ style: "percent",
42
55
  maximumFractionDigits: 0,
43
- roundingMode: "trunc",
56
+ roundingMode: "floor",
44
57
  ...options,
45
- style: "percent",
46
- });
58
+ }).format(getPercent(numerator, denumerator) / 100);
47
59
  }
48
60
  /**
49
61
  * Format an unknown object as a string.
package/util/number.d.ts CHANGED
@@ -92,9 +92,9 @@ export declare function wrapNumber(num: number, min: number, max: number): numbe
92
92
  * Get a number as a percentage of another number.
93
93
  *
94
94
  * @param numerator Number representing the amount of progress.
95
- * @param denumerator The number representing the whole amount.
95
+ * @param denumerator The number representing the whole amount (defaults to 100).
96
96
  */
97
- export declare function getPercent(numerator: number, denumerator: number): number;
97
+ export declare function getPercent(numerator: number, denumerator?: number): number;
98
98
  /** Sum an iterable set of numbers and return the total. */
99
99
  export declare function sumNumbers(nums: Iterable<number>): number;
100
100
  /** Find the number that's closest to a target in an iterable set of numbers. */
package/util/number.js CHANGED
@@ -144,10 +144,10 @@ export function wrapNumber(num, min, max) {
144
144
  * Get a number as a percentage of another number.
145
145
  *
146
146
  * @param numerator Number representing the amount of progress.
147
- * @param denumerator The number representing the whole amount.
147
+ * @param denumerator The number representing the whole amount (defaults to 100).
148
148
  */
149
- export function getPercent(numerator, denumerator) {
150
- return Math.max(0, Math.min(100, (100 / denumerator) * numerator));
149
+ export function getPercent(numerator, denumerator = 100) {
150
+ return denumerator === 100 ? numerator : (100 / denumerator) * numerator;
151
151
  }
152
152
  /** Sum an iterable set of numbers and return the total. */
153
153
  export function sumNumbers(nums) {
package/util/units.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type QuantityOptions } from "./format.js";
1
2
  import { ImmutableMap, type MapKey } from "./map.js";
2
3
  import type { ImmutableObject } from "./object.js";
3
4
  /** Conversion from one unit to another (either an amount to multiple by, or a function to convert). */
@@ -6,18 +7,10 @@ type Conversion = number | ((num: number) => number);
6
7
  type Conversions<T extends string> = {
7
8
  readonly [K in T]?: Conversion;
8
9
  };
9
- type UnitProps<T extends string> = {
10
- /** Short abbreviation for this unit, e.g. `km` (defaults to first letter of `id`). */
11
- readonly abbr?: string;
12
- /** Singular name for this unit, e.g. `kilometer` (defaults to `id` + "s"). */
13
- readonly one?: string;
14
- /** Plural name for this unit, e.g. `kilometers` (defaults to `id`). */
15
- readonly many?: string;
10
+ interface UnitProps<T extends string> extends QuantityOptions {
16
11
  /** Conversions to other units (typically needs at least the base conversion, unless it's already the base unit). */
17
12
  readonly to?: Conversions<T>;
18
- /** Possible options for formatting these units with `Intl.NumberFormat` (`.unit` can be specified if different from key, but is not required). */
19
- readonly options?: Readonly<Intl.NumberFormatOptions> | undefined;
20
- };
13
+ }
21
14
  /** Represent a unit. */
22
15
  export declare class Unit<K extends string> {
23
16
  private readonly _to;
@@ -25,23 +18,15 @@ export declare class Unit<K extends string> {
25
18
  readonly list: UnitList<K>;
26
19
  /** String key for this unit, e.g. `kilometer` */
27
20
  readonly key: K;
28
- /** Short abbreviation for this unit, e.g. `km` (defaults to first letter of `id`). */
29
- readonly abbr: string;
30
- /** Singular name for this unit, e.g. `kilometer` (defaults to `id`). */
31
- readonly one: string;
32
- /** Plural name for this unit, e.g. `kilometers` (defaults to `singular` + "s"). */
33
- readonly many: string;
34
- /** Possible options for formatting these units with `Intl.NumberFormat` (`.unit` can be specified if different from key, but is not required). */
35
- readonly options: Readonly<Intl.NumberFormatOptions> | undefined;
36
- /** Title for this unit (uses format `abbr (plural)`, e.g. `fl oz (US fluid ounces)`) */
37
- get title(): string;
21
+ /** Possible options for formatting these units. */
22
+ readonly options: Readonly<QuantityOptions> | undefined;
38
23
  constructor(
39
24
  /** `UnitList` this unit belongs to. */
40
25
  list: UnitList<K>,
41
26
  /** String key for this unit, e.g. `kilometer` */
42
27
  key: K,
43
28
  /** Props to configure this unit. */
44
- { abbr, one, many, options, to }: UnitProps<K>);
29
+ { to, ...options }: UnitProps<K>);
45
30
  /** Convert an amount from this unit to another unit. */
46
31
  to(amount: number, targetKey?: K): number;
47
32
  /** Convert an amount from another unit to this unit. */
@@ -53,7 +38,7 @@ export declare class Unit<K extends string> {
53
38
  * - Uses `Intl.NumberFormat` if this is a supported unit (so e.g. `ounce` is translated to e.g. `Unze` in German).
54
39
  * - Polyfills unsupported units to use long/short form based on `options.unitDisplay`.
55
40
  */
56
- format(amount: number, options?: Intl.NumberFormatOptions): string;
41
+ format(amount: number, options?: QuantityOptions): string;
57
42
  }
58
43
  /**
59
44
  * Represent a list of units.
package/util/units.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { RequiredError } from "../error/RequiredError.js";
2
2
  import { ValueError } from "../error/ValueError.js";
3
3
  import { DAY, HOUR, MILLION, MINUTE, MONTH, NNBSP, SECOND, WEEK, YEAR } from "./constants.js";
4
- import { formatNumber, formatQuantity, pluralizeQuantity } from "./format.js";
4
+ import { formatUnit } from "./format.js";
5
5
  import { ImmutableMap } from "./map.js";
6
6
  import { getProps } from "./object.js";
7
7
  /** Convert an amount using a `Conversion. */
@@ -15,30 +15,17 @@ export class Unit {
15
15
  list;
16
16
  /** String key for this unit, e.g. `kilometer` */
17
17
  key;
18
- /** Short abbreviation for this unit, e.g. `km` (defaults to first letter of `id`). */
19
- abbr;
20
- /** Singular name for this unit, e.g. `kilometer` (defaults to `id`). */
21
- one;
22
- /** Plural name for this unit, e.g. `kilometers` (defaults to `singular` + "s"). */
23
- many;
24
- /** Possible options for formatting these units with `Intl.NumberFormat` (`.unit` can be specified if different from key, but is not required). */
18
+ /** Possible options for formatting these units. */
25
19
  options;
26
- /** Title for this unit (uses format `abbr (plural)`, e.g. `fl oz (US fluid ounces)`) */
27
- get title() {
28
- return `${this.abbr} (${this.many})`;
29
- }
30
20
  constructor(
31
21
  /** `UnitList` this unit belongs to. */
32
22
  list,
33
23
  /** String key for this unit, e.g. `kilometer` */
34
24
  key,
35
25
  /** Props to configure this unit. */
36
- { abbr = key.slice(0, 1), one = key.replace(/-/, " "), many = `${one}s`, options, to }) {
26
+ { to, ...options }) {
37
27
  this.list = list;
38
28
  this.key = key;
39
- this.abbr = abbr;
40
- this.one = one;
41
- this.many = many;
42
29
  this.options = options;
43
30
  this._to = to;
44
31
  }
@@ -83,13 +70,7 @@ export class Unit {
83
70
  * - Polyfills unsupported units to use long/short form based on `options.unitDisplay`.
84
71
  */
85
72
  format(amount, options) {
86
- // If possible use `Intl` so that the user's locale is used.
87
- if (Intl.supportedValuesOf("unit").includes(this.key))
88
- return formatNumber(amount, { style: "unit", unitDisplay: "short", ...this.options, ...options, unit: this.key });
89
- // Otherwise, use the default number format.
90
- // If unitDisplay is "long" use the singular/plural form.
91
- const o = { style: "decimal", unitDisplay: "short", ...this.options, ...options };
92
- return o.unitDisplay === "long" ? pluralizeQuantity(amount, this.one, this.many, o) : formatQuantity(amount, this.abbr, o);
73
+ return formatUnit(amount, this.key, { ...this.options, ...options });
93
74
  }
94
75
  }
95
76
  /**
@@ -185,14 +166,14 @@ const TIME_OPTIONS = {
185
166
  };
186
167
  /** Time units. */
187
168
  export const TIME_UNITS = new UnitList({
188
- millisecond: { abbr: "ms", options: TIME_OPTIONS },
189
- second: { to: { millisecond: SECOND }, options: TIME_OPTIONS },
190
- minute: { to: { millisecond: MINUTE }, options: TIME_OPTIONS },
191
- hour: { to: { millisecond: HOUR }, options: TIME_OPTIONS },
192
- day: { to: { millisecond: DAY }, options: TIME_OPTIONS },
193
- week: { to: { millisecond: WEEK }, options: TIME_OPTIONS },
194
- month: { to: { millisecond: MONTH }, options: TIME_OPTIONS },
195
- year: { to: { millisecond: YEAR }, options: TIME_OPTIONS },
169
+ millisecond: { ...TIME_OPTIONS, abbr: "ms" },
170
+ second: { ...TIME_OPTIONS, to: { millisecond: SECOND } },
171
+ minute: { ...TIME_OPTIONS, to: { millisecond: MINUTE } },
172
+ hour: { ...TIME_OPTIONS, to: { millisecond: HOUR } },
173
+ day: { ...TIME_OPTIONS, to: { millisecond: DAY } },
174
+ week: { ...TIME_OPTIONS, to: { millisecond: WEEK } },
175
+ month: { ...TIME_OPTIONS, to: { millisecond: MONTH } },
176
+ year: { ...TIME_OPTIONS, to: { millisecond: YEAR } },
196
177
  });
197
178
  /** Length units. */
198
179
  export const LENGTH_UNITS = new UnitList({
@@ -293,6 +274,16 @@ export const TEMPERATURE_UNITS = new UnitList({
293
274
  many: "degrees Celsius",
294
275
  to: { fahrenheit: n => n * (9 / 5) + 32, kelvin: n => n + 273.15 },
295
276
  },
296
- fahrenheit: { abbr: "°F", one: "degree Fahrenheit", many: "degrees Fahrenheit", to: { celsius: n => (n - 32) * (5 / 9) } },
297
- kelvin: { abbr: "°K", one: "degree Kelvin", many: "degrees Kelvin", to: { celsius: n => n - 273.15 } },
277
+ fahrenheit: {
278
+ abbr: "°F",
279
+ one: "degree Fahrenheit",
280
+ many: "degrees Fahrenheit",
281
+ to: { celsius: n => (n - 32) * (5 / 9) },
282
+ },
283
+ kelvin: {
284
+ abbr: "°K",
285
+ one: "degree Kelvin",
286
+ many: "degrees Kelvin",
287
+ to: { celsius: n => n - 273.15 },
288
+ },
298
289
  });