Passed
Push — master ( 6c6ee2...3c163a )
by Pauli
02:21
created

Util::arraySortByColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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