shelving 1.42.0 → 1.45.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/db/Database.d.ts CHANGED
@@ -29,10 +29,10 @@ export declare class Database<V extends Validators<Datas> = Validators<Datas>> {
29
29
  }
30
30
  /** A documents reference within a specific database. */
31
31
  export declare class DatabaseQuery<T extends Data = Data> extends Query<T> implements Observable<Results<T>>, Validatable<Entries<T>>, Iterable<Entry<T>> {
32
- readonly provider: Provider;
32
+ readonly db: Database;
33
33
  readonly validator: Validator<T>;
34
34
  readonly collection: string;
35
- constructor(provider: Provider, validator: Validator<T>, collection: string, filters?: Filters<T>, sorts?: Sorts<T>, limit?: number | null);
35
+ constructor(db: Database, validator: Validator<T>, collection: string, filters?: Filters<T>, sorts?: Sorts<T>, limit?: number | null);
36
36
  /** Reference a document in this query's collection. */
37
37
  doc(id: string): DatabaseDocument<T>;
38
38
  /**
@@ -114,11 +114,11 @@ export declare class DatabaseQuery<T extends Data = Data> extends Query<T> imple
114
114
  export declare function getQueryData<T extends Data>(entries: Entries<T>, ref: DatabaseQuery<T>): Entry<T>;
115
115
  /** A document reference within a specific database. */
116
116
  export declare class DatabaseDocument<T extends Data = Data> implements Observable<Result<T>>, Validatable<T> {
117
- readonly provider: Provider;
117
+ readonly db: Database;
118
118
  readonly validator: Validator<T>;
119
119
  readonly collection: string;
120
120
  readonly id: string;
121
- constructor(provider: Provider, validator: Validator<T>, collection: string, id: string);
121
+ constructor(db: Database, validator: Validator<T>, collection: string, id: string);
122
122
  /** Create a query on this document's collection. */
123
123
  query(filters?: Filters<T>, sorts?: Sorts<T>, limit?: number | null): DatabaseQuery<T>;
124
124
  /** Get an 'optional' reference to this document (uses a `ModelQuery` with an `id` filter). */
package/db/Database.js CHANGED
@@ -18,11 +18,11 @@ export class Database {
18
18
  }
19
19
  /** Create a query on a collection in this model. */
20
20
  query(collection, filters, sorts, limit) {
21
- return new DatabaseQuery(this.provider, this.validators[collection], collection, filters, sorts, limit);
21
+ return new DatabaseQuery(this, this.validators[collection], collection, filters, sorts, limit);
22
22
  }
23
23
  /** Reference a document in a collection in this model. */
24
24
  doc(collection, id) {
25
- return new DatabaseDocument(this.provider, this.validators[collection], collection, id);
25
+ return new DatabaseDocument(this, this.validators[collection], collection, id);
26
26
  }
27
27
  /**
28
28
  * Create a new document with a random ID.
@@ -38,15 +38,15 @@ export class Database {
38
38
  }
39
39
  /** A documents reference within a specific database. */
40
40
  export class DatabaseQuery extends Query {
41
- constructor(provider, validator, collection, filters, sorts, limit) {
41
+ constructor(db, validator, collection, filters, sorts, limit) {
42
42
  super(filters, sorts, limit);
43
- this.provider = provider;
43
+ this.db = db;
44
44
  this.validator = validator;
45
45
  this.collection = collection;
46
46
  }
47
47
  /** Reference a document in this query's collection. */
48
48
  doc(id) {
49
- return new DatabaseDocument(this.provider, this.validator, this.collection, id);
49
+ return new DatabaseDocument(this.db, this.validator, this.collection, id);
50
50
  }
51
51
  /**
52
52
  * Create a new document with a random ID.
@@ -56,21 +56,21 @@ export class DatabaseQuery extends Query {
56
56
  * @return String ID for the created document (possibly promised).
57
57
  */
58
58
  add(data) {
59
- return this.provider.add(this, data);
59
+ return this.db.provider.add(this, data);
60
60
  }
61
61
  /**
62
62
  * Get an iterable that yields the results of this entry.
63
63
  * @return Map containing the results.
64
64
  */
65
65
  get entries() {
66
- return this.provider.getQuery(this);
66
+ return this.db.provider.getQuery(this);
67
67
  }
68
68
  /**
69
69
  * Get an iterable that yields the results of this entry.
70
70
  * @return Map containing the results.
71
71
  */
72
72
  get results() {
73
- return callAsync(getMap, this.provider.getQuery(this));
73
+ return callAsync(getMap, this.db.provider.getQuery(this));
74
74
  }
75
75
  /**
76
76
  * Count the number of results of this set of documents.
@@ -84,7 +84,7 @@ export class DatabaseQuery extends Query {
84
84
  * @return `true` if a document exists or `false` otherwise (possibly promised).
85
85
  */
86
86
  get exists() {
87
- return callAsync(hasItems, this.provider.getQuery(this.max(1)));
87
+ return callAsync(hasItems, this.db.provider.getQuery(this.max(1)));
88
88
  }
89
89
  /**
90
90
  * Get an entry for the first document matched by this query or `undefined` if this query has no results.
@@ -93,7 +93,7 @@ export class DatabaseQuery extends Query {
93
93
  * @throws RequiredError if there were no results for this query.
94
94
  */
95
95
  get result() {
96
- return callAsync(getFirstItem, this.provider.getQuery(this.max(1)));
96
+ return callAsync(getFirstItem, this.db.provider.getQuery(this.max(1)));
97
97
  }
98
98
  /**
99
99
  * Get an entry for the first document matched by this query.
@@ -102,7 +102,7 @@ export class DatabaseQuery extends Query {
102
102
  * @throws RequiredError if there were no results for this query.
103
103
  */
104
104
  get data() {
105
- return callAsync(getQueryData, this.provider.getQuery(this.max(1)), this);
105
+ return callAsync(getQueryData, this.db.provider.getQuery(this.max(1)), this);
106
106
  }
107
107
  /**
108
108
  * Subscribe to all matching documents.
@@ -112,7 +112,7 @@ export class DatabaseQuery extends Query {
112
112
  * @return Function that ends the subscription.
113
113
  */
114
114
  subscribe(next) {
115
- return this.provider.subscribeQuery(this, new ResultsObserver(typeof next === "function" ? { next } : next));
115
+ return this.db.provider.subscribeQuery(this, new ResultsObserver(typeof next === "function" ? { next } : next));
116
116
  }
117
117
  /**
118
118
  * Set all matching documents to the same exact value.
@@ -121,7 +121,7 @@ export class DatabaseQuery extends Query {
121
121
  * @return Nothing (possibly promised).
122
122
  */
123
123
  set(data) {
124
- return this.provider.setQuery(this, data);
124
+ return this.db.provider.setQuery(this, data);
125
125
  }
126
126
  /**
127
127
  * Update all matching documents with the same partial value.
@@ -130,14 +130,14 @@ export class DatabaseQuery extends Query {
130
130
  * @return Nothing (possibly promised).
131
131
  */
132
132
  update(updates) {
133
- return this.provider.updateQuery(this, updates instanceof Update ? updates : new DataUpdate(updates));
133
+ return this.db.provider.updateQuery(this, updates instanceof Update ? updates : new DataUpdate(updates));
134
134
  }
135
135
  /**
136
136
  * Delete all matching documents.
137
137
  * @return Nothing (possibly promised).
138
138
  */
139
139
  delete() {
140
- return this.provider.deleteQuery(this);
140
+ return this.db.provider.deleteQuery(this);
141
141
  }
142
142
  /** Iterate over the resuls (will throw `Promise` if the results are asynchronous). */
143
143
  [Symbol.iterator]() {
@@ -175,33 +175,33 @@ export function getQueryData(entries, ref) {
175
175
  }
176
176
  /** A document reference within a specific database. */
177
177
  export class DatabaseDocument {
178
- constructor(provider, validator, collection, id) {
179
- this.provider = provider;
178
+ constructor(db, validator, collection, id) {
179
+ this.db = db;
180
180
  this.validator = validator;
181
181
  this.collection = collection;
182
182
  this.id = id;
183
183
  }
184
184
  /** Create a query on this document's collection. */
185
185
  query(filters, sorts, limit) {
186
- return new DatabaseQuery(this.provider, this.validator, this.collection, filters, sorts, limit);
186
+ return new DatabaseQuery(this.db, this.validator, this.collection, filters, sorts, limit);
187
187
  }
188
188
  /** Get an 'optional' reference to this document (uses a `ModelQuery` with an `id` filter). */
189
189
  get optional() {
190
- return new DatabaseQuery(this.provider, this.validator, this.collection, new Filters(new EqualFilter("id", this.id)));
190
+ return new DatabaseQuery(this.db, this.validator, this.collection, new Filters(new EqualFilter("id", this.id)));
191
191
  }
192
192
  /**
193
193
  * Does this document exist?
194
194
  * @return `true` if a document exists or `false` otherwise (possibly promised).
195
195
  */
196
196
  get exists() {
197
- return callAsync(Boolean, this.provider.get(this));
197
+ return callAsync(Boolean, this.db.provider.get(this));
198
198
  }
199
199
  /**
200
200
  * Get the result of this document.
201
201
  * @return Document's data, or `undefined` if the document doesn't exist (possibly promised).
202
202
  */
203
203
  get result() {
204
- return this.provider.get(this);
204
+ return this.db.provider.get(this);
205
205
  }
206
206
  /**
207
207
  * Get the data of this document.
@@ -211,7 +211,7 @@ export class DatabaseDocument {
211
211
  * @throws RequiredError if the document's result was undefined.
212
212
  */
213
213
  get data() {
214
- return callAsync(getDocumentData, this.provider.get(this), this);
214
+ return callAsync(getDocumentData, this.db.provider.get(this), this);
215
215
  }
216
216
  /**
217
217
  * Subscribe to the result of this document (indefinitely).
@@ -221,19 +221,19 @@ export class DatabaseDocument {
221
221
  * @return Function that ends the subscription.
222
222
  */
223
223
  subscribe(next) {
224
- return this.provider.subscribe(this, typeof next === "function" ? { next } : next);
224
+ return this.db.provider.subscribe(this, typeof next === "function" ? { next } : next);
225
225
  }
226
226
  /** Set the complete data of this document. */
227
227
  set(data) {
228
- return this.provider.set(this, data);
228
+ return this.db.provider.set(this, data);
229
229
  }
230
230
  /** Update this document. */
231
231
  update(updates) {
232
- return this.provider.update(this, updates instanceof Update ? updates : new DataUpdate(updates));
232
+ return this.db.provider.update(this, updates instanceof Update ? updates : new DataUpdate(updates));
233
233
  }
234
234
  /** Delete this document. */
235
235
  delete() {
236
- return this.provider.delete(this);
236
+ return this.db.provider.delete(this);
237
237
  }
238
238
  /** Validate data for this query reference. */
239
239
  validate(unsafeData) {
package/db/Operation.d.ts CHANGED
@@ -3,6 +3,7 @@ import { ImmutableArray, Nullish, Data, Key } from "../util/index.js";
3
3
  import type { Database, DatabaseDocument, DatabaseQuery } from "./Database.js";
4
4
  /** Represent a write operation on a database. */
5
5
  export declare abstract class Operation {
6
+ /** Run this operation and return the result operation. */
6
7
  abstract run(db: Database): Promise<Operation>;
7
8
  }
8
9
  /** Represent a list of write operations on a database run in series. */
@@ -22,7 +23,9 @@ export declare class Operations extends Operation {
22
23
  /** Represent a add operation made to a collection in a database. */
23
24
  export declare class AddOperation<T extends Data> extends Operation {
24
25
  /** Create a new add operation on a collection. */
25
- static on<X extends Data>({ collection }: DatabaseDocument | DatabaseQuery, data: X): AddOperation<X>;
26
+ static on<X extends Data>({ collection }: DatabaseDocument<X> | DatabaseQuery<X>, data: X): AddOperation<X>;
27
+ /** Run a new add operation on a collection and return the result operation. */
28
+ static run<X extends Data>({ collection, db }: DatabaseDocument<X> | DatabaseQuery<X>, data: X): Promise<SetOperation<X>>;
26
29
  readonly collection: string;
27
30
  readonly data: T;
28
31
  constructor(collection: string, data: T);
@@ -33,7 +36,9 @@ export declare class AddOperation<T extends Data> extends Operation {
33
36
  /** Represent a set operation made to a single document in a database. */
34
37
  export declare class SetOperation<T extends Data> extends Operation {
35
38
  /** Create a new add operation on a collection. */
36
- static on<X extends Data>({ collection, id }: DatabaseDocument, data: X): SetOperation<X>;
39
+ static on<X extends Data>({ collection, id }: DatabaseDocument<X>, data: X): SetOperation<X>;
40
+ /** Run a new set operation on a collection and return the result operation. */
41
+ static run<X extends Data>({ collection, id, db }: DatabaseDocument<X>, data: X): Promise<SetOperation<X>>;
37
42
  readonly collection: string;
38
43
  readonly id: string;
39
44
  readonly data: T;
@@ -45,7 +50,9 @@ export declare class SetOperation<T extends Data> extends Operation {
45
50
  /** Represent an update operation made to a single document in a database. */
46
51
  export declare class UpdateOperation<T extends Data> extends Operation {
47
52
  /** Create a new update operation on a document. */
48
- static on<X extends Data>({ collection, id }: DatabaseDocument<X>, updates?: PropUpdates<X>): UpdateOperation<X>;
53
+ static on<X extends Data>({ collection, id }: DatabaseDocument<X>, updates: PropUpdates<X>): UpdateOperation<X>;
54
+ /** Run a new set operation on a collection and return the result operation. */
55
+ static run<X extends Data>({ collection, id, db }: DatabaseDocument<X>, updates: PropUpdates<X>): Promise<UpdateOperation<X>>;
49
56
  readonly collection: string;
50
57
  readonly id: string;
51
58
  readonly updates: PropUpdates<T>;
@@ -56,8 +63,10 @@ export declare class UpdateOperation<T extends Data> extends Operation {
56
63
  }
57
64
  /** Represent a delete operation made to a single document in a database. */
58
65
  export declare class DeleteOperation extends Operation {
59
- /** Create a new update operation on a document. */
60
- static on({ collection, id }: DatabaseDocument): DeleteOperation;
66
+ /** Create a new delete operation on a document. */
67
+ static on<X extends Data>({ collection, id }: DatabaseDocument<X>): DeleteOperation;
68
+ /** Run a new delete operation on a document. */
69
+ static run<X extends Data>({ collection, id, db }: DatabaseDocument<X>): Promise<DeleteOperation>;
61
70
  readonly collection: string;
62
71
  readonly id: string;
63
72
  constructor(collection: string, id: string);
package/db/Operation.js CHANGED
@@ -32,6 +32,10 @@ export class AddOperation extends Operation {
32
32
  static on({ collection }, data) {
33
33
  return new AddOperation(collection, data);
34
34
  }
35
+ /** Run a new add operation on a collection and return the result operation. */
36
+ static run({ collection, db }, data) {
37
+ return new AddOperation(collection, data).run(db);
38
+ }
35
39
  async run(db) {
36
40
  const id = await db.query(this.collection).add(this.data);
37
41
  return new SetOperation(this.collection, id, this.data); // When an add operation is run it returns a set operation so the operation is repeatable.
@@ -55,6 +59,10 @@ export class SetOperation extends Operation {
55
59
  static on({ collection, id }, data) {
56
60
  return new SetOperation(collection, id, data);
57
61
  }
62
+ /** Run a new set operation on a collection and return the result operation. */
63
+ static run({ collection, id, db }, data) {
64
+ return new SetOperation(collection, id, data).run(db);
65
+ }
58
66
  async run(db) {
59
67
  await db.doc(this.collection, this.id).set(this.data);
60
68
  return this;
@@ -75,9 +83,13 @@ export class UpdateOperation extends Operation {
75
83
  this.updates = updates;
76
84
  }
77
85
  /** Create a new update operation on a document. */
78
- static on({ collection, id }, updates = {}) {
86
+ static on({ collection, id }, updates) {
79
87
  return new UpdateOperation(collection, id, updates);
80
88
  }
89
+ /** Run a new set operation on a collection and return the result operation. */
90
+ static run({ collection, id, db }, updates) {
91
+ return new UpdateOperation(collection, id, updates).run(db);
92
+ }
81
93
  async run(db) {
82
94
  await db.doc(this.collection, this.id).update(this.updates);
83
95
  return this;
@@ -96,10 +108,14 @@ export class DeleteOperation extends Operation {
96
108
  this.collection = collection;
97
109
  this.id = id;
98
110
  }
99
- /** Create a new update operation on a document. */
111
+ /** Create a new delete operation on a document. */
100
112
  static on({ collection, id }) {
101
113
  return new DeleteOperation(collection, id);
102
114
  }
115
+ /** Run a new delete operation on a document. */
116
+ static run({ collection, id, db }) {
117
+ return new DeleteOperation(collection, id).run(db);
118
+ }
103
119
  async run(db) {
104
120
  await db.doc(this.collection, this.id).delete();
105
121
  return this;
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.42.0",
14
+ "version": "1.45.0",
15
15
  "repository": "https://github.com/dhoulb/shelving",
16
16
  "author": "Dave Houlbrooke <dave@shax.com>",
17
17
  "license": "0BSD",
@@ -63,14 +63,14 @@
63
63
  "@types/jest": "^27.4.0",
64
64
  "@types/react": "^17.0.38",
65
65
  "@types/react-dom": "^17.0.11",
66
- "@typescript-eslint/eslint-plugin": "^5.8.1",
67
- "@typescript-eslint/parser": "^5.8.1",
66
+ "@typescript-eslint/eslint-plugin": "^5.9.0",
67
+ "@typescript-eslint/parser": "^5.9.0",
68
68
  "eslint": "^8.6.0",
69
69
  "eslint-config-prettier": "^8.3.0",
70
- "eslint-plugin-import": "^2.25.3",
70
+ "eslint-plugin-import": "^2.25.4",
71
71
  "eslint-plugin-prettier": "^4.0.0",
72
- "firebase": "^9.6.1",
73
- "jest": "^27.4.5",
72
+ "firebase": "^9.6.2",
73
+ "jest": "^27.4.7",
74
74
  "jest-ts-webcompat-resolver": "^1.0.0",
75
75
  "prettier": "^2.5.1",
76
76
  "react": "^17.0.2",
@@ -30,13 +30,13 @@ export function useAsyncDocument(ref, maxAge = 1000) {
30
30
  function getCachedResult(ref) {
31
31
  if (!ref)
32
32
  return undefined;
33
- const provider = findSourceProvider(ref.provider, CacheProvider);
33
+ const provider = findSourceProvider(ref.db.provider, CacheProvider);
34
34
  return provider.isCached(ref) ? provider.cache.get(ref) : NOVALUE;
35
35
  }
36
36
  /** Effect that subscribes a component to the cache for a reference. */
37
37
  function subscribeEffect(ref, maxAge, next, error) {
38
38
  if (ref) {
39
- const provider = findSourceProvider(ref.provider, CacheProvider);
39
+ const provider = findSourceProvider(ref.db.provider, CacheProvider);
40
40
  const stopCache = provider.cache.subscribe(ref, { next, error });
41
41
  if (maxAge === true) {
42
42
  // If `maxAge` is true subscribe to the source for as long as this component is attached.
package/react/useQuery.js CHANGED
@@ -30,18 +30,18 @@ export function useAsyncQuery(ref, maxAge = 1000) {
30
30
  function getCachedResults(ref) {
31
31
  if (!ref)
32
32
  return undefined;
33
- const provider = findSourceProvider(ref.provider, CacheProvider);
33
+ const provider = findSourceProvider(ref.db.provider, CacheProvider);
34
34
  return provider.isCached(ref) ? getMap(provider.cache.getQuery(ref)) : NOVALUE;
35
35
  }
36
36
  /** Effect that subscribes a component to the cache for a reference. */
37
37
  function subscribeEffect(ref, maxAge, next, error) {
38
38
  if (ref) {
39
- const provider = findSourceProvider(ref.provider, CacheProvider);
39
+ const provider = findSourceProvider(ref.db.provider, CacheProvider);
40
40
  const observer = new ResultsObserver({ next, error });
41
41
  const stopCache = provider.cache.subscribeQuery(ref, observer);
42
42
  if (maxAge === true) {
43
43
  // If `maxAge` is true subscribe to the source for as long as this component is attached.
44
- const stopSource = ref.provider.subscribeQuery(ref, observer);
44
+ const stopSource = ref.db.provider.subscribeQuery(ref, observer);
45
45
  return () => {
46
46
  stopCache();
47
47
  stopSource();
@@ -12,10 +12,12 @@ export declare type PropUpdates<T extends Data> = {
12
12
  };
13
13
  /** Update that can be applied to a data object to update its props. */
14
14
  export declare class DataUpdate<T extends Data> extends Update<T> implements Iterable<Prop<PropUpdates<T>>>, Transformable<T, T> {
15
+ /** Return a data update with a specific prop marked for update. */
16
+ static with<X extends Data, K extends Key<X>>(key: Nullish<K>, value: X[K] | Update<X[K]>): DataUpdate<X>;
15
17
  readonly updates: PropUpdates<T>;
16
18
  constructor(props: PropUpdates<T>);
17
19
  transform(existing: T): T;
18
- /** Return a new object with the specified additional transform for a prop. */
20
+ /** Return a data update with a specific prop marked for update. */
19
21
  with<K extends Key<T>>(key: Nullish<K>, value: T[K] | Update<T[K]>): this;
20
22
  /** Iterate over the transforms in this object. */
21
23
  [Symbol.iterator](): Iterator<Prop<PropUpdates<T>>, void>;
@@ -6,10 +6,14 @@ export class DataUpdate extends Update {
6
6
  super();
7
7
  this.updates = props;
8
8
  }
9
+ /** Return a data update with a specific prop marked for update. */
10
+ static with(key, value) {
11
+ return new DataUpdate(!isNullish(key) ? { [key]: value } : {});
12
+ }
9
13
  transform(existing) {
10
14
  return transformProps(existing, this.updates);
11
15
  }
12
- /** Return a new object with the specified additional transform for a prop. */
16
+ /** Return a data update with a specific prop marked for update. */
13
17
  with(key, value) {
14
18
  if (isNullish(key))
15
19
  return this;
package/util/date.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ /** One second in millseconds. */
2
+ export declare const SECOND = 1000;
1
3
  /** One minute in millseconds. */
2
4
  export declare const MINUTE: number;
3
5
  /** One hour in millseconds. */
@@ -6,6 +8,8 @@ export declare const HOUR: number;
6
8
  export declare const DAY: number;
7
9
  /** One week in millseconds. */
8
10
  export declare const WEEK: number;
11
+ /** One month in millseconds. */
12
+ export declare const MONTH: number;
9
13
  /** One year in millseconds. */
10
14
  export declare const YEAR: number;
11
15
  /** Is a value a date? */
@@ -53,46 +57,73 @@ export declare function getMonday(target?: PossibleDate): Date;
53
57
  export declare function addDays(change: number, target?: PossibleDate): Date;
54
58
  /** Return a new date that increase or decreases the number of hours based on an input date. */
55
59
  export declare function addHours(change: number, target?: PossibleDate): Date;
60
+ /**
61
+ * Get the duration (in milliseconds) between two dates.
62
+ *
63
+ * @param target The date when the thing will happen or did happen.
64
+ * @param current Today's date (or a different date to measure from).
65
+ */
66
+ export declare const getDuration: (target?: PossibleDate | undefined, current?: PossibleDate | undefined) => number;
56
67
  /** Count the number of seconds until a date. */
57
- export declare const secondsUntil: (target: PossibleDate, current?: PossibleDate | undefined) => number;
68
+ export declare const getSecondsUntil: (target: PossibleDate, current?: PossibleDate | undefined) => number;
58
69
  /** Count the number of days ago a date was. */
59
- export declare const secondsAgo: (target: PossibleDate, current?: PossibleDate | undefined) => number;
70
+ export declare const getSecondsAgo: (target: PossibleDate, current?: PossibleDate | undefined) => number;
60
71
  /** Count the number of days until a date. */
61
- export declare const daysUntil: (target: PossibleDate, current?: PossibleDate | undefined) => number;
72
+ export declare const getDaysUntil: (target: PossibleDate, current?: PossibleDate | undefined) => number;
62
73
  /** Count the number of days ago a date was. */
63
- export declare const daysAgo: (target: PossibleDate, current?: PossibleDate | undefined) => number;
74
+ export declare const getDaysAgo: (target: PossibleDate, current?: PossibleDate | undefined) => number;
64
75
  /** Count the number of weeks until a date. */
65
- export declare const weeksUntil: (target: PossibleDate, current?: PossibleDate | undefined) => number;
76
+ export declare const getWeeksUntil: (target: PossibleDate, current?: PossibleDate | undefined) => number;
66
77
  /** Count the number of weeks ago a date was. */
67
- export declare const weeksAgo: (target: PossibleDate, current?: PossibleDate | undefined) => number;
68
- /** Return a friendly gap between two dates, e.g. `in 10 days` or `16 hours ago` or `yesterday` */
69
- export declare function formatWhen(target: PossibleDate, current?: PossibleDate): string;
78
+ export declare const getWeeksAgo: (target: PossibleDate, current?: PossibleDate | undefined) => number;
79
+ /** Format a full description of a duration of time using the most reasonable units e.g. `5 years` or `1 week` or `4 minutes` or `12 milliseconds`. */
80
+ export declare function formatFullDuration(ms: number): string;
81
+ /** Format a description of a duration of time using the most reasonable units e.g. `5y` or `4m` or `12ms`. */
82
+ export declare function formatDuration(ms: number): string;
70
83
  /**
71
- * Return when a date happened, e.g. `10 days` or `2 hours` or `-1 week`
72
- * @param target The date when the thing will happen.
84
+ * Return full description of the gap between two dates, e.g. `in 10 days` or `2 hours ago`
85
+ *
86
+ * @param target The date when the thing will happen or did happen.
73
87
  * @param current Today's date (or a different date to measure from).
74
88
  */
75
- export declare function formatUntil(target: PossibleDate, current?: PossibleDate): string;
89
+ export declare function formatFullWhen(target: PossibleDate, current?: PossibleDate): string;
76
90
  /**
77
- * Return a compact version of when a date happened, e.g. `10d` or `2h` or `-1w`
91
+ * Return full description of when a date happened, e.g. `10 days` or `2 hours` or `-1 week`
92
+ *
78
93
  * @param target The date when the thing will happen.
79
94
  * @param current Today's date (or a different date to measure from).
80
95
  */
81
- export declare function formatShortUntil(target: PossibleDate, current?: PossibleDate): string;
96
+ export declare const formatFullUntil: (target: PossibleDate, current?: PossibleDate | undefined) => string;
82
97
  /**
83
- * Return when a date will happen, e.g. `10 days` or `2 hours` or `-1 week`
98
+ * Return full description of when a date will happen, e.g. `10 days` or `2 hours` or `-1 week`
99
+ *
84
100
  * @param target The date when the thing happened.
85
101
  * @param current Today's date (or a different date to measure from).
86
102
  */
87
- export declare function formatAgo(target: PossibleDate, current?: PossibleDate): string;
103
+ export declare const formatFullAgo: (target: PossibleDate, current?: PossibleDate | undefined) => string;
88
104
  /**
89
- * Return a compact version of when a date will happen, e.g. `10d` or `2h` or `-1w`
105
+ * Compact how long until a date happens, e.g. `in 10d` or `2h ago` or `in 1w`
106
+ *
107
+ * @param target The date when the thing will happen.
108
+ * @param current Today's date (or a different date to measure from).
109
+ */
110
+ export declare function formatWhen(target: PossibleDate, current?: PossibleDate): string;
111
+ /**
112
+ * Return short description of when a date happened, e.g. `10d` or `2h` or `-1w`
113
+ *
114
+ * @param target The date when the thing will happen.
115
+ * @param current Today's date (or a different date to measure from).
116
+ */
117
+ export declare const formatUntil: (target: PossibleDate, current?: PossibleDate | undefined) => string;
118
+ /**
119
+ * Return short description of when a date will happen, e.g. `10d` or `2h` or `-1w`
120
+ *
90
121
  * @param target The date when the thing happened.
91
122
  * @param current Today's date (or a different date to measure from).
92
123
  */
93
- export declare function formatShortAgo(target: PossibleDate, current?: PossibleDate): string;
124
+ export declare const formatAgo: (target: PossibleDate, current?: PossibleDate | undefined) => string;
94
125
  /** Format a date in the browser locale. */
95
- export declare function formatDate(date: PossibleDate, options?: Intl.DateTimeFormatOptions): string;
126
+ export declare const formatDate: (date: PossibleDate) => string;
96
127
  /** Is a date in the past? */
97
128
  export declare const isPast: (target: PossibleDate, current?: PossibleDate | undefined) => boolean;
98
129
  /** Is a date in the future? */
package/util/date.js CHANGED
@@ -1,15 +1,19 @@
1
1
  import { AssertionError } from "../error/index.js";
2
- import { formatNumber } from "./number.js";
2
+ import { formatFullQuantity, formatQuantity } from "./number.js";
3
+ /** One second in millseconds. */
4
+ export const SECOND = 1000;
3
5
  /** One minute in millseconds. */
4
- export const MINUTE = 60 * 1000;
6
+ export const MINUTE = 60 * SECOND;
5
7
  /** One hour in millseconds. */
6
- export const HOUR = MINUTE * 60;
8
+ export const HOUR = 60 * MINUTE;
7
9
  /** One day in millseconds. */
8
- export const DAY = HOUR * 24;
10
+ export const DAY = 24 * HOUR;
9
11
  /** One week in millseconds. */
10
- export const WEEK = DAY * 7;
12
+ export const WEEK = 7 * DAY;
13
+ /** One month in millseconds. */
14
+ export const MONTH = 30 * DAY;
11
15
  /** One year in millseconds. */
12
- export const YEAR = DAY * 365;
16
+ export const YEAR = 365 * DAY;
13
17
  /** Is a value a date? */
14
18
  export const isDate = (v) => v instanceof Date;
15
19
  /**
@@ -101,111 +105,114 @@ export function addHours(change, target) {
101
105
  date.setHours(date.getHours() + change);
102
106
  return date;
103
107
  }
108
+ /**
109
+ * Get the duration (in milliseconds) between two dates.
110
+ *
111
+ * @param target The date when the thing will happen or did happen.
112
+ * @param current Today's date (or a different date to measure from).
113
+ */
114
+ export const getDuration = (target, current) => getDate(target).getTime() - getDate(current).getTime();
104
115
  /** Count the number of seconds until a date. */
105
- export const secondsUntil = (target, current) => Math.round(getDate(target).getTime() - getDate(current).getTime()) / 1000;
116
+ export const getSecondsUntil = (target, current) => getDuration(target, current) / 1000;
106
117
  /** Count the number of days ago a date was. */
107
- export const secondsAgo = (target, current) => 0 - secondsUntil(target, current);
118
+ export const getSecondsAgo = (target, current) => 0 - getSecondsUntil(target, current);
108
119
  /** Count the number of days until a date. */
109
- export const daysUntil = (target, current) => Math.round((getMidnight(target).getTime() - getMidnight(current).getTime()) / 86400000);
120
+ export const getDaysUntil = (target, current) => Math.round((getMidnight(target).getTime() - getMidnight(current).getTime()) / 86400000);
110
121
  /** Count the number of days ago a date was. */
111
- export const daysAgo = (target, current) => 0 - daysUntil(target, current);
122
+ export const getDaysAgo = (target, current) => 0 - getDaysUntil(target, current);
112
123
  /** Count the number of weeks until a date. */
113
- export const weeksUntil = (target, current) => Math.floor(daysUntil(target, current) / 7);
124
+ export const getWeeksUntil = (target, current) => Math.floor(getDaysUntil(target, current) / 7);
114
125
  /** Count the number of weeks ago a date was. */
115
- export const weeksAgo = (target, current) => 0 - weeksUntil(target, current);
116
- /**
117
- * Get information about the difference between two dates.
118
- * - Used by `formatWhen()` and `formatAgo()` etc
119
- * @returns Tuple in `[amount, units]` format, e.g. `[3, "days"]` or `[-16, "hours"]` or `[1, "week"]`
120
- */
121
- function diffDates(target, current) {
122
- const seconds = (getDate(target).getTime() - getDate(current).getTime()) / 1000;
123
- const abs = Math.abs(seconds);
124
- // Up to 99 seconds, e.g. '22 seconds ago'
125
- if (abs < 99) {
126
- const num = Math.round(seconds);
127
- return [num, num === 1 ? "second" : "seconds"];
128
- }
129
- // Up to one hour — show minutes, e.g. '18 minutes ago'
130
- if (abs < 3600) {
131
- const num = Math.round(seconds / 60);
132
- return [num, num === 1 ? "minute" : "minutes"];
133
- }
134
- // Up to 24 hours — show hours, e.g. '23 hours ago'
135
- if (abs < 86400) {
136
- const num = Math.round(seconds / 3600);
137
- return [num, num === 1 ? "hour" : "hours"];
138
- }
139
- // Up to 2 weeks — show days, e.g. '13 days ago'
140
- if (abs < 1209600) {
141
- const num = Math.round(seconds / 86400);
142
- return [num, num === 1 ? "day" : "days"];
143
- }
144
- // Up to 2 months — show weeks, e.g. '6 weeks ago'
145
- if (abs < 5184000) {
146
- const num = Math.round(seconds / 604800);
147
- return [num, num === 1 ? "week" : "weeks"];
148
- }
149
- // Up to 18 months — show months, e.g. '6 months ago'
150
- if (abs < 46656000) {
151
- const num = Math.round(seconds / 2592000);
152
- return [num, num === 1 ? "month" : "months"];
153
- }
154
- // Above 18 months — show years, e.g. '2 years ago'
155
- return [Math.round(seconds / 31536000), "year"];
126
+ export const getWeeksAgo = (target, current) => 0 - getWeeksUntil(target, current);
127
+ /** Format a full description of a duration of time using the most reasonable units e.g. `5 years` or `1 week` or `4 minutes` or `12 milliseconds`. */
128
+ export function formatFullDuration(ms) {
129
+ const abs = Math.abs(ms);
130
+ if (abs <= 99 * SECOND)
131
+ return formatFullQuantity(ms, "second", "seconds", 0); // Up to 99 seconds, e.g. '22 seconds ago'
132
+ if (abs <= HOUR)
133
+ return formatFullQuantity(ms / MINUTE, "minute", "minutes", 0); // Up to one hour — show minutes, e.g. '18 minutes ago'
134
+ if (abs <= DAY)
135
+ return formatFullQuantity(ms / HOUR, "hour", "hours", 0); // Up to one day — show hours, e.g. '23 hours ago'
136
+ if (abs <= 2 * WEEK)
137
+ return formatFullQuantity(ms / DAY, "day", "days", 0); // Up to 2 weeks — show days, e.g. '13 days ago'
138
+ if (abs <= 10 * WEEK)
139
+ return formatFullQuantity(ms / WEEK, "week", "weeks", 0); // Up to 2 months — show weeks, e.g. '6 weeks ago'
140
+ if (abs <= 18 * MONTH)
141
+ return formatFullQuantity(ms / MONTH, "month", "months", 0); // Up to 18 months — show months, e.g. '6 months ago'
142
+ return formatFullQuantity(ms / YEAR, "year", "years", 0); // Above 18 months — show years, e.g. '2 years ago'
156
143
  }
157
- /** Return a friendly gap between two dates, e.g. `in 10 days` or `16 hours ago` or `yesterday` */
158
- export function formatWhen(target, current) {
159
- const [amount, unit] = diffDates(target, current);
160
- // Special case for rough equality.
161
- if (unit === "second" && amount > -30 && amount < 30)
162
- return "just now";
163
- // Return either `in 22 days` or `1 hour ago`
164
- const future = amount >= 0;
165
- const abs = Math.abs(amount);
166
- const str = formatNumber(abs);
167
- return future ? `in ${str} ${unit}` : `${str} ${unit} ago`;
144
+ /** Format a description of a duration of time using the most reasonable units e.g. `5y` or `4m` or `12ms`. */
145
+ export function formatDuration(ms) {
146
+ const abs = Math.abs(ms);
147
+ if (abs <= 99 * SECOND)
148
+ return formatQuantity(ms, "s", 0); // Up to 99 seconds, e.g. '22 seconds ago'
149
+ if (abs <= HOUR)
150
+ return formatQuantity(ms / MINUTE, "m", 0); // Up to one hour show minutes, e.g. '18 minutes ago'
151
+ if (abs <= DAY)
152
+ return formatQuantity(ms / HOUR, "h", 0); // Up to one day — show hours, e.g. '23 hours ago'
153
+ if (abs <= 2 * WEEK)
154
+ return formatQuantity(ms / DAY, "d", 0); // Up to 2 weeks — show days, e.g. '13 days ago'
155
+ if (abs <= 10 * WEEK)
156
+ return formatQuantity(ms / WEEK, "w", 0); // Up to 2 months — show weeks, e.g. '6 weeks ago'
157
+ if (abs <= 18 * MONTH)
158
+ return formatQuantity(ms / MONTH, "m", 0); // Up to 18 months — show months, e.g. '6 months ago'
159
+ return formatQuantity(ms / YEAR, "y", 0); // Above 18 months — show years, e.g. '2 years ago'
168
160
  }
169
161
  /**
170
- * Return when a date happened, e.g. `10 days` or `2 hours` or `-1 week`
171
- * @param target The date when the thing will happen.
162
+ * Return full description of the gap between two dates, e.g. `in 10 days` or `2 hours ago`
163
+ *
164
+ * @param target The date when the thing will happen or did happen.
172
165
  * @param current Today's date (or a different date to measure from).
173
166
  */
174
- export function formatUntil(target, current) {
175
- const [amount, units] = diffDates(target, current);
176
- return `${formatNumber(amount)} ${units}`;
167
+ export function formatFullWhen(target, current) {
168
+ const ms = getDuration(target, current);
169
+ const abs = Math.abs(ms);
170
+ const duration = formatFullDuration(abs);
171
+ return abs < 10 * SECOND ? "just now" : ms > 0 ? `in ${duration}` : `${duration} ago`;
177
172
  }
178
173
  /**
179
- * Return a compact version of when a date happened, e.g. `10d` or `2h` or `-1w`
174
+ * Return full description of when a date happened, e.g. `10 days` or `2 hours` or `-1 week`
175
+ *
180
176
  * @param target The date when the thing will happen.
181
177
  * @param current Today's date (or a different date to measure from).
182
178
  */
183
- export function formatShortUntil(target, current) {
184
- const [amount, units] = diffDates(target, current);
185
- return `${formatNumber(amount)}${units.substr(0, 1)}`;
186
- }
179
+ export const formatFullUntil = (target, current) => formatFullDuration(getDuration(target, current));
187
180
  /**
188
- * Return when a date will happen, e.g. `10 days` or `2 hours` or `-1 week`
181
+ * Return full description of when a date will happen, e.g. `10 days` or `2 hours` or `-1 week`
182
+ *
189
183
  * @param target The date when the thing happened.
190
184
  * @param current Today's date (or a different date to measure from).
191
185
  */
192
- export function formatAgo(target, current) {
193
- const [amount, units] = diffDates(current, target);
194
- return `${formatNumber(amount)} ${units}`;
186
+ export const formatFullAgo = (target, current) => formatFullDuration(getDuration(current, target));
187
+ /**
188
+ * Compact how long until a date happens, e.g. `in 10d` or `2h ago` or `in 1w`
189
+ *
190
+ * @param target The date when the thing will happen.
191
+ * @param current Today's date (or a different date to measure from).
192
+ */
193
+ export function formatWhen(target, current) {
194
+ const ms = getDuration(target, current);
195
+ const abs = Math.abs(ms);
196
+ const duration = formatDuration(abs);
197
+ return abs < 10 * SECOND ? "just now" : ms > 0 ? `in ${duration}` : `${duration} ago`;
195
198
  }
196
199
  /**
197
- * Return a compact version of when a date will happen, e.g. `10d` or `2h` or `-1w`
200
+ * Return short description of when a date happened, e.g. `10d` or `2h` or `-1w`
201
+ *
202
+ * @param target The date when the thing will happen.
203
+ * @param current Today's date (or a different date to measure from).
204
+ */
205
+ export const formatUntil = (target, current) => formatDuration(getDuration(target, current));
206
+ /**
207
+ * Return short description of when a date will happen, e.g. `10d` or `2h` or `-1w`
208
+ *
198
209
  * @param target The date when the thing happened.
199
210
  * @param current Today's date (or a different date to measure from).
200
211
  */
201
- export function formatShortAgo(target, current) {
202
- const [amount, units] = diffDates(current, target);
203
- return `${formatNumber(amount)}${units.substr(0, 1)}`;
204
- }
212
+ export const formatAgo = (target, current) => formatDuration(getDuration(current, target));
205
213
  /** Format a date in the browser locale. */
206
- export function formatDate(date, options) {
207
- return new Intl.DateTimeFormat(undefined, options).format(getDate(date));
208
- }
214
+ export const formatDate = (date) => _formatter.format(getDate(date));
215
+ const _formatter = new Intl.DateTimeFormat(undefined, {});
209
216
  /** Is a date in the past? */
210
217
  export const isPast = (target, current) => getDate(target) < getDate(current);
211
218
  /** Is a date in the future? */
package/util/number.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ export declare const TRILLION = 1000000000000;
2
+ export declare const BILLION = 1000000000;
3
+ export declare const MILLION = 1000000;
1
4
  /** Is a value a number? */
2
5
  export declare const isNumber: (v: unknown) => v is number;
3
6
  /**
@@ -21,25 +24,69 @@ export declare function getNumber(value: unknown): number;
21
24
  *
22
25
  * @param num The number to round.
23
26
  * @param step The rounding to round to, e.g. `2` or `0.1` (defaults to `1`, i.e. round numbers).
24
- * @returns The number rounded to the step.
27
+ *
28
+ * @returns The number rounded to the specified step.
25
29
  */
26
- export declare function roundStep(num: number, step?: number): number;
30
+ export declare const roundStep: (num: number, step?: number) => number;
27
31
  /**
28
32
  * Round a number to a specified set of decimal places.
29
- * - Doesn't include excess `0` zeroes like `num.toFixed()` and `num.toPrecision()` do.
33
+ * - Better than `Math.round()` because it allows a `precision` argument.
34
+ * - Better than `num.toFixed()` because it trims excess `0` zeroes.
30
35
  *
31
- * @param num The number to format.
32
- * @param precision Maximum of digits shown after the decimal point (defaults to 10), with zeroes trimmed.
33
- * @returns The number formatted as a string in the browser's current locale.
36
+ * @param num The number to round.
37
+ * @param precision Maximum number of digits shown after the decimal point (defaults to 10).
38
+ *
39
+ * @returns The number rounded to the specified precision.
40
+ */
41
+ export declare const roundNumber: (num: number, precision?: number) => number;
42
+ /**
43
+ * Truncate a number to a specified set of decimal places.
44
+ * - Better than `Math.trunc()` because it allows a `precision` argument.
45
+ *
46
+ * @param num The number to truncate.
47
+ * @param precision Maximum number of digits shown after the decimal point (defaults to 10).
48
+ *
49
+ * @returns The number truncated to the specified precision.
34
50
  */
35
- export declare const roundNumber: (num: number, precision?: number) => string;
51
+ export declare const truncateNumber: (num: number, precision?: number) => number;
36
52
  /**
37
53
  * Format a number (based on the user's browser settings).
54
+ *
38
55
  * @param num The number to format.
39
- * @param precision Maximum of digits shown after the decimal point (defaults to 10), with zeroes trimmed.
56
+ * @param maxPrecision Maximum number of digits shown after the decimal point (defaults to 2).
57
+ * @param minPrecision Minimum number of digits shown after the decimal point (defaults to 0).
58
+ *
40
59
  * @returns The number formatted as a string in the browser's current locale.
41
60
  */
42
- export declare const formatNumber: (num: number, precision?: number) => string;
61
+ export declare const formatNumber: (num: number, maxPrecision?: number, minPrecision?: number) => string;
62
+ /** Format a number with a short suffix (number and suffix are separated by a non-breaking narrow space). */
63
+ export declare const formatQuantity: (num: number, suffix: string, maxPrecision?: number | undefined, minPrecision?: number | undefined) => string;
64
+ /** Format a number with a longer full-word suffix (number and suffix are separated by a non-breaking space). */
65
+ export declare function formatFullQuantity(num: number, singular: string, plural: string, maxPrecision?: number, minPrecision?: number): string;
66
+ /**
67
+ * Cram a large whole numbers into a space efficient format, e.g. `14.7M`
68
+ * - Improves glanceability.
69
+ * - Keeps number of characters under five if possible.
70
+ *
71
+ * - Numbers over 100 trillion: `157T`
72
+ * - Numbers over 10 trillion: `15.7T` (includes zero e.g. `40.0T` for consistency).
73
+ * - Numbers over 1 trillion: `1.57T` (includes zeros e.g. `4.00T` for consistency).
74
+ * - Numbers over 100 billion: `157B`
75
+ * - Numbers over 10 billion: `15.7B` (includes zero e.g. `40.0B` for consistency).
76
+ * - Numbers over 1 billion: `1.57B` (includes zeros e.g. `4.00B` for consistency).
77
+ * - Numbers over 100 million: `157M`
78
+ * - Numbers over 10 million: `15.7M` (includes zero e.g. `40.0M` for consistency).
79
+ * - Numbers over 1 million: `1.57M` (includes zeros e.g. `4.00M` for consistency).
80
+ * - Numbers over 100,000: `157K`
81
+ * - Numbers over 10,000: `15.7K` (includes zero e.g. `14.0K` for consistency).
82
+ * - Smaller numbers: `1570` and `157` and `15.7` and `1.6`
83
+ *
84
+ * @param num The number to format.
85
+ * @param precision Maximum number of digits shown after the decimal point (defaults to 10, only used for numbers under 10,000).
86
+ *
87
+ * @returns The number formatted as a crammed string.
88
+ */
89
+ export declare function cramNumber(num: number): string;
43
90
  /**
44
91
  * Is a number within a specified range?
45
92
  *
package/util/number.js CHANGED
@@ -1,4 +1,9 @@
1
1
  import { AssertionError } from "../error/index.js";
2
+ import { NBSP, NNBSP } from "./string.js";
3
+ // Constants.
4
+ export const TRILLION = 1000000000000;
5
+ export const BILLION = 1000000000;
6
+ export const MILLION = 1000000;
2
7
  /** Is a value a number? */
3
8
  export const isNumber = (v) => typeof v === "number";
4
9
  /**
@@ -36,29 +41,87 @@ export function getNumber(value) {
36
41
  *
37
42
  * @param num The number to round.
38
43
  * @param step The rounding to round to, e.g. `2` or `0.1` (defaults to `1`, i.e. round numbers).
39
- * @returns The number rounded to the step.
44
+ *
45
+ * @returns The number rounded to the specified step.
40
46
  */
41
- export function roundStep(num, step = 1) {
42
- if (step < 0.00001)
43
- throw new AssertionError("roundToStep() does not work accurately with steps smaller than 0.00001", step);
44
- return Math.round(num / step) * step;
45
- }
47
+ export const roundStep = (num, step = 1) => Math.round(num / step) * step;
46
48
  /**
47
49
  * Round a number to a specified set of decimal places.
48
- * - Doesn't include excess `0` zeroes like `num.toFixed()` and `num.toPrecision()` do.
50
+ * - Better than `Math.round()` because it allows a `precision` argument.
51
+ * - Better than `num.toFixed()` because it trims excess `0` zeroes.
49
52
  *
50
- * @param num The number to format.
51
- * @param precision Maximum of digits shown after the decimal point (defaults to 10), with zeroes trimmed.
52
- * @returns The number formatted as a string in the browser's current locale.
53
+ * @param num The number to round.
54
+ * @param precision Maximum number of digits shown after the decimal point (defaults to 10).
55
+ *
56
+ * @returns The number rounded to the specified precision.
53
57
  */
54
- export const roundNumber = (num, precision = 10) => new Intl.NumberFormat("en-US", { maximumFractionDigits: precision }).format(num);
58
+ export const roundNumber = (num, precision = 0) => Math.round(num * 10 ** precision) / 10 ** precision;
59
+ /**
60
+ * Truncate a number to a specified set of decimal places.
61
+ * - Better than `Math.trunc()` because it allows a `precision` argument.
62
+ *
63
+ * @param num The number to truncate.
64
+ * @param precision Maximum number of digits shown after the decimal point (defaults to 10).
65
+ *
66
+ * @returns The number truncated to the specified precision.
67
+ */
68
+ export const truncateNumber = (num, precision = 0) => Math.trunc(num * 10 ** precision) / 10 ** precision;
55
69
  /**
56
70
  * Format a number (based on the user's browser settings).
71
+ *
57
72
  * @param num The number to format.
58
- * @param precision Maximum of digits shown after the decimal point (defaults to 10), with zeroes trimmed.
73
+ * @param maxPrecision Maximum number of digits shown after the decimal point (defaults to 2).
74
+ * @param minPrecision Minimum number of digits shown after the decimal point (defaults to 0).
75
+ *
59
76
  * @returns The number formatted as a string in the browser's current locale.
60
77
  */
61
- export const formatNumber = (num, precision = 10) => new Intl.NumberFormat(undefined, { maximumFractionDigits: precision }).format(num);
78
+ export const formatNumber = (num, maxPrecision = 4, minPrecision = 0) => new Intl.NumberFormat(undefined, { maximumFractionDigits: maxPrecision, minimumFractionDigits: minPrecision }).format(num);
79
+ /** Format a number with a short suffix (number and suffix are separated by a non-breaking narrow space). */
80
+ export const formatQuantity = (num, suffix, maxPrecision, minPrecision) => `${formatNumber(num, maxPrecision, minPrecision)}${NNBSP}${suffix}`;
81
+ /** Format a number with a longer full-word suffix (number and suffix are separated by a non-breaking space). */
82
+ export function formatFullQuantity(num, singular, plural, maxPrecision, minPrecision) {
83
+ const qty = formatNumber(num, maxPrecision, minPrecision);
84
+ return `${qty}${NBSP}${qty === "1" ? singular : plural}`;
85
+ }
86
+ /**
87
+ * Cram a large whole numbers into a space efficient format, e.g. `14.7M`
88
+ * - Improves glanceability.
89
+ * - Keeps number of characters under five if possible.
90
+ *
91
+ * - Numbers over 100 trillion: `157T`
92
+ * - Numbers over 10 trillion: `15.7T` (includes zero e.g. `40.0T` for consistency).
93
+ * - Numbers over 1 trillion: `1.57T` (includes zeros e.g. `4.00T` for consistency).
94
+ * - Numbers over 100 billion: `157B`
95
+ * - Numbers over 10 billion: `15.7B` (includes zero e.g. `40.0B` for consistency).
96
+ * - Numbers over 1 billion: `1.57B` (includes zeros e.g. `4.00B` for consistency).
97
+ * - Numbers over 100 million: `157M`
98
+ * - Numbers over 10 million: `15.7M` (includes zero e.g. `40.0M` for consistency).
99
+ * - Numbers over 1 million: `1.57M` (includes zeros e.g. `4.00M` for consistency).
100
+ * - Numbers over 100,000: `157K`
101
+ * - Numbers over 10,000: `15.7K` (includes zero e.g. `14.0K` for consistency).
102
+ * - Smaller numbers: `1570` and `157` and `15.7` and `1.6`
103
+ *
104
+ * @param num The number to format.
105
+ * @param precision Maximum number of digits shown after the decimal point (defaults to 10, only used for numbers under 10,000).
106
+ *
107
+ * @returns The number formatted as a crammed string.
108
+ */
109
+ export function cramNumber(num) {
110
+ const abs = Math.abs(num);
111
+ if (abs >= TRILLION)
112
+ return `${_significance(num / TRILLION)}T`;
113
+ if (abs >= BILLION)
114
+ return `${_significance(num / BILLION)}B`;
115
+ if (abs >= MILLION)
116
+ return `${_significance(num / MILLION)}M`;
117
+ if (abs >= 10000)
118
+ return `${_significance(num / 1000)}K`;
119
+ return truncateNumber(num, 2).toString();
120
+ }
121
+ function _significance(num) {
122
+ const digits = num >= 100 ? 0 : num >= 10 ? 1 : 2;
123
+ return truncateNumber(num, digits).toFixed(digits);
124
+ }
62
125
  /**
63
126
  * Is a number within a specified range?
64
127
  *
package/util/string.d.ts CHANGED
@@ -1,4 +1,10 @@
1
1
  import { ImmutableArray } from "./array.js";
2
+ /** Non-breaking space. */
3
+ export declare const NBSP = "\u00A0";
4
+ /** Thin space. */
5
+ export declare const THINSP = "\u2009";
6
+ /** Non-breaking narrow space. */
7
+ export declare const NNBSP = "\u202F";
2
8
  /** Is a value a string? */
3
9
  export declare const isString: (v: unknown) => v is string;
4
10
  /**
package/util/string.js CHANGED
@@ -3,6 +3,12 @@ import { formatDate } from "./date.js";
3
3
  import { isData } from "./data.js";
4
4
  import { isArray } from "./array.js";
5
5
  import { formatNumber, isBetween } from "./number.js";
6
+ /** Non-breaking space. */
7
+ export const NBSP = "\xA0";
8
+ /** Thin space. */
9
+ export const THINSP = "\u2009";
10
+ /** Non-breaking narrow space. */
11
+ export const NNBSP = "\u202F";
6
12
  /** Is a value a string? */
7
13
  export const isString = (v) => typeof v === "string";
8
14
  /**
package/util/units.d.ts CHANGED
@@ -1,11 +1,39 @@
1
+ /** Valid information about a unit of measure. */
2
+ export declare type UnitData = {
3
+ /** Plural name for a unit, e.g. `feet` */
4
+ readonly plural?: string;
5
+ /** Type of a unit. */
6
+ readonly type: UnitType;
7
+ /** Short suffix for this unit, e.g. `km` */
8
+ readonly suffix: string;
9
+ /** All units must specify their 'base' unit, e.g. `meter` for for distance units and `liter` for volume units. */
10
+ readonly base: number;
11
+ } & {
12
+ [K in UnitReference]?: number;
13
+ };
14
+ /** Valid system of measurement reference. */
15
+ export declare type UnitType = "percentage" | "angle" | "temperature" | "length" | "speed" | "pace" | "mass" | "time" | "volume";
16
+ /** Valid unit of measurement reference (correspond to units allowed in `Intl.NumberFormat`, but not all). */
17
+ export declare type UnitReference = "percent" | "degree" | "millimeter" | "centimeter" | "meter" | "kilometer" | "mile" | "yard" | "foot" | "inch" | "liter" | "milliliter" | "gallon" | "fluid-ounce" | "milligram" | "gram" | "kilogram" | "pound" | "stone" | "ounce" | "millisecond" | "second" | "minute" | "day" | "hour" | "week" | "month" | "year";
18
+ /** List of units. */
19
+ export declare const UNITS: {
20
+ [K in UnitReference]: UnitData;
21
+ };
22
+ /** Convert between two units of the same type. */
23
+ export declare function convertUnits(num: number, from: UnitReference, to: UnitReference): number;
1
24
  /**
2
- * Valid distance unit names.
3
- * - Correspond to units allowed in `Intl.NumberFormat`
25
+ * Format a number with a given unit of measure, e.g. `12 kg` or `29.5 l`
26
+ *
27
+ * @param num The number to format.
28
+ * @param unit String reference for a unit of measure e.g. `kilometer`
29
+ * @param maxPrecision Number of decimal places to round the number to e.g. `2`
4
30
  */
5
- export declare type Unit = "millimeter" | "centimeter" | "meter" | "kilometer" | "mile" | "yard" | "foot" | "inch";
6
- /** Convert between two distance units. */
7
- export declare const convertUnits: (num: number, from: Unit, to: Unit) => number;
8
- /** Detect which type of distance unit has been typed, e.g. mile or kilometer. */
9
- export declare const detectUnit: (str: string, defaultUnit: Unit) => Unit | false;
10
- /** Format a distance. */
11
- export declare const formatUnit: (num: number, unit?: Unit, precision?: number) => string;
31
+ export declare const formatUnits: (num: number, unit: UnitReference, maxPrecision?: number | undefined, minPrecision?: number | undefined) => string;
32
+ /**
33
+ * Format a number with a given unit of measure, e.g. `12 kilograms` or `29.5 liters` or `1 degree`
34
+ *
35
+ * @param num The number to format.
36
+ * @param unit String reference for a unit of measure e.g. `kilometer`
37
+ * @param maxPrecision Number of decimal places to round the number to e.g. `2`
38
+ */
39
+ export declare const formatFullUnits: (num: number, unit: UnitReference, maxPrecision?: number | undefined, minPrecision?: number | undefined) => string;
package/util/units.js CHANGED
@@ -1,92 +1,64 @@
1
- import { formatNumber } from "./number.js";
2
- /** Conversion table for distance unit. */
3
- const unitsData = {
4
- millimeter: {
5
- meter: 0.001,
6
- regex: /(\d|\b)(millimeters?|millimetres?|mms?)\b/i,
7
- short: "mm",
8
- },
9
- centimeter: {
10
- meter: 0.01,
11
- regex: /(\d|\b)(centimeters?|centimetres?|cms?)\b/i,
12
- short: "cm",
13
- },
14
- meter: {
15
- meter: 1,
16
- centimeter: 100,
17
- millimeter: 1000,
18
- regex: /(\d|\b)(metres?|meters?|m)\b/i,
19
- short: "m",
20
- },
21
- kilometer: {
22
- meter: 1000,
23
- centimeter: 100000,
24
- millimeter: 1000000,
25
- regex: /(\d|\b)(kilometres?|kilometers?|kms?|k)\b/i,
26
- short: "km",
27
- precision: 2,
28
- m: "mile", // e.g. `10m` means miles.
29
- },
30
- mile: {
31
- meter: 1609.344,
32
- yard: 1760,
33
- foot: 5280,
34
- inch: 63360,
35
- regex: /(\d|\b)(miles?|mi|m)\b/i,
36
- short: "mi",
37
- precision: 2,
38
- m: "mile", // e.g. `10m` means miles.
39
- },
40
- yard: {
41
- meter: 0.9144,
42
- inch: 36,
43
- foot: 3,
44
- regex: /(\d|\b)(yards?|yds?|y)\b/i,
45
- short: "yd",
46
- },
47
- foot: {
48
- meter: 0.3048,
49
- inch: 12,
50
- regex: /(\d|\b)(feets?|foots?|fts?|f)\b/i,
51
- short: "ft",
52
- },
53
- inch: {
54
- meter: 0.0254,
55
- regex: /(\d|\b)(inches|inch|in)\b/i,
56
- short: "in",
57
- },
1
+ import { AssertionError } from "../error/index.js";
2
+ import { DAY, HOUR, MINUTE, MONTH, SECOND, WEEK, YEAR } from "./date.js";
3
+ import { formatFullQuantity, formatQuantity } from "./number.js";
4
+ import { NNBSP } from "./string.js";
5
+ /** List of units. */
6
+ export const UNITS = {
7
+ "percent": { type: "percentage", base: 1, suffix: "%" },
8
+ "degree": { type: "angle", base: 1, suffix: "deg" },
9
+ "millimeter": { type: "length", base: 0.001, suffix: "mm" },
10
+ "centimeter": { type: "length", base: 0.01, suffix: "cm" },
11
+ "meter": { type: "length", base: 1, centimeter: 100, millimeter: 1000, suffix: "m" },
12
+ "kilometer": { type: "length", base: 1000, centimeter: 100000, millimeter: 1000000, suffix: "km" },
13
+ "inch": { type: "length", base: 0.0254, suffix: "in" },
14
+ "foot": { type: "length", base: 0.3048, inch: 12, suffix: "ft", plural: "feet" },
15
+ "yard": { type: "length", base: 0.9144, inch: 36, foot: 3, suffix: "yd" },
16
+ "mile": { type: "length", base: 1609.344, yard: 1760, foot: 5280, inch: 63360, suffix: "mi" },
17
+ "milliliter": { type: "volume", base: 1, suffix: "ml" },
18
+ "liter": { type: "volume", base: 1000, suffix: "l" },
19
+ "fluid-ounce": { type: "volume", base: 29.5735295625, gallon: 128, suffix: `fl${NNBSP}oz` },
20
+ "gallon": { type: "volume", base: 3785.411784, suffix: "gal" },
21
+ "milligram": { type: "mass", base: 0.001, suffix: "mg" },
22
+ "gram": { type: "mass", base: 1, suffix: "g" },
23
+ "kilogram": { type: "mass", base: 1000, suffix: "kg" },
24
+ "ounce": { type: "mass", base: 28.349523125, pound: 0.0625, suffix: "oz" },
25
+ "pound": { type: "mass", base: 453.59237, ounce: 16, suffix: "lb" },
26
+ "stone": { type: "mass", base: 6350.29318, pound: 14, ounce: 224, suffix: "st", plural: "stone" },
27
+ "millisecond": { type: "time", base: 1, suffix: "ms" },
28
+ "second": { type: "time", base: SECOND, suffix: "s" },
29
+ "minute": { type: "time", base: MINUTE, suffix: "m" },
30
+ "hour": { type: "time", base: HOUR, suffix: "h" },
31
+ "day": { type: "time", base: DAY, suffix: "d" },
32
+ "week": { type: "time", base: WEEK, suffix: "w" },
33
+ "month": { type: "time", base: MONTH, suffix: "m" },
34
+ "year": { type: "time", base: YEAR, suffix: "y" },
58
35
  };
59
- /** Convert between two distance units. */
60
- export const convertUnits = (num, from, to) => {
36
+ /** Convert between two units of the same type. */
37
+ export function convertUnits(num, from, to) {
61
38
  if (from === to)
62
39
  return num;
63
- const d = unitsData[from];
64
- const t = d[to];
65
- if (t)
66
- return num * t;
67
- return (num * d.meter) / unitsData[to].meter;
68
- };
69
- /** Detect which type of distance unit has been typed, e.g. mile or kilometer. */
70
- export const detectUnit = (str, defaultUnit) => {
71
- if (str.match(/^[0-9.,\s]+$/))
72
- return defaultUnit; // Purely numeric number uses default unit.
73
- if (str.match(unitsData.kilometer.regex))
74
- return "kilometer";
75
- if (str.match(unitsData.centimeter.regex))
76
- return "centimeter";
77
- if (str.match(/(\d|\b)m\b/i))
78
- return unitsData[defaultUnit].m || "meter"; // e.g. `10m` could mean meter or mile.
79
- if (str.match(unitsData.meter.regex))
80
- return "meter";
81
- if (str.match(unitsData.mile.regex))
82
- return "mile";
83
- if (str.match(unitsData.yard.regex))
84
- return "yard";
85
- if (str.match(unitsData.foot.regex))
86
- return "foot";
87
- if (str.match(unitsData.inch.regex))
88
- return "inch";
89
- return false; // Cannot figure out unit.
90
- };
91
- /** Format a distance. */
92
- export const formatUnit = (num, unit = "meter", precision = unitsData[unit].precision || 0) => `${formatNumber(num, precision)} ${unitsData[unit].short}`;
40
+ const fromData = UNITS[from];
41
+ const exact = fromData[to]; // Get the exact conversion if possible (e.g. 5280 feet in a mile).
42
+ if (typeof exact === "number")
43
+ return num * exact;
44
+ const toData = UNITS[to];
45
+ if (fromData.type !== toData.type)
46
+ throw new AssertionError(`Target unit must be ${fromData.type}`, toData.type);
47
+ return (num * fromData.base) / toData.base;
48
+ }
49
+ /**
50
+ * Format a number with a given unit of measure, e.g. `12 kg` or `29.5 l`
51
+ *
52
+ * @param num The number to format.
53
+ * @param unit String reference for a unit of measure e.g. `kilometer`
54
+ * @param maxPrecision Number of decimal places to round the number to e.g. `2`
55
+ */
56
+ export const formatUnits = (num, unit, maxPrecision, minPrecision) => formatQuantity(num, UNITS[unit].suffix, maxPrecision, minPrecision);
57
+ /**
58
+ * Format a number with a given unit of measure, e.g. `12 kilograms` or `29.5 liters` or `1 degree`
59
+ *
60
+ * @param num The number to format.
61
+ * @param unit String reference for a unit of measure e.g. `kilometer`
62
+ * @param maxPrecision Number of decimal places to round the number to e.g. `2`
63
+ */
64
+ export const formatFullUnits = (num, unit, maxPrecision, minPrecision) => formatFullQuantity(num, unit, UNITS[unit].plural || `${unit}s`, maxPrecision, minPrecision);