將位元組數值轉換為 KB、MB、GB 等人類可讀格式是常見需求。本文提供高效能、支援 IEC/SI 雙制、可配置精度的 formatBytes() 函數,並對比常見實作陷阱。
<?php
/**
* 格式化位元組大小
* @param int|float $bytes 位元組數 (負數視為 0)
* @param int $decimals 小數位數 (預設 2)
* @param string $sep 數值與單位分隔符 (預設空格)
* @param bool $binary true=1024進制(IEC), false=1000進制(SI)
* @return string 例:1.46 MiB、1.5 MB、1024 B
*/
function formatBytes(int|float $bytes, int $decimals = 2, string $sep = ' ', bool $binary = true): string {
if ($bytes < 0) $bytes = 0;
$base = $binary ? 1024 : 1000;
$units = $binary
? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
if ($bytes < $base) {
return round($bytes, $decimals) . $sep . $units[0];
}
// 使用對數快速定位單位指數,避免迴圈
$exp = (int)floor(log($bytes, $base));
$exp = min($exp, count($units) - 1);
$value = $bytes / ($base ** $exp);
return round($value, $decimals) . $sep . $units[$exp];
}
// 測試
$tests = [0, 500, 1023, 1024, 1536, 1048576, 1073741824, 1099511627776, 1.2e15];
foreach ($tests as $b) {
echo "{$b} => " . formatBytes($b) . " | " . formatBytes($b, 1, '', false) . "\n";
}
/*
0 => 0 B | 0B
500 => 500 B | 500B
1023 => 1023 B | 1.0KB
1024 => 1.00 KiB | 1.0KB
1536 => 1.50 KiB | 1.5KB
1048576 => 1.00 MiB | 1.0MB
1073741824 => 1.00 GiB | 1.0GB
1099511627776 => 1.00 TiB | 1.0TB
1.2E+15 => 1.10 PiB | 1.1PB
*/
?>
| 實作方式 | 代碼片段 | 問題 | 修正 |
|---|---|---|---|
| 遞迴 | if($b>1024) return format($b/1024,$t+1); | 遞迴深度有限、效能差、單位陣列索引易錯位 | 用對數定位或迴圈 |
| 迴圈除法 | while($b>=1024){$b/=1024;$i++;} | 較慢、浮點誤差累積、需額外判斷 $b<1024 邊界 | 可用,但不如對數快 |
| 對數定位 | $exp=floor(log($b,1024)); | 極快 O(1)、無誤差累積、代碼簡潔 | 推薦 |
| sprintf 格式化 | sprintf('%.2f %s',$b,$unit) | 需先算好單位,配合對數法最佳 | 可用於最終格式化 |
<?php
function formatBytesLocalized(float $bytes, string $locale = 'zh_TW', bool $binary = true): string {
if (!extension_loaded('intl')) return formatBytes($bytes, 2, ' ', $binary);
$fmt = new NumberFormatter($locale, NumberFormatter::DECIMAL);
$fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 2);
$fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 0);
$base = $binary ? 1024 : 1000;
$units = $binary
? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
if ($bytes < $base) return $fmt->format($bytes) . ' ' . $units[0];
$exp = min((int)floor(log($bytes, $base)), count($units) - 1);
$value = $bytes / ($base ** $exp);
return $fmt->format($value) . ' ' . $units[$exp];
}
// 測試
echo formatBytesLocalized(1572864, 'zh_TW'); // 1.50 MiB
echo formatBytesLocalized(1572864, 'en_US'); // 1.5 MiB
echo formatBytesLocalized(1572864, 'de_DE'); // 1,5 MiB
echo formatBytesLocalized(1572864, 'zh_CN'); // 1.50 MiB
?>
| 標準 | 基數 | 單位符號 | 適用場景 |
|---|---|---|---|
| IEC 60027-2 (二進制) | 1024 | KiB, MiB, GiB, TiB... | 記憶體、檔案大小、作業系統顯示 |
| SI (十進制) | 1000 | KB, MB, GB, TB... | 硬碟廠商標示、網路頻寬、資料傳輸速率 |
注意:1 TB 硬碟 (SI) = 1,000,000,000,000 B ≈ 931 GiB (IEC),OS 顯示較小是正常現象。
-- MySQL 內建函數
SELECT FORMAT_BYTES(1073741824); -- 1.00 GiB
SELECT FORMAT_BYTES(1000000000); -- 1000.00 MB (SI)
-- 自訂函數 (舊版本)
CREATE FUNCTION format_bytes(bytes BIGINT, binary BOOL) RETURNS VARCHAR(32)
DETERMINISTIC
BEGIN
DECLARE units VARCHAR(64) DEFAULT 'B,KB,MB,GB,TB,PB';
DECLARE base INT DEFAULT IF(binary, 1024, 1000);
DECLARE i INT DEFAULT 0;
DECLARE val DOUBLE DEFAULT bytes;
IF bytes < base THEN RETURN CONCAT(ROUND(bytes,2),' B'); END IF;
WHILE val >= base AND i < 5 DO
SET val = val / base;
SET i = i + 1;
END WHILE;
RETURN CONCAT(ROUND(val,2),' ',SPLIT_STR(units,',',i+1));
END;
formatBytes():O(1) 效能、無遞迴/迴圈、支援雙制、參數化配置。$binary=false。intl 擴展使用 NumberFormatter。if-else 鏈、浮點誤差累積的迴圈除法。