1+ <?php namespace Nabeghe \FileSize ;
2+
3+ use InvalidArgumentException ;
4+
5+ /**
6+ * Helper methods related to file size operations.
7+ */
8+ class FileSize
9+ {
10+ /**
11+ * Returns all supported units.
12+ *
13+ * - bits: ['b', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb', 'Eb', 'Zb', 'Yb']
14+ * - bytes: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
15+ *
16+ * @param bool $bits Return bit units instead of byte units.
17+ * @return array
18+ */
19+ public static function getAllUnits (bool $ bits = false ): array
20+ {
21+ return $ bits
22+ // bit, kilobit, megabit, gigabit, terabit, petabit, exabit, zettabit, yottabit
23+ ? ['b ' , 'Kb ' , 'Mb ' , 'Gb ' , 'Tb ' , 'Pb ' , 'Eb ' , 'Zb ' , 'Yb ' ]
24+ // byte, kilobyte, megabyte, gigabyte, terabyte, petabyte, exabyte, zettabyte, yottabyte
25+ : ['B ' , 'KB ' , 'MB ' , 'GB ' , 'TB ' , 'PB ' , 'EB ' , 'ZB ' , 'YB ' ];
26+ }
27+
28+ /**
29+ * Checks whether a unit is valid or not.
30+ *
31+ * @param string $unit Unit value.
32+ * @return bool
33+ */
34+ public static function isValidUnit (string $ unit ): bool
35+ {
36+ $ units = [
37+ static ::getAllUnits (),
38+ static ::getAllUnits (true ),
39+ ];
40+
41+ $ normalized_unit = ucfirst (strtolower ($ unit ));
42+
43+ if (strtolower ($ unit ) === 'b ' ) {
44+ $ normalized_unit = 'b ' ;
45+ }
46+
47+ if (strtoupper ($ unit ) === 'B ' && strlen ($ unit ) === 1 ) {
48+ $ normalized_unit = 'B ' ;
49+ }
50+
51+ return in_array ($ normalized_unit , $ units , true );
52+ }
53+
54+ /**
55+ * Format a number with optional decimals, Returns integer if no decimals needed.
56+ *
57+ * @param float|int $size Number to format.
58+ * @param int $decimals Optional. Decimal places. Default 2.
59+ * @return int|float Formatted number as int or float.
60+ */
61+ public static function format ($ size , int $ decimals = 2 )
62+ {
63+ if (is_int ($ size )) {
64+ return $ size ;
65+ }
66+
67+ $ rounded = round ($ size , $ decimals );
68+ $ is_whole = ($ rounded == (int ) $ rounded );
69+
70+ return $ is_whole ? (int ) $ rounded : $ rounded ;
71+ }
72+
73+ /**
74+ * Converts a data size from one unit to another (supports bits and bytes).
75+ *
76+ * @param float|int $size The numeric value to convert.
77+ * @param string $fromUnit Source unit (e.g. "MB", "Gb", "B", "Kb").
78+ * @param string $toUnit Target unit to convert to.
79+ * @param bool|int $format Optional. Round and format result to given decimal places (true = 2 decimals). Default false.
80+ * @param bool|int $isBinary Optional. Whether to use binary (1024) or decimal (1000) base. Default true.
81+ * @return float|int Converted value (raw or formatted).
82+ * @throws InvalidArgumentException If either unit is invalid.
83+ */
84+ public static function convert ($ size , string $ fromUnit , string $ toUnit , $ format = false , $ isBinary = true )
85+ {
86+ $ units = [
87+ 'b ' => -1 ,
88+ 'B ' => 0 ,
89+ 'KB ' => 1 ,
90+ 'MB ' => 2 ,
91+ 'GB ' => 3 ,
92+ 'TB ' => 4 ,
93+ 'PB ' => 5 ,
94+ 'EB ' => 6 ,
95+ 'ZB ' => 7 ,
96+ 'YB ' => 8 ,
97+ 'Kb ' => 1 ,
98+ 'Mb ' => 2 ,
99+ 'Gb ' => 3 ,
100+ 'Tb ' => 4 ,
101+ 'Pb ' => 5 ,
102+ 'Eb ' => 6 ,
103+ 'Zb ' => 7 ,
104+ 'Yb ' => 8 ,
105+ ];
106+
107+ if (!isset ($ units [$ fromUnit ])) {
108+ throw new InvalidArgumentException ("Invalid fromUnit: $ fromUnit " );
109+ }
110+
111+ if (!isset ($ units [$ toUnit ])) {
112+ throw new InvalidArgumentException ("Invalid toUnit: $ toUnit " );
113+ }
114+
115+ $ is_bit = function ($ unit ) {
116+ $ len = strlen ($ unit );
117+ return $ len > 0 && substr ($ unit , -1 ) === 'b ' && $ unit !== 'B ' ;
118+ };
119+
120+ $ is_from_bit = $ is_bit ($ fromUnit );
121+ $ is_to_bit = $ is_bit ($ toUnit );
122+
123+ // Get index values (powers of 1024)
124+ $ from_index = $ units [$ fromUnit ];
125+ $ to_index = $ units [$ toUnit ];
126+
127+ // Convert to bytes
128+ $ bytes = $ size ;
129+ if ($ is_from_bit ) {
130+ $ bytes = $ size / 8 ; // Convert bits to bytes
131+ }
132+ if ($ from_index > 0 ) {
133+ $ bytes *= pow ($ isBinary ? 1024 : 1000 , $ from_index ); // Convert from fromUnit to base bytes
134+ }
135+
136+ // Convert from bytes to target unit
137+ $ result = $ bytes ;
138+ if ($ to_index > 0 ) {
139+ $ result /= pow ($ isBinary ? 1024 : 1000 , $ to_index ); // Convert to toUnit
140+ }
141+ if ($ is_to_bit ) {
142+ $ result *= 8 ; // Convert bytes to bits
143+ }
144+
145+ if ($ format ) {
146+ $ result = static ::format ($ result , $ format === true ? 2 : $ format );
147+ }
148+
149+ return $ result ;
150+ }
151+
152+ /**
153+ * Compares two data sizes across different units.
154+ *
155+ * @param float|int $size1 First value to compare.
156+ * @param string $unit1 Unit of the first value (e.g. "MB", "Gb").
157+ * @param float|int $size2 Second value to compare.
158+ * @param string $unit2 Unit of the second value.
159+ * @return int Returns -1 if size1 < size2, 1 if size1 > size2, or 0 if equal.
160+ */
161+ public static function compare ($ size1 , string $ unit1 , $ size2 , string $ unit2 ): int
162+ {
163+ // Convert both values to bytes using convert method
164+ $ bytes1 = static ::convert ($ size1 , $ unit1 , 'B ' );
165+ $ bytes2 = static ::convert ($ size2 , $ unit2 , 'B ' );
166+
167+ if ($ bytes1 < $ bytes2 ) {
168+ return -1 ;
169+ } elseif ($ bytes1 > $ bytes2 ) {
170+ return 1 ;
171+ } else {
172+ return 0 ;
173+ }
174+ }
175+
176+ /**
177+ * Detects the most suitable unit for a given size (bytes or bits).
178+ *
179+ * @param float|int $size The size to evaluate.
180+ * @param bool $isBits Whether the size is in bits (default: false for bytes).
181+ * @param float|null $finalSize Outputs the normalized size after unit scaling.
182+ * @param bool|int $isBinary Optional. Whether to use binary (1024) or decimal (1000) base. Default true.
183+ * @return string The best-fit unit (e.g. "MB", "Gb").
184+ */
185+ public static function detectUnit ($ size , bool $ isBits = false , ?float &$ finalSize = null , bool $ isBinary = true ): string
186+ {
187+ $ units = $ isBits ? ['b ' , 'Kb ' , 'Mb ' , 'Gb ' , 'Tb ' , 'Pb ' , 'Eb ' , 'Zb ' , 'Yb ' ] : ['B ' , 'KB ' , 'MB ' , 'GB ' , 'TB ' , 'PB ' , 'EB ' , 'ZB ' , 'YB ' ];
188+
189+ $ divider = $ isBinary ? 1024 : 1000 ;
190+ $ unitIndex = 0 ;
191+
192+ // Normalize up (e.g. 2048 -> 2 KB)
193+ while ($ size >= $ divider && $ unitIndex < count ($ units ) - 1 ) {
194+ $ size /= $ divider ;
195+ $ unitIndex ++;
196+ }
197+
198+ // Normalize down (e.g. 0.0005 GB -> 0.5 MB)
199+ while ($ size < 1 && $ unitIndex > 0 ) {
200+ $ size *= $ divider ;
201+ $ unitIndex --;
202+ }
203+
204+ $ finalSize = $ size ;
205+
206+ return $ units [$ unitIndex ];
207+ }
208+
209+ /**
210+ * Converts a size to a human-readable string with appropriate unit.
211+ *
212+ * @param float|int $size The size to format.
213+ * @param bool $isBits Whether to use bit units (default: false for bytes).
214+ * @param array|null $labels Optional map of custom labels for units.
215+ * @param bool|int $isBinary Optional. Whether to use binary (1024) or decimal (1000) base. Default true.
216+ * @return string Human-readable formatted string (e.g. "1.5 MB").
217+ */
218+ public static function readable ($ size , bool $ isBits = false , ?array $ labels = [], bool $ isBinary = true ): string
219+ {
220+ $ unit = static ::detectUnit ($ size , $ isBits , $ finalSize , $ isBinary );
221+
222+ if ($ labels && isset ($ labels [$ unit ])) {
223+ $ unit = $ labels [$ unit ];
224+ }
225+
226+ return static ::format ($ finalSize ).' ' .$ unit ;
227+ }
228+
229+ /**
230+ * Converts a size with unit to a human-readable string.
231+ *
232+ * @param float|int $size The numeric value to convert.
233+ * @param string $unit Unit of the value (e.g. "MB", "Kb").
234+ * @param array|null $labels Optional custom unit labels.
235+ * @param bool|int $isBinary Optional. Whether to use binary (1024) or decimal (1000) base. Default true.
236+ * @return string Human-readable formatted string.
237+ */
238+ public static function readableFromUnit ($ size , string $ unit , ?array $ labels = [], bool $ isBinary = true ): string
239+ {
240+ $ is_bits = strpos ($ unit , 'b ' );
241+
242+ $ bytes = static ::convert ($ size , $ unit , $ is_bits !== false ? 'b ' : 'B ' , false , $ isBinary );
243+
244+ return static ::readable ($ bytes , $ is_bits , $ labels , $ isBinary );
245+ }
246+
247+ /**
248+ * Parses a human-readable size string like "1.5 GB" or "200 Kb".
249+ *
250+ * @param string $input Human-readable size string.
251+ * @return array [$size, $unit].
252+ * @throws InvalidArgumentException If input format or unit is invalid.
253+ */
254+ public static function parse (string $ input ): array
255+ {
256+ if (!preg_match ('/^\s*([\d.]+)\s*([a-zA-Z]+)\s*$/ ' , $ input , $ matches )) {
257+ throw new InvalidArgumentException ("Invalid size string: $ input " );
258+ }
259+
260+ $ size = (float ) $ matches [1 ];
261+ $ unit = $ matches [2 ];
262+
263+ if (!static ::isValidUnit ($ unit )) {
264+ throw new InvalidArgumentException ("Invalid unit in string: $ unit " );
265+ }
266+
267+ return [$ size , $ unit ];
268+ }
269+
270+ /**
271+ * Calculates what percentage size1 is of size2.
272+ *
273+ * @param float|int $size1 First size value.
274+ * @param string $unit1 Unit of the first size.
275+ * @param float|int $size2 Second size value.
276+ * @param string $unit2 Unit of the second size.
277+ * @param bool|int $isBinary Optional. Whether to use binary (1024) or decimal (1000) base. Default true.
278+ * @return float Percentage value.
279+ */
280+ public static function percentage ($ size1 , string $ unit1 , $ size2 , string $ unit2 , bool $ isBinary = true ): float
281+ {
282+ $ bytes1 = static ::convert ($ size1 , $ unit1 , 'B ' , false , $ isBinary );
283+ $ bytes2 = static ::convert ($ size2 , $ unit2 , 'B ' , false , $ isBinary );
284+
285+ return ($ bytes2 == 0 ) ? 0.0 : ($ bytes1 / $ bytes2 ) * 100 ;
286+ }
287+ }
0 commit comments