├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── src ├── FileSize.php └── FileSize │ ├── Exception │ └── FileSizeException.php │ ├── Math │ └── Math.php │ ├── Parser │ └── SizeStringParser.php │ └── UnitMap │ ├── UnitMap.php │ └── UnitMapper.php └── tests ├── FileSizeTest.php └── phpunit.xml /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | composer.lock 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Chris Ullyott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Stable Version](https://poser.pugx.org/chrisullyott/php-filesize/v/stable)](https://packagist.org/packages/chrisullyott/php-filesize) 2 | [![Total Downloads](https://poser.pugx.org/chrisullyott/php-filesize/downloads)](https://packagist.org/packages/chrisullyott/php-filesize) 3 | 4 | # php-filesize 5 | 6 | A flexible package for handling file sizes and converting between units. 7 | 8 | ### Installation 9 | 10 | Include in your project, or, install with [Composer](https://getcomposer.org/): 11 | 12 | ```bash 13 | $ composer require chrisullyott/php-filesize 14 | ``` 15 | 16 | ### Instantiate 17 | 18 | A `FileSize` object, both on creation and within its methods, understands just about any expression of data size. You may instantiate it with a size, or leave it initially empty. 19 | 20 | ```php 21 | use ChrisUllyott\FileSize; 22 | 23 | $size = new FileSize('500 GB'); 24 | ``` 25 | 26 | ### Convert between units 27 | 28 | Use `as()` to export the size in another format. 29 | 30 | ```php 31 | echo $size->as('MB'); // 512000 32 | ``` 33 | 34 | A variety of file size strings are supported here as well. 35 | 36 | ```php 37 | echo $size->as('megabytes'); // 512000 38 | ``` 39 | 40 | The second argument specifies decimal precision (default is `2`). 41 | 42 | ```php 43 | echo $size->as('TB', 3); // 0.488 44 | ``` 45 | 46 | ### User-friendly formatting 47 | 48 | Use `asAuto()` to get a user-friendly string: 49 | 50 | ```php 51 | $size = new FileSize('1234522678.12 KB'); 52 | 53 | echo $size->asAuto(); // '1.15 TB' 54 | ``` 55 | 56 | Optionally, `asAuto()` also provides a decimal precision. 57 | 58 | ```php 59 | $size = new FileSize('1234522678.12 KB'); 60 | 61 | echo $size->asAuto(5); // '1.14974 TB' 62 | ``` 63 | 64 | Or, simply `echo` the object for the same functionality: 65 | 66 | ```php 67 | echo $size; // '1.15 TB' 68 | ``` 69 | 70 | ### Modify the size 71 | 72 | To make changes, use `add()`, `subtract()`, `multiplyBy()`, and `divideBy()`. 73 | 74 | ```php 75 | $size = new FileSize('4 GB'); 76 | 77 | $size->add('2G') 78 | ->subtract('1 gigabytes') 79 | ->multiplyBy(4) 80 | ->divideBy(2); 81 | 82 | echo $size; // '10.00 GB' 83 | ``` 84 | 85 | Negative values are supported. In the case below, 1.2 megabytes are subtracted: 86 | 87 | ```php 88 | $size->add('-1.2mb'); 89 | ``` 90 | 91 | You may also use `add()` and `subtract()` with an array of values: 92 | 93 | ```php 94 | $size->add(['50mb', '140mb', '1.2mb']); 95 | ``` 96 | 97 | ### Number base 98 | 99 | The second argument of the constructor is the number base, which accepts either `2` (binary) or `10` (decimal). We use binary by default. To handle sizes in decimal: 100 | 101 | ```php 102 | $size = new FileSize(10921134, 10); 103 | 104 | echo $size; // '10.92 MB' 105 | ``` 106 | 107 | ### Decimal separator 108 | 109 | The third argument of the constructor is the decimal separator, which is a period `.` by default. Here, you can use a comma instead. The chosen decimal separator will be used both to parse numbers properly, and also to format them on output. 110 | 111 | ```php 112 | $size = new FileSize('1.234.522.678,12 KB', 2, ','); 113 | 114 | echo $size; // '1,15 TB' 115 | ``` 116 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrisullyott/php-filesize", 3 | "type": "library", 4 | "description": "Easily calculate file sizes and convert between units.", 5 | "homepage": "https://github.com/chrisullyott/php-filesize", 6 | "license": "MIT", 7 | "authors": [{ 8 | "name": "Chris Ullyott", 9 | "email": "contact@chrisullyott.com", 10 | "homepage": "http://www.chrisullyott.com" 11 | }], 12 | "support": { 13 | "issues": "https://github.com/chrisullyott/php-filesize/issues", 14 | "source": "https://github.com/chrisullyott/php-filesize" 15 | }, 16 | "require": { 17 | "php": ">=7.1" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "^7" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "ChrisUllyott\\": "src/" 25 | } 26 | }, 27 | "keywords": [ 28 | "php", 29 | "size-calculation" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/FileSize.php: -------------------------------------------------------------------------------- 1 | unitMapper = new UnitMapper(); 54 | 55 | $this->base = $base; 56 | $this->decimalMark = $decimalMark; 57 | 58 | $this->bytes = $size ? $this->sizeToBytes($size) : 0; 59 | } 60 | 61 | /** 62 | * Get the byte count from an arbitrary size string. 63 | * 64 | * @param string|int $size Such as '100 MB' 65 | * @return int 66 | */ 67 | private function sizeToBytes($size) 68 | { 69 | if (is_int($size)) return $size; 70 | 71 | $object = SizeStringParser::parse($size); 72 | $value = $this->toFloatValue($object->value); 73 | $unit = $object->unit ?? UnitMap::BYTE; 74 | 75 | return $this->convert($value, $unit, UnitMap::BYTE); 76 | } 77 | 78 | /** 79 | * Add one or many filesizes. 80 | * 81 | * @param array|string|int $sizes 82 | * @return self 83 | */ 84 | public function add($sizes) 85 | { 86 | foreach ((array) $sizes as $size) { 87 | $this->addSize($size); 88 | } 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * Subtract one or many filesizes. 95 | * 96 | * @param array|string|int $sizes 97 | * @return self 98 | */ 99 | public function subtract($sizes) 100 | { 101 | foreach ((array) $sizes as $size) { 102 | $this->subtractSize($size); 103 | } 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Add to this filesize. 110 | * 111 | * @param string|int $size Such as '100 MB' 112 | * @return self 113 | */ 114 | private function addSize($size) 115 | { 116 | $this->bytes += $this->sizeToBytes($size); 117 | 118 | return $this; 119 | } 120 | 121 | /** 122 | * Subtract from this filesize. 123 | * 124 | * @param string|int $size Such as '100 MB' 125 | * @return self 126 | */ 127 | private function subtractSize($size) 128 | { 129 | $this->bytes -= $this->sizeToBytes($size); 130 | 131 | return $this; 132 | } 133 | 134 | /** 135 | * Multiply the filesize by a number. 136 | * 137 | * @param int|float $n A number 138 | * @return self 139 | */ 140 | public function multiplyBy($n) 141 | { 142 | $this->bytes = $this->formatBytes($this->bytes * $n); 143 | 144 | return $this; 145 | } 146 | 147 | /** 148 | * Divide the filesize by a number. 149 | * 150 | * @param int|float $n A number 151 | * @return self 152 | */ 153 | public function divideBy($n) 154 | { 155 | return $this->multiplyBy(1 / $n); 156 | } 157 | 158 | /** 159 | * Get the filesize in a given unit. 160 | * 161 | * @param string $unitString Unit such as 'B', 'KB', etc 162 | * @param int $precision Round to this many decimal places 163 | * @return float|int 164 | */ 165 | public function as($unitString, $precision = 2) 166 | { 167 | return $this->convert($this->bytes, UnitMap::BYTE, $unitString, $precision); 168 | } 169 | 170 | /** 171 | * Get the filesize as a human-friendly string. 172 | * 173 | * @param int $precision Round to this many decimal places 174 | * @return string 175 | */ 176 | public function asAuto($precision = 2) 177 | { 178 | $factor = Math::factorByBytes($this->bytes); 179 | $size = $this->bytes / Math::bytesByFactor($factor, $this->base); 180 | $unit = $this->unitMapper->keyFromIndex($factor); 181 | 182 | return $this->formatNumber($size, $precision, $unit); 183 | } 184 | 185 | /** 186 | * Print the filesize as a human-friendly string. 187 | * 188 | * @return string 189 | */ 190 | public function __toString() 191 | { 192 | return $this->asAuto(); 193 | } 194 | 195 | /** 196 | * Change the filesize unit measurement using arbitrary units. 197 | * 198 | * @param int $size The current size 199 | * @param string $fromUnit The current unit 200 | * @param string $toUnit The desired unit 201 | * @param int $precision Round to this many decimal places 202 | * @return float|int 203 | */ 204 | private function convert($size, $fromUnit, $toUnit, $precision = null) 205 | { 206 | $fromUnit = $this->unitMapper->keyFromString($fromUnit); 207 | $toUnit = $this->unitMapper->keyFromString($toUnit); 208 | 209 | if ($fromUnit !== $toUnit) { 210 | $index1 = $this->unitMapper->indexFromKey($fromUnit); 211 | $index2 = $this->unitMapper->indexFromKey($toUnit); 212 | $factor = $index1 - $index2; 213 | $size = (float) $size * Math::bytesByFactor($factor, $this->base); 214 | } 215 | 216 | if ($toUnit === UnitMap::BYTE) { 217 | return $this->formatBytes($size); 218 | } 219 | 220 | return $this->formatNumber($size, $precision); 221 | } 222 | 223 | /** 224 | * Convert a numeric string to a float, accounting for the decimal separator. 225 | * 226 | * @param string $value A numeric string 227 | * @return float 228 | */ 229 | private function toFloatValue($value) 230 | { 231 | $value = strval($value); 232 | 233 | $chars = '0-9' . preg_quote('-' . $this->decimalMark); 234 | $value = preg_replace("/[^{$chars}]/", '', $value); 235 | $value = str_replace($this->decimalMark, '.', $value); 236 | 237 | return floatval($value); 238 | } 239 | 240 | /** 241 | * Format a numeric string into a byte count (integer). 242 | * 243 | * @param string $number A numeric string or float 244 | * @return int 245 | */ 246 | private function formatBytes($number) 247 | { 248 | return (int) ceil($number); 249 | } 250 | 251 | /** 252 | * Format a number for output. 253 | * 254 | * @param float|int $value The number value 255 | * @param int $precision Round to this many decimal places 256 | * @param string $unit A unit string to append 257 | * @return float|string 258 | */ 259 | private function formatNumber($value, $precision = null, $unit = null) 260 | { 261 | $value = !is_null($precision) ? round($value, $precision) : $value; 262 | 263 | if ($this->decimalMark !== '.') { 264 | $value = str_replace('.', $this->decimalMark, $value); 265 | } 266 | 267 | return $unit ? "{$value} {$unit}" : $value; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/FileSize/Exception/FileSizeException.php: -------------------------------------------------------------------------------- 1 | $matches[1], 'unit' => $matches[2] ?? null]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/FileSize/UnitMap/UnitMap.php: -------------------------------------------------------------------------------- 1 | ['b', 'byte'], 23 | 'KB' => ['k', 'kb', 'kib', 'kilobyte', 'kibibyte'], 24 | 'MB' => ['m', 'mb', 'mib', 'megabyte', 'mebibyte'], 25 | 'GB' => ['g', 'gb', 'gib', 'gigabyte', 'gibibyte'], 26 | 'TB' => ['t', 'tb', 'tib', 'terabyte', 'tebibyte'], 27 | 'PB' => ['p', 'pb', 'pib', 'petabyte', 'pebibyte'], 28 | 'EB' => ['e', 'eb', 'eib', 'exabyte', 'exbibyte'], 29 | 'ZB' => ['z', 'zb', 'zib', 'zettabyte', 'zebibyte'], 30 | 'YB' => ['y', 'yb', 'yib', 'yottabyte', 'yobibyte'] 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /src/FileSize/UnitMap/UnitMapper.php: -------------------------------------------------------------------------------- 1 | 'MB'. 5 | */ 6 | 7 | namespace ChrisUllyott\FileSize\UnitMap; 8 | 9 | use ChrisUllyott\FileSize\Exception\FileSizeException; 10 | 11 | class UnitMapper 12 | { 13 | /** 14 | * A store of unit map keys. 15 | * 16 | * @var array 17 | */ 18 | private $keys = []; 19 | 20 | /** 21 | * A store of previously mapped unit strings. 22 | * 23 | * @var array 24 | */ 25 | private $cache = []; 26 | 27 | /** 28 | * Constructor. 29 | */ 30 | public function __construct() 31 | { 32 | $keyStrings = array_keys(UnitMap::$map); 33 | $keyIndeces = array_flip($keyStrings); 34 | 35 | $this->keys = [ 36 | 'by_index' => $keyStrings, 37 | 'by_string' => $keyIndeces 38 | ]; 39 | } 40 | 41 | /** 42 | * Map an arbitrary unit string to a unit map key. 43 | * 44 | * @param string $unitString Such as 'Megabytes' 45 | * @return string 46 | */ 47 | public function keyFromString($unitString) 48 | { 49 | if (isset($this->cache[$unitString])) { 50 | return $this->cache[$unitString]; 51 | } 52 | 53 | $sanitizedString = self::sanitizeUnitString($unitString); 54 | 55 | foreach (UnitMap::$map as $key => $list) { 56 | if (in_array($sanitizedString, $list)) { 57 | $this->cache[$unitString] = $key; 58 | return $key; 59 | } 60 | } 61 | 62 | throw new FileSizeException("Unrecognized unit \"{$unitString}\""); 63 | } 64 | 65 | /** 66 | * Look up a map index number from a key. 67 | * 68 | * @param string $key Such as 'MB' 69 | * @return int 70 | */ 71 | public function indexFromKey($key) 72 | { 73 | return $this->keys['by_string'][$key]; 74 | } 75 | 76 | /** 77 | * Look up a map key from an index number. 78 | * 79 | * @param int $index An integer 80 | * @return string 81 | */ 82 | public function keyFromIndex($index) 83 | { 84 | return $this->keys['by_index'][$index]; 85 | } 86 | 87 | /** 88 | * Sanitize a unit string into a version that can be mapped. 89 | * 90 | * @param string $unitString Such as '100 MB' 91 | * @return string 92 | */ 93 | private static function sanitizeUnitString($unitString) 94 | { 95 | return str_replace('bytes', 'byte', strtolower($unitString)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/FileSizeTest.php: -------------------------------------------------------------------------------- 1 | assertSame($size->asAuto(), '10.42 MB'); 20 | } 21 | 22 | /** 23 | * @test 24 | */ 25 | public function base_ten_conversions_are_accurate() 26 | { 27 | $size = new FileSize(10921134, 10); 28 | 29 | $this->assertSame($size->asAuto(), '10.92 MB'); 30 | } 31 | 32 | /** 33 | * @test 34 | */ 35 | public function bytes_are_returned_as_an_integer() 36 | { 37 | $size = new FileSize('128974848'); 38 | 39 | $this->assertSame($size->as('B'), 128974848); 40 | } 41 | 42 | /** 43 | * @test 44 | */ 45 | public function partial_bytes_are_rounded_up() 46 | { 47 | $size = new FileSize('99.7 bytes'); 48 | 49 | $this->assertSame($size->as('B'), 100); 50 | } 51 | 52 | /** 53 | * @test 54 | */ 55 | public function sizes_can_be_added() 56 | { 57 | $size = new FileSize('123 megabytes'); 58 | $size->add('150 KiB'); 59 | 60 | $this->assertSame($size->as('B'), 129128448); 61 | } 62 | 63 | /** 64 | * @test 65 | */ 66 | public function negative_sizes_can_be_added() 67 | { 68 | $size = new FileSize('10 MB'); 69 | $size->add('-20 MB'); 70 | 71 | $this->assertSame($size->asAuto(), '-10 MB'); 72 | } 73 | 74 | /** 75 | * @test 76 | */ 77 | public function sizes_can_be_subtracted() 78 | { 79 | $size = new FileSize('123 M'); 80 | $size->subtract('150 kilobytes'); 81 | 82 | $this->assertSame($size->as('B'), 128821248); 83 | } 84 | 85 | /** 86 | * @test 87 | */ 88 | public function negative_sizes_can_be_subtracted() 89 | { 90 | $size = new FileSize('10 MB'); 91 | $size->subtract('-20 MB'); 92 | 93 | $this->assertSame($size->asAuto(), '30 MB'); 94 | } 95 | 96 | /** 97 | * @test 98 | */ 99 | public function arrays_can_be_added() 100 | { 101 | $size = new FileSize(); 102 | $size->add(['50mb', '140mb', '1.2mb']); 103 | 104 | $this->assertSame($size->as('MB'), 191.2); 105 | } 106 | 107 | /** 108 | * @test 109 | */ 110 | public function sizes_can_be_multiplied() 111 | { 112 | $size = new FileSize('425.51 m'); 113 | $size->multiplyBy(9.125); 114 | 115 | $this->assertSame($size->as('GB'), 3.79); 116 | } 117 | 118 | /** 119 | * @test 120 | */ 121 | public function sizes_can_be_divided() 122 | { 123 | $size = new FileSize('300K'); 124 | $size->divideBy(2); 125 | 126 | $this->assertSame($size->as('KiB'), (float) 150); 127 | } 128 | 129 | /** 130 | * @test 131 | */ 132 | public function sizes_can_be_converted_up() 133 | { 134 | $size = new FileSize('123456789 TB'); 135 | 136 | $this->assertSame($size->as('exabytes'), 5.74); 137 | } 138 | 139 | /** 140 | * @test 141 | */ 142 | public function sizes_can_be_converted_down() 143 | { 144 | $size = new FileSize('1 GB'); 145 | 146 | $this->assertSame($size->as('megabytes'), (float) 1024); 147 | } 148 | 149 | /** 150 | * @test 151 | */ 152 | public function size_value_is_unchanged_without_conversion() 153 | { 154 | $size = new FileSize('525 GB'); 155 | 156 | $this->assertSame($size->as('GB'), (float) 525); 157 | } 158 | 159 | /** 160 | * @test 161 | */ 162 | public function friendly_formatting_is_valid_for_small_values() 163 | { 164 | $size = new FileSize('1.2345 KB'); 165 | $size->divideBy(3); 166 | 167 | $this->assertSame($size->asAuto(), '422 B'); 168 | } 169 | 170 | /** 171 | * @test 172 | */ 173 | public function friendly_formatting_is_valid_for_large_values() 174 | { 175 | $size = new FileSize('1234522678.12 KB'); 176 | 177 | $this->assertSame($size->asAuto(), '1.15 TB'); 178 | } 179 | 180 | /** 181 | * @test 182 | */ 183 | public function custom_decimal_mark_is_supported() 184 | { 185 | $size = new FileSize(10921134, 10, ','); 186 | 187 | $this->assertSame($size->asAuto(), '10,92 MB'); 188 | } 189 | 190 | /** 191 | * @test 192 | */ 193 | public function custom_decimal_and_thousands_marks_are_supported() 194 | { 195 | $size = new FileSize('1.234.522.678,12 KB', 2, ','); 196 | 197 | $this->assertSame($size->asAuto(), '1,15 TB'); 198 | } 199 | 200 | /** 201 | * @test 202 | */ 203 | public function smallest_integer_is_supported() 204 | { 205 | $size = new FileSize(PHP_INT_MIN); 206 | 207 | $this->assertIsNumeric($size->as('YB')); 208 | } 209 | 210 | /** 211 | * @test 212 | */ 213 | public function largest_integer_is_supported() 214 | { 215 | $size = new FileSize(PHP_INT_MAX); 216 | 217 | $this->assertIsNumeric($size->as('YB')); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | ./ 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | --------------------------------------------------------------------------------