-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
Copy patharrays.ts
364 lines (330 loc) · 12.9 KB
/
arrays.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { percentageOf, percentageWithin } from "./numbers";
/**
* Quickly resample an array to have less/more data points. If an input which is larger
* than the desired size is provided, it will be downsampled. Similarly, if the input
* is smaller than the desired size then it will be upsampled.
* @param {number[]} input The input array to resample.
* @param {number} points The number of samples to end up with.
* @returns {number[]} The resampled array.
*/
export function arrayFastResample(input: number[], points: number): number[] {
if (input.length === points) return input; // short-circuit a complicated call
// Heavily inspired by matrix-media-repo (used with permission)
// https://github.com/turt2live/matrix-media-repo/blob/abe72c87d2e29/util/util_audio/fastsample.go#L10
const samples: number[] = [];
if (input.length > points) {
// Danger: this loop can cause out of memory conditions if the input is too small.
const everyNth = Math.round(input.length / points);
for (let i = 0; i < input.length; i += everyNth) {
samples.push(input[i]);
}
} else {
// Smaller inputs mean we have to spread the values over the desired length. We
// end up overshooting the target length in doing this, but we're not looking to
// be super accurate so we'll let the sanity trims do their job.
const spreadFactor = Math.ceil(points / input.length);
for (const val of input) {
samples.push(...arraySeed(val, spreadFactor));
}
}
// Trim to size & return
return arrayTrimFill(samples, points, arraySeed(input[input.length - 1], points));
}
/**
* Attempts a smooth resample of the given array. This is functionally similar to arrayFastResample
* though can take longer due to the smoothing of data.
* @param {number[]} input The input array to resample.
* @param {number} points The number of samples to end up with.
* @returns {number[]} The resampled array.
*/
export function arraySmoothingResample(input: number[], points: number): number[] {
if (input.length === points) return input; // short-circuit a complicated call
let samples: number[] = [];
if (input.length > points) {
// We're downsampling. To preserve the curve we'll actually reduce our sample
// selection and average some points between them.
// All we're doing here is repeatedly averaging the waveform down to near our
// target value. We don't average down to exactly our target as the loop might
// never end, and we can over-average the data. Instead, we'll get as far as
// we can and do a followup fast resample (the neighbouring points will be close
// to the actual waveform, so we can get away with this safely).
while (samples.length > points * 2 || samples.length === 0) {
samples = [];
for (let i = 1; i < input.length - 1; i += 2) {
const prevPoint = input[i - 1];
const nextPoint = input[i + 1];
const currPoint = input[i];
const average = (prevPoint + nextPoint + currPoint) / 3;
samples.push(average);
}
input = samples;
}
return arrayFastResample(samples, points);
} else {
// In practice there's not much purpose in burning CPU for short arrays only to
// end up with a result that can't possibly look much different than the fast
// resample, so just skip ahead to the fast resample.
return arrayFastResample(input, points);
}
}
/**
* Rescales the input array to have values that are inclusively within the provided
* minimum and maximum.
* @param {number[]} input The array to rescale.
* @param {number} newMin The minimum value to scale to.
* @param {number} newMax The maximum value to scale to.
* @returns {number[]} The rescaled array.
*/
export function arrayRescale(input: number[], newMin: number, newMax: number): number[] {
const min: number = Math.min(...input);
const max: number = Math.max(...input);
return input.map((v) => percentageWithin(percentageOf(v, min, max), newMin, newMax));
}
/**
* Creates an array of the given length, seeded with the given value.
* @param {T} val The value to seed the array with.
* @param {number} length The length of the array to create.
* @returns {T[]} The array.
*/
export function arraySeed<T>(val: T, length: number): T[] {
// Size the array up front for performance, and use `fill` to let the browser
// optimize the operation better than we can with a `for` loop, if it wants.
return new Array<T>(length).fill(val);
}
/**
* Trims or fills the array to ensure it meets the desired length. The seed array
* given is pulled from to fill any missing slots - it is recommended that this be
* at least `len` long. The resulting array will be exactly `len` long, either
* trimmed from the source or filled with the some/all of the seed array.
* @param {T[]} a The array to trim/fill.
* @param {number} len The length to trim or fill to, as needed.
* @param {T[]} seed Values to pull from if the array needs filling.
* @returns {T[]} The resulting array of `len` length.
*/
export function arrayTrimFill<T>(a: T[], len: number, seed: T[]): T[] {
// Dev note: we do length checks because the spread operator can result in some
// performance penalties in more critical code paths. As a utility, it should be
// as fast as possible to not cause a problem for the call stack, no matter how
// critical that stack is.
if (a.length === len) return a;
if (a.length > len) return a.slice(0, len);
return a.concat(seed.slice(0, len - a.length));
}
/**
* Clones an array as fast as possible, retaining references of the array's values.
* @param a The array to clone. Must be defined.
* @returns A copy of the array.
*/
export function arrayFastClone<T>(a: T[]): T[] {
return a.slice(0, a.length);
}
/**
* Determines if the two arrays are different either in length, contents,
* or order of those contents.
* @param a The first array. Must be defined.
* @param b The second array. Must be defined.
* @returns True if they are different, false otherwise.
*/
export function arrayHasOrderChange(a: any[], b: any[]): boolean {
if (a.length === b.length) {
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return true;
}
return false;
} else {
return true; // like arrayHasDiff, a difference in length is a natural change
}
}
/**
* Determines if two arrays are different through a shallow comparison.
* @param a The first array. Must be defined.
* @param b The second array. Must be defined.
* @returns True if they are different, false otherwise.
*/
export function arrayHasDiff(a: any[], b: any[]): boolean {
if (a.length === b.length) {
// When the lengths are equal, check to see if either array is missing
// an element from the other.
if (b.some((i) => !a.includes(i))) return true;
if (a.some((i) => !b.includes(i))) return true;
// if all the keys are common, say so
return false;
} else {
return true; // different lengths means they are naturally diverged
}
}
export type Diff<T> = { added: T[]; removed: T[] };
/**
* Performs a diff on two arrays. The result is what is different with the
* first array (`added` in the returned object means objects in B that aren't
* in A). Shallow comparisons are used to perform the diff.
* @param a The first array. Must be defined.
* @param b The second array. Must be defined.
* @returns The diff between the arrays.
*/
export function arrayDiff<T>(a: T[], b: T[]): Diff<T> {
return {
added: b.filter((i) => !a.includes(i)),
removed: a.filter((i) => !b.includes(i)),
};
}
/**
* Returns the intersection of two arrays.
* @param a The first array. Must be defined.
* @param b The second array. Must be defined.
* @returns The intersection of the arrays.
*/
export function arrayIntersection<T>(a: T[], b: T[]): T[] {
return a.filter((i) => b.includes(i));
}
/**
* Unions arrays, deduping contents using a Set.
* @param a The arrays to merge.
* @returns The union of all given arrays.
*/
export function arrayUnion<T>(...a: T[][]): T[] {
return Array.from(
a.reduce((c, v) => {
v.forEach((i) => c.add(i));
return c;
}, new Set<T>()),
);
}
/**
* Moves a single element from fromIndex to toIndex.
* @param {array} list the list from which to construct the new list.
* @param {number} fromIndex the index of the element to move.
* @param {number} toIndex the index of where to put the element.
* @returns {array} A new array with the requested value moved.
*/
export function moveElement<T>(list: T[], fromIndex: number, toIndex: number): T[] {
const result = Array.from(list);
const [removed] = result.splice(fromIndex, 1);
result.splice(toIndex, 0, removed);
return result;
}
/**
* Helper functions to perform LINQ-like queries on arrays.
*/
export class ArrayUtil<T> {
/**
* Create a new array helper.
* @param a The array to help. Can be modified in-place.
*/
public constructor(private a: T[]) {}
/**
* The value of this array, after all appropriate alterations.
*/
public get value(): T[] {
return this.a;
}
/**
* Groups an array by keys.
* @param fn The key-finding function.
* @returns This.
*/
public groupBy<K>(fn: (a: T) => K): GroupedArray<K, T> {
const obj = this.a.reduce((rv: Map<K, T[]>, val: T) => {
const k = fn(val);
if (!rv.has(k)) rv.set(k, []);
rv.get(k)!.push(val);
return rv;
}, new Map<K, T[]>());
return new GroupedArray(obj);
}
}
/**
* Helper functions to perform LINQ-like queries on groups (maps).
*/
export class GroupedArray<K, T> {
/**
* Creates a new group helper.
* @param val The group to help. Can be modified in-place.
*/
public constructor(private val: Map<K, T[]>) {}
/**
* The value of this group, after all applicable alterations.
*/
public get value(): Map<K, T[]> {
return this.val;
}
/**
* Orders the grouping into an array using the provided key order.
* @param keyOrder The key order.
* @returns An array helper of the result.
*/
public orderBy(keyOrder: K[]): ArrayUtil<T> {
const a: T[] = [];
for (const k of keyOrder) {
if (!this.val.has(k)) continue;
a.push(...this.val.get(k)!);
}
return new ArrayUtil(a);
}
}
export const concat = (...arrays: Uint8Array[]): Uint8Array => {
return arrays.reduce((concatenatedSoFar: Uint8Array, toBeConcatenated: Uint8Array) => {
const concatenated = new Uint8Array(concatenatedSoFar.length + toBeConcatenated.length);
concatenated.set(concatenatedSoFar, 0);
concatenated.set(toBeConcatenated, concatenatedSoFar.length);
return concatenated;
}, new Uint8Array(0));
};
/**
* Async version of Array.every.
*/
export async function asyncEvery<T>(values: Iterable<T>, predicate: (value: T) => Promise<boolean>): Promise<boolean> {
for (const value of values) {
if (!(await predicate(value))) return false;
}
return true;
}
/**
* Async version of Array.some.
*/
export async function asyncSome<T>(values: Iterable<T>, predicate: (value: T) => Promise<boolean>): Promise<boolean> {
for (const value of values) {
if (await predicate(value)) return true;
}
return false;
}
/**
* Async version of Array.some that runs all promises in parallel.
* @param values
* @param predicate
*/
export async function asyncSomeParallel<T>(
values: Array<T>,
predicate: (value: T) => Promise<boolean>,
): Promise<boolean> {
try {
return await Promise.any<boolean>(
values.map((value) =>
predicate(value).then((result) => (result ? Promise.resolve(true) : Promise.reject(false))),
),
);
} catch (e) {
// If the array is empty or all the promises are false, Promise.any will reject an AggregateError
if (e instanceof AggregateError) return false;
throw e;
}
}
/**
* Async version of Array.filter.
* If one of the promises rejects, the whole operation will reject.
* @param values
* @param predicate
*/
export async function asyncFilter<T>(values: Array<T>, predicate: (value: T) => Promise<boolean>): Promise<Array<T>> {
const results = await Promise.all(values.map(predicate));
return values.filter((_, i) => results[i]);
}
export function filterBoolean<T>(values: Array<T | null | undefined>): T[] {
return values.filter(Boolean) as T[];
}