small array utilities
5.8 kB
215 lines
1/**
2 * returns a sequence of numbers
3 * @param start starting number (inclusive)
4 * @param end ending number (exclusive)
5 * @param step steps between numbers
6 * @returns an array containing the specified sequence of numbers
7 */
8/*#__NO_SIDE_EFFECTS__*/
9export const range = (start: number, end: number, step: number = 1): number[] => {
10 const len = Math.max(Math.ceil((end - start) / step), 0);
11 const result = new Array(len);
12
13 for (let i = 0; i < len; i++) {
14 result[i] = start + (i * step);
15 }
16
17 return result;
18};
19
20/**
21 * splits an array into chunks
22 * @param array array to chunk
23 * @param size maximum amount of elements per chunk
24 * @returns array containing chunks of elements
25 */
26/*#__NO_SIDE_EFFECTS__*/
27export const chunked = <T>(array: T[], size: number): T[][] => {
28 const chunks: T[][] = [];
29
30 for (let i = 0, il = array.length; i < il; i += size) {
31 chunks.push(array.slice(i, i + size));
32 }
33
34 return chunks;
35};
36
37/**
38 * returns transformed elements, skipping undefined values
39 * @param array array to transform
40 * @param mapper function that returns either the transformed element, or undefined
41 * @returns an array of transformed elements
42 */
43/*#__NO_SIDE_EFFECTS__*/
44export const mapDefined = <T, R>(array: T[], mapper: (value: T) => R | undefined): R[] => {
45 const len = array.length;
46 const mapped: R[] = [];
47
48 let idx = 0;
49 let temp: R | undefined;
50
51 for (; idx < len; idx++) {
52 if ((temp = mapper(array[idx])) !== undefined) {
53 mapped.push(temp);
54 }
55 }
56
57 return mapped;
58};
59
60/**
61 * partitions an array into two, based on whether they satisfy a given test
62 * @param array array to test
63 * @param predicate function to test each element with
64 * @returns a tuple, with the first array containing elements that satisfy the test,
65 * and the second array containing elements that doesn't satisfy the test.
66 */
67/*#__NO_SIDE_EFFECTS__*/
68export const partition = <T>(array: T[], predicate: (item: T) => unknown): [T[], T[]] => {
69 const a: T[] = [];
70 const b: T[] = [];
71
72 for (const item of array) {
73 (predicate(item) ? a : b).push(item);
74 }
75
76 return [a, b];
77};
78
79/**
80 * returns elements present in the first array but not in the second
81 * @param a the source array
82 * @param b the array to compare against
83 * @returns an array of elements in a not present in b
84 */
85/*#__NO_SIDE_EFFECTS__*/
86export const difference = <T>(a: T[], b: T[]): T[] => {
87 return [...new Set(a).difference(new Set(b))];
88};
89
90/**
91 * returns elements present in the first array but not in the second, based on a selector
92 * @param a the source array
93 * @param b the array to compare against
94 * @param selector function to derive comparison keys
95 * @returns an array of elements in a not present in b based on the selector
96 */
97/*#__NO_SIDE_EFFECTS__*/
98export const differenceBy = <T, K>(a: T[], b: T[], selector: (value: T) => K): T[] => {
99 const bSet = new Set(b.map(selector));
100 const aSet = new Set<K>();
101
102 return a.filter((value) => {
103 const key = selector(value);
104
105 if (!bSet.has(key) && !aSet.has(key)) {
106 aSet.add(key);
107 return true;
108 }
109
110 return false;
111 });
112};
113
114/**
115 * returns elements common to both arrays
116 * @param a the first array
117 * @param b the second array
118 * @returns an array of elements present in both a and b
119 */
120/*#__NO_SIDE_EFFECTS__*/
121export const intersection = <T>(a: T[], b: T[]): T[] => {
122 return [...new Set(a).intersection(new Set(b))];
123};
124
125/**
126 * returns an array with duplicate elements removed
127 * @param array the array to deduplicate
128 * @returns a new array with unique elements
129 */
130/*#__NO_SIDE_EFFECTS__*/
131export const unique = <T>(array: T[]): T[] => {
132 return [...new Set(array)];
133};
134
135/**
136 * returns an array with elements unique by a selector's return value
137 * @param array the array to process
138 * @param selector function to derive uniqueness keys
139 * @returns a new array with elements unique based on the selector's key
140 */
141/*#__NO_SIDE_EFFECTS__*/
142export const uniqueBy = <T, K>(array: T[], selector: (value: T, index: number) => K): T[] => {
143 const keys = new Set<K>();
144 const values: T[] = [];
145
146 for (let i = 0, il = array.length; i < il; i++) {
147 const value = array[i];
148 const key = selector(value, i);
149
150 if (keys.has(key)) {
151 continue;
152 }
153
154 keys.add(key);
155 values.push(value);
156 }
157
158 return values;
159};
160
161/**
162 * returns a random element from the array
163 * @param arr the array to sample
164 * @returns a random element or undefined if the array is empty
165 */
166/*#__NO_SIDE_EFFECTS__*/
167export const sampleOne = <T>(arr: T[]): T | undefined => {
168 const len = arr.length;
169 return len !== 0 ? arr[Math.floor(Math.random() * len)] : undefined;
170};
171
172/**
173 * selects a random subset of up to n elements from the array
174 * @param arr the array to sample from
175 * @param n the maximum number of elements to select
176 * @returns a new array containing up to n randomly selected elements
177 */
178/*#__NO_SIDE_EFFECTS__*/
179export const sample = <T>(arr: T[], n: number): T[] => {
180 const result = [...arr];
181 const len = result.length;
182
183 n = Math.min(n, len);
184
185 for (let i = 0; i < n; i++) {
186 const j = i + Math.floor(Math.random() * (len - i));
187 [result[i], result[j]] = [result[j], result[i]];
188 }
189
190 return len > n ? result.slice(0, Math.max(0, n)) : result;
191};
192
193/**
194 * shuffles the elements of the array
195 * @param arr the array to shuffle
196 * @returns a new array with elements in random order
197 */
198/*#__NO_SIDE_EFFECTS__*/
199export const shuffle = <T>(arr: T[]): T[] => {
200 return sample(arr, Infinity);
201};
202
203/** a type of falsy values */
204export type FalsyValue = false | null | undefined | 0 | '';
205
206/**
207 * filters out falsy values from the array
208 * @param arr the array to filter
209 * @returns a new array with all falsy values removed
210 */
211/*#__NO_SIDE_EFFECTS__*/
212export const definite = <T>(arr: (T | FalsyValue)[]): T extends FalsyValue ? never[] : T[] => {
213 // deno-lint-ignore no-explicit-any
214 return arr.filter(Boolean) as any;
215};