shelving 1.185.2 → 1.186.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.185.2",
3
+ "version": "1.186.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@ export class ChoiceSchema extends Schema {
26
26
  validate(unsafeValue = this.value) {
27
27
  if (typeof unsafeValue === "string" && isOption(this.options, unsafeValue))
28
28
  return unsafeValue;
29
- throw unsafeValue ? "Unknown value" : "Required";
29
+ throw unsafeValue ? `Unknown ${this.one}` : "Required";
30
30
  }
31
31
  // Implement iterable.
32
32
  *[Symbol.iterator]() {
@@ -0,0 +1,33 @@
1
+ import { type CurrencyCode } from "../util/currency.js";
2
+ import { NumberSchema, type NumberSchemaOptions } from "./NumberSchema.js";
3
+ /** Allowed options for `CurrencyAmountSchema`. */
4
+ export interface CurrencyAmountSchemaOptions extends NumberSchemaOptions {
5
+ readonly symbol?: string | undefined;
6
+ readonly currency: CurrencyCode;
7
+ }
8
+ /**
9
+ * Schema representing a numeric amount in a specific currency.
10
+ *
11
+ * - The validation step is inferred from the currency's minor units.
12
+ * - The default formatter renders amounts using shelving's currency helpers.
13
+ *
14
+ * @example
15
+ * const PRICE = new CurrencyAmountSchema({ currency: "GBP", min: 0 });
16
+ * PRICE.validate("12.345"); // 12.35
17
+ * PRICE.format(12.3); // "£12.30"
18
+ */
19
+ export declare class CurrencyAmountSchema extends NumberSchema {
20
+ readonly currency: CurrencyCode;
21
+ readonly symbol: string;
22
+ constructor({ currency, one, title, symbol, step, format, ...options }: CurrencyAmountSchemaOptions);
23
+ }
24
+ /** Valid non-negative monetary amount in the a currency. */
25
+ export declare const CURRENCY_AMOUNT: (currency: CurrencyCode) => CurrencyAmountSchema;
26
+ export declare const USD_AMOUNT: CurrencyAmountSchema;
27
+ export declare const GBP_AMOUNT: CurrencyAmountSchema;
28
+ export declare const EUR_AMOUNT: CurrencyAmountSchema;
29
+ /** Valid optional monetary amount in the default currency, or `null`. */
30
+ export declare const NULLABLE_CURRENCY_AMOUNT: (currency: CurrencyCode) => import("./NullableSchema.js").NullableSchema<number>;
31
+ export declare const NULLABLE_USD_AMOUNT: import("./NullableSchema.js").NullableSchema<number>;
32
+ export declare const NULLABLE_GBP_AMOUNT: import("./NullableSchema.js").NullableSchema<number>;
33
+ export declare const NULLABLE_EUR_AMOUNT: import("./NullableSchema.js").NullableSchema<number>;
@@ -0,0 +1,41 @@
1
+ import { getCurrencyStep, getCurrencySymbol, requireCurrencyCode } from "../util/currency.js";
2
+ import { formatCurrency } from "../util/format.js";
3
+ import { NULLABLE } from "./NullableSchema.js";
4
+ import { NumberSchema } from "./NumberSchema.js";
5
+ /**
6
+ * Schema representing a numeric amount in a specific currency.
7
+ *
8
+ * - The validation step is inferred from the currency's minor units.
9
+ * - The default formatter renders amounts using shelving's currency helpers.
10
+ *
11
+ * @example
12
+ * const PRICE = new CurrencyAmountSchema({ currency: "GBP", min: 0 });
13
+ * PRICE.validate("12.345"); // 12.35
14
+ * PRICE.format(12.3); // "£12.30"
15
+ */
16
+ export class CurrencyAmountSchema extends NumberSchema {
17
+ currency;
18
+ symbol;
19
+ constructor({ currency, one = "amount", title = "Amount", symbol, step, format = (value, options) => formatCurrency(value, currency, options), ...options }) {
20
+ const validCurrency = requireCurrencyCode(currency, CurrencyAmountSchema);
21
+ super({
22
+ one,
23
+ title,
24
+ step: step ?? getCurrencyStep(validCurrency, CurrencyAmountSchema),
25
+ format,
26
+ ...options,
27
+ });
28
+ this.currency = validCurrency;
29
+ this.symbol = symbol ?? getCurrencySymbol(validCurrency, CurrencyAmountSchema);
30
+ }
31
+ }
32
+ /** Valid non-negative monetary amount in the a currency. */
33
+ export const CURRENCY_AMOUNT = (currency) => new CurrencyAmountSchema({ currency });
34
+ export const USD_AMOUNT = new CurrencyAmountSchema({ currency: "USD" });
35
+ export const GBP_AMOUNT = new CurrencyAmountSchema({ currency: "GBP" });
36
+ export const EUR_AMOUNT = new CurrencyAmountSchema({ currency: "EUR" });
37
+ /** Valid optional monetary amount in the default currency, or `null`. */
38
+ export const NULLABLE_CURRENCY_AMOUNT = (currency) => NULLABLE(CURRENCY_AMOUNT(currency));
39
+ export const NULLABLE_USD_AMOUNT = NULLABLE(USD_AMOUNT);
40
+ export const NULLABLE_GBP_AMOUNT = NULLABLE(GBP_AMOUNT);
41
+ export const NULLABLE_EUR_AMOUNT = NULLABLE(EUR_AMOUNT);
@@ -0,0 +1,21 @@
1
+ import type { ImmutableArray } from "../util/array.js";
2
+ import { type CurrencyCode } from "../util/currency.js";
3
+ import type { StringSchemaOptions } from "./StringSchema.js";
4
+ import { StringSchema } from "./StringSchema.js";
5
+ /** Options for a `CurrencyCodeSchema` */
6
+ export interface CurrencyCodeSchemaOptions extends Omit<StringSchemaOptions, "input" | "min" | "max" | "match" | "multiline"> {
7
+ currencies?: ImmutableArray<CurrencyCode>;
8
+ }
9
+ /**
10
+ * Type of `StringSchema` that defines a valid currency code.
11
+ */
12
+ export declare class CurrencyCodeSchema extends StringSchema {
13
+ readonly currencies: ImmutableArray<CurrencyCode>;
14
+ constructor({ one, title, currencies, ...options }: CurrencyCodeSchemaOptions);
15
+ sanitize(insaneString: string): string;
16
+ validate(value?: unknown): string;
17
+ }
18
+ /** Valid currency code, e.g. `GBP` */
19
+ export declare const CURRENCY_CODE: CurrencyCodeSchema;
20
+ /** Valid currency code, e.g. `GBP`, or `null` */
21
+ export declare const NULLABLE_CURRENCY_CODE: import("./NullableSchema.js").NullableSchema<string>;
@@ -0,0 +1,35 @@
1
+ import { CURRENCY_CODES } from "../util/currency.js";
2
+ import { NULLABLE } from "./NullableSchema.js";
3
+ import { StringSchema } from "./StringSchema.js";
4
+ /**
5
+ * Type of `StringSchema` that defines a valid currency code.
6
+ */
7
+ export class CurrencyCodeSchema extends StringSchema {
8
+ currencies;
9
+ constructor({ one = "currency", title = "Currency", currencies = CURRENCY_CODES, ...options }) {
10
+ super({
11
+ one,
12
+ title,
13
+ ...options,
14
+ min: 3,
15
+ max: 3, // Valid currency code is 3 uppercase letters.
16
+ case: "upper",
17
+ match: /^[A-Z]{3}$/, // Valid currency code is 3 uppercase letters.
18
+ });
19
+ this.currencies = currencies;
20
+ }
21
+ sanitize(insaneString) {
22
+ // Strip characters that aren't A-Z (including whitespace).
23
+ return super.sanitize(insaneString).replace(/[^A-Z+]/g, "");
24
+ }
25
+ validate(value) {
26
+ const currency = super.validate(value);
27
+ if (!this.currencies.includes(currency))
28
+ throw "Unknown currency code";
29
+ return currency;
30
+ }
31
+ }
32
+ /** Valid currency code, e.g. `GBP` */
33
+ export const CURRENCY_CODE = new CurrencyCodeSchema({});
34
+ /** Valid currency code, e.g. `GBP`, or `null` */
35
+ export const NULLABLE_CURRENCY_CODE = NULLABLE(CURRENCY_CODE);
@@ -1,4 +1,5 @@
1
1
  import type { PossibleDate } from "../util/date.js";
2
+ import { formatDate } from "../util/format.js";
2
3
  import type { Nullish } from "../util/null.js";
3
4
  import type { SchemaOptions } from "./Schema.js";
4
5
  import { Schema } from "./Schema.js";
@@ -10,6 +11,8 @@ export interface DateSchemaOptions extends SchemaOptions {
10
11
  readonly min?: Nullish<PossibleDate>;
11
12
  readonly max?: Nullish<PossibleDate>;
12
13
  readonly input?: DateInputType | undefined;
14
+ /** Format the date for display in downstream UIs. */
15
+ readonly format?: typeof formatDate | undefined;
13
16
  /**
14
17
  * Rounding step (in milliseconds, because that's the base unit for time).
15
18
  * - E.g. `1000 * 60` will round to the nearest minute.
@@ -18,15 +21,15 @@ export interface DateSchemaOptions extends SchemaOptions {
18
21
  readonly step?: number | undefined;
19
22
  }
20
23
  export declare class DateSchema extends Schema<string> {
21
- readonly value: PossibleDate;
24
+ readonly value: PossibleDate | undefined;
22
25
  readonly min: Date | undefined;
23
26
  readonly max: Date | undefined;
24
27
  readonly input: DateInputType;
25
28
  readonly step: number | undefined;
26
- constructor({ one, min, max, value, input, step, ...options }: DateSchemaOptions);
29
+ format: (value: Date) => string;
30
+ constructor({ one, min, max, value, input, step, format, ...options }: DateSchemaOptions);
27
31
  validate(value?: unknown): string;
28
32
  stringify(value: Date): string;
29
- format(value: Date): string;
30
33
  }
31
34
  /** Valid date, e.g. `2005-09-12` (required because falsy values are invalid). */
32
35
  export declare const DATE: DateSchema;
@@ -8,30 +8,29 @@ export class DateSchema extends Schema {
8
8
  max;
9
9
  input;
10
10
  step;
11
- constructor({ one = "date", min, max, value = "now", input = "date", step, ...options }) {
11
+ format;
12
+ constructor({ one = "date", min, max, value, input = "date", step, format = formatDate, ...options }) {
12
13
  super({ one, title: "Date", value, ...options });
13
14
  this.min = getDate(min);
14
15
  this.max = getDate(max);
15
16
  this.input = input;
16
17
  this.step = step;
18
+ this.format = format;
17
19
  }
18
20
  validate(value = this.value) {
19
21
  const date = getDate(value);
20
22
  if (!date)
21
- throw value ? "Invalid date" : "Required";
22
- const rounded = typeof this.step === "number" ? new Date(roundStep(date.getTime(), this.step)) : date;
23
- if (this.min && rounded < this.min)
23
+ throw value ? `Invalid ${this.one} format` : "Required";
24
+ const stepped = typeof this.step === "number" ? new Date(roundStep(date.getTime(), this.step)) : date;
25
+ if (this.min && stepped < this.min)
24
26
  throw `Minimum ${this.format(this.min)}`;
25
- if (this.max && rounded > this.max)
27
+ if (this.max && stepped > this.max)
26
28
  throw `Maximum ${this.format(this.max)}`;
27
- return this.stringify(rounded);
29
+ return this.stringify(stepped);
28
30
  }
29
31
  stringify(value) {
30
32
  return requireDateString(value);
31
33
  }
32
- format(value) {
33
- return formatDate(value);
34
- }
35
34
  }
36
35
  /** Valid date, e.g. `2005-09-12` (required because falsy values are invalid). */
37
36
  export const DATE = new DateSchema({});
@@ -6,8 +6,7 @@ import { DateSchema, type DateSchemaOptions } from "./DateSchema.js";
6
6
  * - If you wish to define an _abstract_ time without a timezone, e.g. a daily alarm, use `TimeSchema` instead.
7
7
  */
8
8
  export declare class DateTimeSchema extends DateSchema {
9
- constructor({ one, title, input, ...options }: DateSchemaOptions);
10
- format(value: Date): string;
9
+ constructor({ one, title, input, format, ...options }: DateSchemaOptions);
11
10
  stringify(value: Date): string;
12
11
  }
13
12
  /** Valid datetime, e.g. `2005-09-12T08:00:00Z` (required because falsy values are invalid). */
@@ -8,11 +8,8 @@ import { NULLABLE } from "./NullableSchema.js";
8
8
  * - If you wish to define an _abstract_ time without a timezone, e.g. a daily alarm, use `TimeSchema` instead.
9
9
  */
10
10
  export class DateTimeSchema extends DateSchema {
11
- constructor({ one = "time", title = "Time", input = "datetime-local", ...options }) {
12
- super({ one, title, input, ...options });
13
- }
14
- format(value) {
15
- return formatDateTime(value);
11
+ constructor({ one = "time", title = "Time", input = "datetime-local", format = formatDateTime, ...options }) {
12
+ super({ one, title, input, format, ...options });
16
13
  }
17
14
  stringify(value) {
18
15
  return value.toISOString();
@@ -13,9 +13,9 @@ export class FileSchema extends StringSchema {
13
13
  const path = super.validate(unsafeValue);
14
14
  const extension = getFileExtension(path);
15
15
  if (!extension)
16
- throw "Must be file name with extension";
16
+ throw `Must have extension`;
17
17
  if (this.types && !isProp(this.types, extension))
18
- throw "Invalid file type";
18
+ throw `Invalid extension`;
19
19
  return path;
20
20
  }
21
21
  }
@@ -1,3 +1,4 @@
1
+ import { formatNumber } from "../util/format.js";
1
2
  import type { SchemaOptions } from "./Schema.js";
2
3
  import { Schema } from "./Schema.js";
3
4
  /** Allowed options for `NumberSchema` */
@@ -6,17 +7,20 @@ export interface NumberSchemaOptions extends SchemaOptions {
6
7
  readonly min?: number | undefined;
7
8
  readonly max?: number | undefined;
8
9
  readonly step?: number | undefined;
10
+ /** Format the number for display in downstream UIs. */
11
+ readonly format?: typeof formatNumber | undefined;
9
12
  }
10
13
  /** Schema that defines a valid number. */
11
14
  export declare class NumberSchema extends Schema<number> {
12
- readonly value: number;
15
+ readonly value: number | undefined;
13
16
  readonly min: number;
14
17
  readonly max: number;
15
18
  readonly step: number | undefined;
16
- constructor({ one, title, min, max, step, value, ...options }: NumberSchemaOptions);
17
- validate(unsafeValue?: unknown): number;
19
+ format: (value: number) => string;
20
+ constructor({ one, title, min, max, step, format, value, ...options }: NumberSchemaOptions);
21
+ validate(value?: unknown): number;
18
22
  }
19
- /** Valid number, e.g. `2048.12345` or `0` zero. */
23
+ /** Valid number, e.g. `2048.12345` or `0` zero and a default value of zero. */
20
24
  export declare const NUMBER: NumberSchema;
21
25
  /** Valid optional number, e.g. `2048.12345` or `0` zero, or `null` */
22
26
  export declare const NULLABLE_NUMBER: import("./NullableSchema.js").NullableSchema<number>;
@@ -7,38 +7,40 @@ export class NumberSchema extends Schema {
7
7
  min;
8
8
  max;
9
9
  step;
10
- constructor({ one = "number", title = "Number", min = Number.NEGATIVE_INFINITY, max = Number.POSITIVE_INFINITY, step, value = 0, ...options }) {
10
+ format;
11
+ constructor({ one = "number", title = "Number", min = Number.NEGATIVE_INFINITY, max = Number.POSITIVE_INFINITY, step, format = formatNumber, value, ...options }) {
11
12
  super({ one, title, value, ...options });
12
13
  this.min = min;
13
14
  this.max = max;
14
15
  this.step = step;
16
+ this.format = format;
15
17
  }
16
- validate(unsafeValue = this.value) {
17
- const optionalNumber = getNumber(unsafeValue);
18
- if (typeof optionalNumber !== "number")
19
- throw "Must be number";
20
- const roundedNumber = typeof this.step === "number" ? roundStep(optionalNumber, this.step) : optionalNumber;
21
- if (roundedNumber < this.min)
22
- throw !optionalNumber ? "Required" : `Minimum ${formatNumber(this.min)}`;
23
- if (roundedNumber > this.max)
24
- throw `Maximum ${formatNumber(this.max)}`;
25
- return roundedNumber;
18
+ validate(value = this.value) {
19
+ const number = getNumber(value);
20
+ if (typeof number !== "number")
21
+ throw value ? `Must be ${this.one}` : "Required";
22
+ const stepped = typeof this.step === "number" ? roundStep(number, this.step) : number;
23
+ if (stepped < this.min)
24
+ throw !number ? "Required" : `Minimum ${this.format(this.min)}`;
25
+ if (stepped > this.max)
26
+ throw `Maximum ${this.format(this.max)}`;
27
+ return stepped;
26
28
  }
27
29
  }
28
- /** Valid number, e.g. `2048.12345` or `0` zero. */
30
+ /** Valid number, e.g. `2048.12345` or `0` zero and a default value of zero. */
29
31
  export const NUMBER = new NumberSchema({ title: "Number" });
30
32
  /** Valid optional number, e.g. `2048.12345` or `0` zero, or `null` */
31
33
  export const NULLABLE_NUMBER = NULLABLE(NUMBER);
32
34
  /** Valid integer number, e.g. `2048` or `0` zero. */
33
- export const INTEGER = new NumberSchema({ step: 1, min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER, value: 0 });
35
+ export const INTEGER = new NumberSchema({ step: 1, min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER });
34
36
  /** Valid positive integer number, e.g. `1,2,3` (not including zero). */
35
- export const POSITIVE_INTEGER = new NumberSchema({ step: 1, min: 1, max: Number.MAX_SAFE_INTEGER, value: 1 });
37
+ export const POSITIVE_INTEGER = new NumberSchema({ step: 1, min: 1, max: Number.MAX_SAFE_INTEGER });
36
38
  /** Valid non-negative integer number, e.g. `0,1,2,3` (including zero). */
37
- export const NON_NEGATIVE_INTEGER = new NumberSchema({ step: 1, min: 0, max: Number.MAX_SAFE_INTEGER, value: 0 });
39
+ export const NON_NEGATIVE_INTEGER = new NumberSchema({ step: 1, min: 0, max: Number.MAX_SAFE_INTEGER });
38
40
  /** Valid negative integer number, e.g. `-1,-2,-3` (not including zero). */
39
- export const NEGATIVE_INTEGER = new NumberSchema({ step: 1, min: Number.MIN_SAFE_INTEGER, max: -1, value: -1 });
41
+ export const NEGATIVE_INTEGER = new NumberSchema({ step: 1, min: Number.MIN_SAFE_INTEGER, max: -1 });
40
42
  /** Valid non-positive integer number, e.g. `0,-1,-2,-3` (including zero). */
41
- export const NON_POSITIVE_INTEGER = new NumberSchema({ step: 1, min: Number.MIN_SAFE_INTEGER, max: 0, value: 0 });
43
+ export const NON_POSITIVE_INTEGER = new NumberSchema({ step: 1, min: Number.MIN_SAFE_INTEGER, max: 0 });
42
44
  /** Valid optional integer number, e.g. `2048` or `0` zero, or `null` */
43
45
  export const NULLABLE_INTEGER = NULLABLE(INTEGER);
44
46
  /** Valid Unix timestamp (including milliseconds). */
@@ -47,7 +49,6 @@ export const TIMESTAMP = new NumberSchema({
47
49
  step: 1,
48
50
  min: Number.MIN_SAFE_INTEGER,
49
51
  max: Number.MAX_SAFE_INTEGER,
50
- value: 0,
51
52
  });
52
53
  /** Valid Unix timestamp (including milliseconds). */
53
54
  export const NULLABLE_TIMESTAMP = NULLABLE_INTEGER;
@@ -1,12 +1,15 @@
1
1
  import type { StringSchemaOptions } from "./StringSchema.js";
2
2
  import { StringSchema } from "./StringSchema.js";
3
+ /** Options for a `PhoneSchema` */
4
+ export interface PhoneSchemaOptions extends Omit<StringSchemaOptions, "input" | "min" | "max" | "match" | "multiline"> {
5
+ }
3
6
  /**
4
7
  * Type of `StringSchema` that defines a valid phone number.
5
8
  * - Multiple string formats are automatically converted to E.164 format (starting with `+` plus).
6
9
  * - Falsy values are converted to `""` empty string.
7
10
  */
8
11
  export declare class PhoneSchema extends StringSchema {
9
- constructor({ one, title, ...options }: Omit<StringSchemaOptions, "input" | "min" | "max" | "match" | "multiline">);
12
+ constructor({ one, title, ...options }: PhoneSchemaOptions);
10
13
  sanitize(insaneString: string): string;
11
14
  }
12
15
  /** Valid phone number, e.g. `+441234567890` */
@@ -1,9 +1,5 @@
1
1
  import { NULLABLE } from "./NullableSchema.js";
2
2
  import { StringSchema } from "./StringSchema.js";
3
- // Valid phone number is max 16 digits made up of:
4
- // - Country code (`+` plus character and 1-3 digits, e.g. `+44` or `+1`).
5
- // - Subscriber number (5-12 digits — the Solomon Islands have five-digit phone numbers apparently).
6
- const PHONE_REGEXP = /^\+[1-9][0-9]{0,2}[0-9]{5,12}$/;
7
3
  /**
8
4
  * Type of `StringSchema` that defines a valid phone number.
9
5
  * - Multiple string formats are automatically converted to E.164 format (starting with `+` plus).
@@ -17,8 +13,12 @@ export class PhoneSchema extends StringSchema {
17
13
  ...options,
18
14
  input: "tel",
19
15
  min: 1,
20
- max: 16, // Valid phone number is 16 digits or fewer (15 numerals with a leading `+` plus).
21
- match: PHONE_REGEXP,
16
+ // Valid phone number is 16 digits or fewer (15 numerals with a leading `+` plus).
17
+ max: 16,
18
+ // Valid phone number is max 16 digits made up of:
19
+ // - Country code (`+` plus character and 1-3 digits, e.g. `+44` or `+1`).
20
+ // - Subscriber number (5-12 digits — the Solomon Islands have five-digit phone numbers apparently).
21
+ match: /^\+[1-9][0-9]{0,2}[0-9]{5,12}$/,
22
22
  });
23
23
  }
24
24
  sanitize(insaneString) {
@@ -35,8 +35,8 @@ export declare class StringSchema extends Schema<string> {
35
35
  readonly multiline: boolean;
36
36
  readonly match: RegExp | undefined;
37
37
  readonly case: "upper" | "lower" | undefined;
38
- constructor({ min, max, value, multiline, match, case: _case, input, ...options }: StringSchemaOptions);
39
- validate(unsafeValue?: unknown): string;
38
+ constructor({ one, min, max, value, multiline, match, case: _case, input, ...options }: StringSchemaOptions);
39
+ validate(value?: unknown): string;
40
40
  /** Sanitize the string by removing unwanted characters. */
41
41
  sanitize(str: string): string;
42
42
  }
@@ -23,8 +23,8 @@ export class StringSchema extends Schema {
23
23
  multiline;
24
24
  match;
25
25
  case;
26
- constructor({ min = 0, max = Number.POSITIVE_INFINITY, value = "", multiline = false, match, case: _case, input = "text", ...options }) {
27
- super({ value, ...options });
26
+ constructor({ one = "string", min = 0, max = Number.POSITIVE_INFINITY, value = "", multiline = false, match, case: _case, input = "text", ...options }) {
27
+ super({ one, value, ...options });
28
28
  this.min = min;
29
29
  this.max = max;
30
30
  this.multiline = multiline;
@@ -32,18 +32,18 @@ export class StringSchema extends Schema {
32
32
  this.case = _case;
33
33
  this.input = input;
34
34
  }
35
- validate(unsafeValue = this.value) {
36
- const possibleString = typeof unsafeValue === "number" ? unsafeValue.toString() : unsafeValue;
37
- if (typeof possibleString !== "string")
38
- throw "Must be string";
39
- const saneString = this.sanitize(possibleString);
40
- if (saneString.length < this.min)
41
- throw possibleString.length ? `Minimum ${this.min} characters` : "Required";
42
- if (saneString.length > this.max)
35
+ validate(value = this.value) {
36
+ const str = typeof value === "number" ? value.toString() : value;
37
+ if (typeof str !== "string")
38
+ throw value ? `Must be ${this.one}` : "Required";
39
+ const sane = this.sanitize(str);
40
+ if (this.match && !this.match.test(sane))
41
+ throw str.length ? `Invalid ${this.one}` : "Required";
42
+ if (sane.length < this.min)
43
+ throw str.length ? `Minimum ${this.min} characters` : "Required";
44
+ if (sane.length > this.max)
43
45
  throw `Maximum ${this.max} characters`;
44
- if (this.match && !this.match.test(saneString))
45
- throw saneString ? "Invalid format" : "Required";
46
- return saneString;
46
+ return sane;
47
47
  }
48
48
  /** Sanitize the string by removing unwanted characters. */
49
49
  sanitize(str) {
@@ -1,9 +1,8 @@
1
1
  import { DateSchema, type DateSchemaOptions } from "./DateSchema.js";
2
2
  /** Define a valid time in 24h hh:mm:ss.fff format, e.g. `23:59` or `24:00 */
3
3
  export declare class TimeSchema extends DateSchema {
4
- constructor({ one, title, input, ...options }: DateSchemaOptions);
4
+ constructor({ one, title, input, format, ...options }: DateSchemaOptions);
5
5
  stringify(value: Date): string;
6
- format(value: Date): string;
7
6
  }
8
7
  /** Valid time, e.g. `2005-09-12` (required because falsy values are invalid). */
9
8
  export declare const TIME: TimeSchema;
@@ -4,15 +4,12 @@ import { DateSchema } from "./DateSchema.js";
4
4
  import { NULLABLE } from "./NullableSchema.js";
5
5
  /** Define a valid time in 24h hh:mm:ss.fff format, e.g. `23:59` or `24:00 */
6
6
  export class TimeSchema extends DateSchema {
7
- constructor({ one = "time", title = "Time", input = "time", ...options }) {
8
- super({ one, title, input, ...options });
7
+ constructor({ one = "time", title = "Time", input = "time", format = formatTime, ...options }) {
8
+ super({ one, title, input, format, ...options });
9
9
  }
10
10
  stringify(value) {
11
11
  return requireTimeString(value);
12
12
  }
13
- format(value) {
14
- return formatTime(value);
15
- }
16
13
  }
17
14
  /** Valid time, e.g. `2005-09-12` (required because falsy values are invalid). */
18
15
  export const TIME = new TimeSchema({});
@@ -25,9 +25,9 @@ export class URISchema extends StringSchema {
25
25
  const str = super.validate(unsafeValue);
26
26
  const uri = getURI(str);
27
27
  if (!uri)
28
- throw str ? "Invalid format" : "Required";
28
+ throw str ? `Invalid ${this.one} format` : "Required";
29
29
  if (this.schemes && !this.schemes.includes(uri.protocol))
30
- throw "Invalid URI scheme";
30
+ throw `Invalid ${this.one} scheme`;
31
31
  return uri.href;
32
32
  }
33
33
  }
@@ -28,9 +28,9 @@ export class URLSchema extends StringSchema {
28
28
  const str = super.validate(unsafeValue);
29
29
  const url = getURL(str, this.base);
30
30
  if (!url)
31
- throw str ? "Invalid format" : "Required";
31
+ throw str ? `Invalid ${this.one} format` : "Required";
32
32
  if (this.schemes && !this.schemes.includes(url.protocol))
33
- throw "Invalid URL scheme";
33
+ throw `Invalid ${this.one} scheme`;
34
34
  return url.href;
35
35
  }
36
36
  }
package/schema/index.d.ts CHANGED
@@ -4,6 +4,8 @@ export * from "./BooleanSchema.js";
4
4
  export * from "./ChoiceSchema.js";
5
5
  export * from "./ColorSchema.js";
6
6
  export * from "./CountrySchema.js";
7
+ export * from "./CurrencyAmountSchema.js";
8
+ export * from "./CurrencyCodeSchema.js";
7
9
  export * from "./DataSchema.js";
8
10
  export * from "./DateSchema.js";
9
11
  export * from "./DateTimeSchema.js";
package/schema/index.js CHANGED
@@ -4,6 +4,8 @@ export * from "./BooleanSchema.js";
4
4
  export * from "./ChoiceSchema.js";
5
5
  export * from "./ColorSchema.js";
6
6
  export * from "./CountrySchema.js";
7
+ export * from "./CurrencyAmountSchema.js";
8
+ export * from "./CurrencyCodeSchema.js";
7
9
  export * from "./DataSchema.js";
8
10
  export * from "./DateSchema.js";
9
11
  export * from "./DateTimeSchema.js";
@@ -0,0 +1,28 @@
1
+ import type { ImmutableArray } from "./array.js";
2
+ import type { AnyCaller } from "./function.js";
3
+ /** ISO 4217 currency code, e.g. `GBP` or `USD`. */
4
+ export type CurrencyCode = string;
5
+ /** Array of all supported currency codes in this runtime. */
6
+ export declare const CURRENCY_CODES: ImmutableArray<CurrencyCode>;
7
+ /**
8
+ * Require that a value is a valid ISO 4217 currency code, and return it as a `Currency` type.
9
+ */
10
+ export declare function getCurrencyCode(value: string): CurrencyCode | undefined;
11
+ /**
12
+ * Require that a value is a valid ISO 4217 currency code, and return it as a `Currency` type.
13
+ */
14
+ export declare function requireCurrencyCode(value: string, caller?: AnyCaller): CurrencyCode;
15
+ /**
16
+ * Get the display symbol used for a currency.
17
+ *
18
+ * @throws {RequiredError} If the currency code is malformed or unsupported.
19
+ *
20
+ * @example getCurrencySymbol("GBP"); // "£"
21
+ */
22
+ export declare function getCurrencySymbol(currency: CurrencyCode, caller?: AnyCaller): string;
23
+ /**
24
+ * Get the "step" value for a currency, i.e. the smallest fractional unit that is used for that currency.
25
+ * - E.g. `0.01` for USD, `0.001` for some cryptocurrencies, and `1` for JPY.
26
+ * @throws {RequiredError} If the currency code is malformed or unsupported.
27
+ */
28
+ export declare function getCurrencyStep(currency: CurrencyCode, caller?: AnyCaller): number;
@@ -0,0 +1,46 @@
1
+ import { RequiredError } from "../error/RequiredError.js";
2
+ /** Array of all supported currency codes in this runtime. */
3
+ export const CURRENCY_CODES = Intl.supportedValuesOf("currency");
4
+ /**
5
+ * Require that a value is a valid ISO 4217 currency code, and return it as a `Currency` type.
6
+ */
7
+ export function getCurrencyCode(value) {
8
+ const currency = value.toUpperCase().trim();
9
+ return CURRENCY_CODES.includes(currency) ? currency : undefined;
10
+ }
11
+ /**
12
+ * Require that a value is a valid ISO 4217 currency code, and return it as a `Currency` type.
13
+ */
14
+ export function requireCurrencyCode(value, caller = requireCurrencyCode) {
15
+ const currency = getCurrencyCode(value);
16
+ if (!currency)
17
+ throw new RequiredError("Unknown currency code", { received: value, caller });
18
+ return currency;
19
+ }
20
+ function _formatter(currency, caller) {
21
+ return new Intl.NumberFormat(undefined, {
22
+ style: "currency",
23
+ currency: requireCurrencyCode(currency, caller),
24
+ currencyDisplay: "narrowSymbol",
25
+ });
26
+ }
27
+ const _isCurrencyNumberPart = ({ type }) => type === "currency";
28
+ /**
29
+ * Get the display symbol used for a currency.
30
+ *
31
+ * @throws {RequiredError} If the currency code is malformed or unsupported.
32
+ *
33
+ * @example getCurrencySymbol("GBP"); // "£"
34
+ */
35
+ export function getCurrencySymbol(currency, caller = getCurrencySymbol) {
36
+ return _formatter(currency, caller).formatToParts(0).find(_isCurrencyNumberPart)?.value;
37
+ }
38
+ /**
39
+ * Get the "step" value for a currency, i.e. the smallest fractional unit that is used for that currency.
40
+ * - E.g. `0.01` for USD, `0.001` for some cryptocurrencies, and `1` for JPY.
41
+ * @throws {RequiredError} If the currency code is malformed or unsupported.
42
+ */
43
+ export function getCurrencyStep(currency, caller = getCurrencyStep) {
44
+ const { minimumFractionDigits = 0 } = _formatter(currency, caller).resolvedOptions();
45
+ return 1 / 10 ** minimumFractionDigits;
46
+ }
package/util/format.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { isArray } from "./array.js";
2
2
  import { SECOND } from "./constants.js";
3
+ import { requireCurrencyCode } from "./currency.js";
3
4
  import { isDate, requireDate } from "./date.js";
4
5
  import { getBestTimeUnit, getMilliseconds } from "./duration.js";
5
6
  import { getPercent } from "./number.js";
@@ -39,7 +40,7 @@ export function formatUnit(num, unit, options) {
39
40
  export function formatCurrency(amount, currency, options) {
40
41
  return Intl.NumberFormat(undefined, {
41
42
  style: "currency",
42
- currency,
43
+ currency: requireCurrencyCode(currency, formatCurrency),
43
44
  ...options,
44
45
  }).format(amount);
45
46
  }
package/util/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from "./class.js";
10
10
  export * from "./color.js";
11
11
  export * from "./constants.js";
12
12
  export * from "./crypto.js";
13
+ export * from "./currency.js";
13
14
  export * from "./data.js";
14
15
  export * from "./date.js";
15
16
  export * from "./debug.js";
package/util/index.js CHANGED
@@ -10,6 +10,7 @@ export * from "./class.js";
10
10
  export * from "./color.js";
11
11
  export * from "./constants.js";
12
12
  export * from "./crypto.js";
13
+ export * from "./currency.js";
13
14
  export * from "./data.js";
14
15
  export * from "./date.js";
15
16
  export * from "./debug.js";