財務系統、電子合同、報表導出中,人民幣數值轉中文大寫是高頻剛需。本文提供三個層次的解決方案:功能完整的企業級封裝類(支持進位、尾零、繁簡體、自定義單位)、函數式精簡實現(利用映射表逆序處理)、教學級逐位解析法。重點解決小數進位、連續零合併、大單位(萬/億)零保留、「整」字補全等核心難點。
class Num2RmbClass {
/**
* 人民幣數值轉中文大寫
* @param string $number 金額字符串(避免浮點精度問題)
* @param string $int_unit 整數單位:'元'(默認) 或 '圓'
* @param bool $is_round 是否四捨五入到分
* @param bool $is_extra_zero 整數末尾為零且有小數時,是否輸出「零」(如 1960.30 → 壹仟玖佰陸拾元零三角)
* @param bool $dec_to_int 小數進位到個位(0.995 → 1 元)
* @return string 中文大寫金額
*/
public static function num2rmb(
string $number = '0',
string $int_unit = '元',
bool $is_round = true,
bool $is_extra_zero = false,
bool $dec_to_int = false
): string {
// 1. 分離整數/小數部分
$parts = explode('.', $number, 2);
$int = $parts[0] ?? '0';
$dec = $parts[1] ?? '';
// 2. 小數處理:超過 2 位時四捨五入/截斷/進位
$decLen = strlen($dec);
if ($decLen > 2) {
if ($is_round) {
$rounded = round((float)("0.$dec"), 2);
if ($rounded >= 1.0 && $dec_to_int) {
// 小數進位到整數部分
$int = (string)((int)$int + 1);
$dec = '00';
} elseif ($rounded >= 1.0) {
// 不進位,小數置為 99 分(最大)
$dec = '99';
} else {
$dec = sprintf('%02d', (int)($rounded * 100));
}
} else {
$dec = substr($dec, 0, 2);
}
} elseif ($decLen === 1) {
$dec .= '0'; // 如 .5 → .50
} elseif ($decLen === 0) {
$dec = '00';
}
// 3. 雙零快速返回
if ($int === '0' && $dec === '00') return '零' . $int_unit . '整';
// 4. 映射表
$chs = ['零','壹','貳','三','肆','伍','陸','柒','捌','玖'];
$uni = ['','拾','佰','仟']; // 段內單位
$decUni = ['角','分']; // 小數單位
$exp = ['','萬','億','兆']; // 大單位(每 4 位)
// 5. 整數部分:從右向左,每 4 位一段
$res = '';
for ($i = strlen($int) - 1, $seg = 0; $i >= 0; $seg++) {
$segStr = '';
// 處理段內 4 位
for ($j = 0; $j < 4 && $i >= 0; $j++, $i--) {
$digit = (int)$int[$i];
$unit = $digit > 0 ? $uni[$j] : '';
$segStr = $chs[$digit] . $unit . $segStr;
}
// 段後處理
$segStr = rtrim($segStr, '零'); // 去尾部零
$segStr = preg_replace('/零+/', '零', $segStr); // 合併連續零
if ($segStr !== '') {
$res = $segStr . $exp[$seg] . $res;
} elseif ($seg > 0 && $res !== '') {
// 段全為零但非最高段,需保留大單位(如 1,0001 → 壹萬零壹)
$res = $exp[$seg] . $res;
}
}
// 6. 小數部分
if ($dec !== '00') {
$res .= $int_unit;
// 可選:整數末尾為零且有小數,輸出「零」
if ($is_extra_zero && substr($int, -1) === '0') $res .= '零';
for ($i = 0; $i < 2; $i++) {
$digit = (int)$dec[$i];
$unit = $digit > 0 ? $decUni[$i] : '';
$res .= $chs[$digit] . $unit;
}
$res = rtrim($res, '零');
$res = preg_replace('/零+/', '零', $res);
} else {
$res .= $int_unit . '整';
}
return $res;
}
}
// 使用示例
echo Num2RmbClass::num2rmb('1600020039.9989', '圓', false, false, false);
// 輸出:壹拾陸億零貳萬零叁拾玖圓玖角玖分
echo Num2RmbClass::num2rmb('1960.30', '元', true, true, false);
// 輸出:壹仟玖佰陸拾元零三角整
echo Num2RmbClass::num2rmb('0.005', '元', true, false, true);
// 輸出:壹元整(小數進位)
/**
* 精簡版:逆序遍歷 + 單位映射表
* @param string $ns 數字字符串
* @return string
*/
function cny(string $ns): string {
static $cnums = ['零','壹','貳','三','肆','伍','陸','柒','捌','玖'];
static $cnyUnits = ['圓','角','分'];
static $grees = ['拾','佰','仟','萬','拾','佰','仟','億'];
// 分離整數/小數
[$ns1, $ns2] = explode('.', $ns . '.00', 2); // 確保有小數部分
$ns2 = array_filter([$ns2[1] ?? '0', $ns2[0] ?? '0']); // 角、分(逆序)
// 整數部分逆序分段處理
$intRev = strrev($ns1);
$intParts = [];
for ($i = 0; $i < strlen($intRev); $i++) {
$digit = $intRev[$i];
$unit = $digit !== '0' ? $grees[$i] : '';
$intParts[] = $digit . $unit;
}
// 合併:小數(逆序) + 整數(逆序) + 空佔位
$all = array_merge($ns2, $intParts, ['']);
// 逆序恢復正序
$all = array_reverse($all);
// 映射數字→漢字
$mapped = array_map(function($v) use ($cnums) {
return strtr($v, $cnums);
}, $all);
return implode('', $mapped);
}
// 測試
echo cny('23058.04'); // 貳萬三仟零伍拾捌圓肆分
strrev、小數 array_filter([角,分]) 統一為低位在前$grees:索引直接對應位權(0→空、1→拾、2→佰、3→仟、4→萬...)strtr 批量替換:一次性將所有數字字符替換為漢字array_filter 去除空單位:零不輸出單位,自動實現零處理/**
* 教學版:逐位處理,邏輯直觀
*/
function num_to_rmb($num) {
$c1 = "零壹貳三肆伍陸柒捌玖"; // 每字 3 字節
$c2 = "分角元拾佰仟萬拾佰仟億"; // 單位對應
$num = round($num, 2) * 100; // 轉為分
if (strlen($num) > 10) return "金額太大";
$i = 0; $c = "";
while (true) {
$n = ($i == 0) ? substr($num, -1, 1) : ($num % 10);
$p1 = substr($c1, 3 * $n, 3);
$p2 = substr($c2, 3 * $i, 3);
// 非零、或零但處於大單位位
if ($n != '0' || ($n == '0' && in_array($p2, ['億','萬','元']))) {
$c = $p1 . $p2 . $c;
} else {
$c = $p1 . $c;
}
$num = (int)($num / 10);
if ($num == 0) break;
$i++;
}
// 後處理:合併零、去尾零
$j = 0; $len = strlen($c);
while ($j < $len) {
$m = substr($c, $j, 6);
if (in_array($m, ['零元','零萬','零億','零零'])) {
$c = substr($c, 0, $j) . substr($c, $j + 3);
$j -= 3; $len -= 3;
}
$j += 3;
}
if (substr($c, -3) === '零') $c = substr($c, 0, -3);
return empty($c) ? "零元整" : $c . "整";
}
| 難點 | 方案一(類) | 方案二(函數式) | 方案三(教學) |
|---|---|---|---|
| 小數進位 | 參數 $dec_to_int 控制 | 不支持 | 不支持 |
| 尾零輸出 | 參數 $is_extra_zero 控制 | 自動處理 | 後處理正則 |
| 大單位零保留 | 段級邏輯顯式處理 | 映射表自然保留 | 條件判斷保留 |
| 連續零合併 | preg_replace('/零+/', '零') | array_filter 隱式 | 字符串替換 |
| 繁簡體切換 | 可擴展映射表 | 需修改靜態數組 | 修改 $c1 |
| 性能 | O(n) 字符串操作 | O(n) 數組操作,極快 | O(n) 字符串操作 |
| 可讀性 | 高(參數語義化) | 中(技巧性強) | 最高(邏輯直觀) |
0.58 * 100 = 57.9999 浮點陷阱InvalidArgumentException方案一(Num2RmbClass)功能最完整、參數化靈活,適合企業級財務系統;方案二(cny 函數)代碼極簡、性能極佳,適合輕量級項目;方案三(教學版)邏輯最清晰,適合理解原理、教學演示。核心難點在於零的語義處理:中間零只保留數字、大單位位零保留單位、尾部零去除、小數進位。建議根據項目複雜度選擇方案,並建立完整測試用例庫。