Passed
Push — master ( 64a75f...2a0431 )
by Pauli
03:01
created

Util::extractIds()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 2
rs 10
1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2018 - 2023
11
 */
12
13
namespace OCA\Music\Utility;
14
15
use OCP\Files\Folder;
16
17
/**
18
 * Miscellaneous static utility functions
19
 */
20
class Util {
21
22
	const UINT32_MAX = 0xFFFFFFFF;
23
	const SINT32_MAX = 0x7FFFFFFF;
24
	const SINT32_MIN = -self::SINT32_MAX - 1;
25
26
	/**
27
	 * Map the given array by calling a named member function for each of the array elements
28
	 */
29
	public static function arrayMapMethod(array $arr, string $methodName, array $methodArgs=[]) : array {
30
		$func = function ($obj) use ($methodName, $methodArgs) {
31
			return \call_user_func_array([$obj, $methodName], $methodArgs);
32
		};
33
		return \array_map($func, $arr);
34
	}
35
36
	/**
37
	 * Extract ID of each array element by calling getId and return
38
	 * the IDs as an array
39
	 */
40
	public static function extractIds(array $arr) : array {
41
		return self::arrayMapMethod($arr, 'getId');
42
	}
43
44
	/**
45
	 * Extract User ID of each array element by calling getUserId and return
46
	 * the IDs as an array
47
	 */
48
	public static function extractUserIds(array $arr) : array {
49
		return self::arrayMapMethod($arr, 'getUserId');
50
	}
51
52
	/**
53
	 * Create look-up table from given array of items which have a `getId` function.
54
	 * @return array where keys are the values returned by `getId` of each item
55
	 */
56
	public static function createIdLookupTable(array $array) : array {
57
		$lut = [];
58
		foreach ($array as $item) {
59
			$lut[$item->getId()] = $item;
60
		}
61
		return $lut;
62
	}
63
64
	/**
65
	 * Get difference of two arrays, i.e. elements belonging to $b but not $a.
66
	 * This function is faster than the built-in array_diff for large arrays but
67
	 * at the expense of higher RAM usage and can be used only for arrays of
68
	 * integers or strings.
69
	 * From https://stackoverflow.com/a/8827033
70
	 */
71
	public static function arrayDiff(array $b, array $a) : array {
72
		$at = \array_flip($a);
73
		$d = [];
74
		foreach ($b as $i) {
75
			if (!isset($at[$i])) {
76
				$d[] = $i;
77
			}
78
		}
79
		return $d;
80
	}
81
82
	/**
83
	 * Get multiple items from @a $array, as indicated by a second array @a $indices.
84
	 */
85
	public static function arrayMultiGet(array $array, array $indices) : array {
86
		$result = [];
87
		foreach ($indices as $index) {
88
			$result[] = $array[$index];
89
		}
90
		return $result;
91
	}
92
93
	/**
94
	 * Like the built-in function \array_filter but this one works recursively on nested arrays.
95
	 * Another difference is that this function always requires an explicit callback condition.
96
	 * Both inner nodes and leafs nodes are passed to the $condition.
97
	 */
98
	public static function arrayFilterRecursive(array  $array, callable $condition) : array {
99
		$result = [];
100
101
		foreach ($array as $key => $value) {
102
			if ($condition($value)) {
103
				if (\is_array($value)) {
104
					$result[$key] = self::arrayFilterRecursive($value, $condition);
105
				} else {
106
					$result[$key] = $value;
107
				}
108
			}
109
		}
110
111
		return $result;
112
	}
113
114
	/**
115
	 * Inverse operation of self::arrayFilterRecursive, keeping only those items where
116
	 * the $condition evaluates to *false*.
117
	 */
118
	public static function arrayRejectRecursive(array $array, callable $condition) : array {
119
		$invCond = function($item) use ($condition) {
120
			return !$condition($item);
121
		};
122
		return self::arrayFilterRecursive($array, $invCond);
123
	}
124
125
	/**
126
	 * Convert the given array $arr so that keys of the potentially multi-dimensional array
127
	 * are converted using the mapping given in $dictionary. Keys not found from $dictionary
128
	 * are not altered.
129
	 */
130
	public static function convertArrayKeys(array $arr, array $dictionary) : array {
131
		$newArr = [];
132
133
		foreach ($arr as $k => $v) {
134
			$key = $dictionary[$k] ?? $k;
135
			$newArr[$key] = \is_array($v) ? self::convertArrayKeys($v, $dictionary) : $v;
136
		}
137
138
		return $newArr;
139
	}
140
141
	/**
142
	 * Walk through the given, potentially multi-dimensional, array and cast all leaf nodes
143
	 * to integer type. The array is modified in-place.
144
	 */
145
	public static function intCastArrayValues(array $arr) : void {
146
		\array_walk_recursive($arr, function(&$value) {
147
			$value = \intval($value);
148
		});
149
	}
150
151
	/**
152
	 * Given a two-dimensional array, sort the outer dimension according to values in the
153
	 * specified column of the inner dimension.
154
	 */
155
	public static function arraySortByColumn(array &$arr, string $column) : void {
156
		\usort($arr, function ($a, $b) use ($column) {
157
			return self::stringCaseCompare($a[$column], $b[$column]);
158
		});
159
	}
160
161
	/**
162
	 * Like the built-in \explode(...) function but this one can be safely called with
163
	 * null string, and no warning will be emitted. Also, this returns an empty array from
164
	 * null and '' inputs while the built-in alternative returns a 1-item array containing
165
	 * an empty string.
166
	 * @param string $delimiter
167
	 * @param string|null $string
168
	 * @return array
169
	 */
170
	public static function explode(string $delimiter, ?string $string) : array {
171
		if ($delimiter === '') {
172
			throw new \UnexpectedValueException();
173
		} elseif ($string === null || $string === '') {
174
			return [];
175
		} else {
176
			return \explode($delimiter, $string);
177
		}
178
	}
179
180
	/**
181
	 * Truncate the given string to maximum length, appendig ellipsis character
182
	 * if the truncation happened. Also null argument may be safely passed and
183
	 * it remains unaltered.
184
	 */
185
	public static function truncate(?string $string, int $maxLength) : ?string {
186
		if ($string === null) {
187
			return null;
188
		} else {
189
			return \mb_strimwidth($string, 0, $maxLength, "\u{2026}");
190
		}
191
	}
192
193
	/**
194
	 * Test if given string starts with another given string
195
	 */
196
	public static function startsWith(string $string, string $potentialStart, bool $ignoreCase=false) : bool {
197
		$actualStart = \substr($string, 0, \strlen($potentialStart));
198
		if ($ignoreCase) {
199
			$actualStart = \mb_strtolower($actualStart);
200
			$potentialStart = \mb_strtolower($potentialStart);
201
		}
202
		return $actualStart === $potentialStart;
203
	}
204
205
	/**
206
	 * Test if given string ends with another given string
207
	 */
208
	public static function endsWith(string $string, string $potentialEnd, bool $ignoreCase=false) : bool {
209
		$actualEnd = \substr($string, -\strlen($potentialEnd));
210
		if ($ignoreCase) {
211
			$actualEnd = \mb_strtolower($actualEnd);
212
			$potentialEnd = \mb_strtolower($potentialEnd);
213
		}
214
		return $actualEnd === $potentialEnd;
215
	}
216
217
	/**
218
	 * Multi-byte safe case-insensitive string comparison
219
	 * @return int negative value if $a is less than $b, positive value if $a is greater than $b, and 0 if they are equal.
220
	 */
221
	public static function stringCaseCompare(?string $a, ?string $b) : int {
222
		return \strcmp(\mb_strtolower($a ?? ''), \mb_strtolower($b ?? ''));
223
	}
224
225
	/**
226
	 * Test if $item is a string and not empty or only consisting of whitespace
227
	 */
228
	public static function isNonEmptyString(/*mixed*/ $item) : bool {
229
		return \is_string($item) && \trim($item) !== '';
230
	}
231
232
	/**
233
	 * Convert file size given in bytes to human-readable format
234
	 */
235
	public static function formatFileSize(?int $bytes, int $decimals = 1) : ?string {
236
		if ($bytes === null) {
237
			return null;
238
		} else {
239
			$units = 'BKMGTP';
240
			$factor = \floor((\strlen((string)$bytes) - 1) / 3);
241
			return \sprintf("%.{$decimals}f", $bytes / \pow(1024, $factor)) . @$units[(int)$factor];
242
		}
243
	}
244
245
	/**
246
	 * Convert time given as seconds to the HH:MM:SS format
247
	 */
248
	public static function formatTime(?int $seconds) : ?string {
249
		if ($seconds === null) {
250
			return null;
251
		} else {
252
			return \sprintf('%02d:%02d:%02d', ($seconds/3600), ($seconds/60%60), $seconds%60);
253
		}
254
	}
255
256
	/**
257
	 * Convert date and time given in the SQL format to the ISO UTC "Zulu format" e.g. "2021-08-19T19:33:15Z"
258
	 */
259
	public static function formatZuluDateTime(?string $dbDateString) : ?string {
260
		if ($dbDateString === null) {
261
			return null;
262
		} else {
263
			$dateTime = new \DateTime($dbDateString);
264
			return $dateTime->format('Y-m-d\TH:i:s.v\Z');
265
		}
266
	}
267
268
	/**
269
	 * Convert date and time given in the SQL format to the ISO UTC "offset format" e.g. "2021-08-19T19:33:15+00:00"
270
	 */
271
	public static function formatDateTimeUtcOffset(?string $dbDateString) : ?string {
272
		if ($dbDateString === null) {
273
			return null;
274
		} else {
275
			$dateTime = new \DateTime($dbDateString);
276
			return $dateTime->format('c');
277
		}
278
	}
279
280
	/**
281
	 * Get a Folder object using a parent Folder object and a relative path
282
	 */
283
	public static function getFolderFromRelativePath(Folder $parentFolder, string $relativePath) : Folder {
284
		if ($relativePath !== null && $relativePath !== '/' && $relativePath !== '') {
285
			$node = $parentFolder->get($relativePath);
286
			if ($node instanceof Folder) {
287
				return $node;
288
			} else {
289
				throw new \InvalidArgumentException('Path points to a file while folder expected');
290
			}
291
		} else {
292
			return $parentFolder;
293
		}
294
	}
295
296
	/**
297
	 * Create relative path from the given working dir (CWD) to the given target path
298
	 * @param string $cwdPath Absolute CWD path
299
	 * @param string $targetPath Absolute target path
300
	 */
301
	public static function relativePath(string $cwdPath, string $targetPath) : string {
302
		$cwdParts = \explode('/', $cwdPath);
303
		$targetParts = \explode('/', $targetPath);
304
305
		// remove the common prefix of the paths
306
		while (\count($cwdParts) > 0 && \count($targetParts) > 0 && $cwdParts[0] === $targetParts[0]) {
307
			\array_shift($cwdParts);
308
			\array_shift($targetParts);
309
		}
310
311
		// prepend up-navigation from CWD to the closest common parent folder with the target
312
		for ($i = 0, $count = \count($cwdParts); $i < $count; ++$i) {
313
			\array_unshift($targetParts, '..');
314
		}
315
316
		return \implode('/', $targetParts);
317
	}
318
319
	/**
320
	 * Given a current working directory path (CWD) and a relative path (possibly containing '..' parts),
321
	 * form an absolute path matching the relative path. This is a reverse operation for Util::relativePath().
322
	 */
323
	public static function resolveRelativePath(string $cwdPath, string $relativePath) : string {
324
		$cwdParts = \explode('/', $cwdPath);
325
		$relativeParts = \explode('/', $relativePath);
326
327
		// get rid of the trailing empty part of CWD which appears when CWD has a trailing '/'
328
		if ($cwdParts[\count($cwdParts)-1] === '') {
329
			\array_pop($cwdParts);
330
		}
331
332
		foreach ($relativeParts as $part) {
333
			if ($part === '..') {
334
				\array_pop($cwdParts);
335
			} else {
336
				\array_push($cwdParts, $part);
337
			}
338
		}
339
340
		return \implode('/', $cwdParts);
341
	}
342
343
	/**
344
	 * Encode a file path so that it can be used as part of a WebDAV URL
345
	 */
346
	public static function urlEncodePath(string $path) : string {
347
		// URL encode each part of the file path
348
		return \join('/', \array_map('rawurlencode', \explode('/', $path)));
349
	}
350
351
	/**
352
	 * Compose URL from parts as returned by the system function parse_url.
353
	 * From https://stackoverflow.com/a/35207936
354
	 */
355
	public static function buildUrl(array $parts) : string {
356
		return (isset($parts['scheme']) ? "{$parts['scheme']}:" : '') .
357
				((isset($parts['user']) || isset($parts['host'])) ? '//' : '') .
358
				(isset($parts['user']) ? "{$parts['user']}" : '') .
359
				(isset($parts['pass']) ? ":{$parts['pass']}" : '') .
360
				(isset($parts['user']) ? '@' : '') .
361
				(isset($parts['host']) ? "{$parts['host']}" : '') .
362
				(isset($parts['port']) ? ":{$parts['port']}" : '') .
363
				(isset($parts['path']) ? "{$parts['path']}" : '') .
364
				(isset($parts['query']) ? "?{$parts['query']}" : '') .
365
				(isset($parts['fragment']) ? "#{$parts['fragment']}" : '');
366
	}
367
368
	/**
369
	 * Swap values of two variables in place
370
	 * @param mixed $a
371
	 * @param mixed $b
372
	 */
373
	public static function swap(&$a, &$b) : void {
374
		$temp = $a;
375
		$a = $b;
376
		$b = $temp;
377
	}
378
379
	/**
380
	 * Limit an integer value between the specified minimum and maximum.
381
	 * A null value is a valid input and will produce a null output.
382
	 */
383
	public static function limit(?int $input, int $min, int $max) : ?int {
384
		if ($input === null) {
385
			return null;
386
		} else {
387
			return \max($min, \min($input, $max));
388
		}
389
	}
390
}
391