coolert /
number_in_chinese
| 1 | <?php |
||||
| 2 | |||||
| 3 | /* |
||||
| 4 | * This file is part of the coolert/number_in_chinese. |
||||
| 5 | * |
||||
| 6 | * (c) coolert <[email protected]> |
||||
| 7 | * |
||||
| 8 | * This source file is subject to the MIT license that is bundled |
||||
| 9 | * with this source code in the file LICENSE. |
||||
| 10 | */ |
||||
| 11 | |||||
| 12 | namespace Coolert\NumberInChinese; |
||||
| 13 | |||||
| 14 | use Coolert\NumberInChinese\Exceptions\DictionarySetException; |
||||
| 15 | use Coolert\NumberInChinese\Exceptions\ExtensionException; |
||||
| 16 | use Coolert\NumberInChinese\Exceptions\InvalidArgumentException; |
||||
| 17 | use Coolert\NumberInChinese\Exceptions\TypeSetException; |
||||
| 18 | |||||
| 19 | /** |
||||
| 20 | * Class Convert. |
||||
| 21 | */ |
||||
| 22 | class Convert |
||||
| 23 | { |
||||
| 24 | const SIMPLE_DIC = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']; |
||||
| 25 | const SIMPLE_SPEC_DIC = ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']; |
||||
| 26 | const UPPER_DIC = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '镹', '拾']; |
||||
| 27 | const SIMPLE_UNIT_DIC = ['无量大数', '万', '亿', '兆', '京', '垓', '秭', '穰', '沟', '涧', '正', '载', '极', '恒河沙', '阿僧祇', '那由他', '不可思议']; |
||||
| 28 | const TRADITION_UNIT_DIC = ['無量大數', '萬', '億', '兆', '京', '垓', '秭', '穰', '溝', '澗', '正', '載', '極', '恆河沙', '阿僧祇', '那由他', '不可思議']; |
||||
| 29 | const EXTENSION = ['bcmath', 'mbstring']; |
||||
| 30 | |||||
| 31 | public $dic; |
||||
| 32 | public $unit_dic; |
||||
| 33 | public $type; |
||||
| 34 | |||||
| 35 | /** |
||||
| 36 | * @throws ExtensionException |
||||
| 37 | */ |
||||
| 38 | 15 | public function __construct() |
|||
| 39 | { |
||||
| 40 | 15 | $extensions = []; |
|||
| 41 | 15 | foreach (self::EXTENSION as $name) { |
|||
| 42 | 15 | $extensions[$name] = true === \extension_loaded($name); |
|||
| 43 | } |
||||
| 44 | 15 | $this->extensionException($extensions); |
|||
| 45 | } |
||||
| 46 | |||||
| 47 | /** |
||||
| 48 | * Check php extension. |
||||
| 49 | * |
||||
| 50 | * @param array $extensions |
||||
| 51 | * |
||||
| 52 | * @throws ExtensionException |
||||
| 53 | */ |
||||
| 54 | 15 | public function extensionException($extensions) |
|||
| 55 | { |
||||
| 56 | 15 | foreach ($extensions as $name => $state) { |
|||
| 57 | 15 | if (false === $state) { |
|||
| 58 | 2 | throw new ExtensionException('Disabled extension '.$name); |
|||
| 59 | } |
||||
| 60 | } |
||||
| 61 | } |
||||
| 62 | |||||
| 63 | /** |
||||
| 64 | * Convert numbers into Chinese numbers. |
||||
| 65 | * |
||||
| 66 | * @param int $number |
||||
| 67 | * @param int $character |
||||
| 68 | * @param int $unit |
||||
| 69 | * |
||||
| 70 | * @return string |
||||
| 71 | * |
||||
| 72 | * @throws InvalidArgumentException |
||||
| 73 | * @throws DictionarySetException |
||||
| 74 | * @throws TypeSetException |
||||
| 75 | */ |
||||
| 76 | 2 | public function convertNumbers($number, $character = 1, $unit = 1) |
|||
| 77 | { |
||||
| 78 | 2 | $number = $this->formatNumber($number); |
|||
| 79 | 2 | $this->selectDictionaries($character, $unit); |
|||
| 80 | 2 | if ('float' == $this->type) { |
|||
| 81 | 1 | $number_arr = \explode('.', $number); |
|||
| 82 | 1 | $integer_part = $this->convertInteger($number_arr[0]); |
|||
| 83 | 1 | $decimal_part = $this->convertDecimal($number_arr[1]); |
|||
| 84 | |||||
| 85 | 1 | return $integer_part.'点'.$decimal_part; |
|||
| 86 | 2 | } elseif ('int' == $this->type) { |
|||
| 87 | 1 | return $this->convertInteger($number); |
|||
| 88 | } else { |
||||
| 89 | 1 | throw new TypeSetException('Invalid type set'); |
|||
| 90 | } |
||||
| 91 | } |
||||
| 92 | |||||
| 93 | /** |
||||
| 94 | * Replace Chinese characters string. |
||||
| 95 | * |
||||
| 96 | * @param string $string |
||||
| 97 | * @param string $replacement |
||||
| 98 | * @param int $start |
||||
| 99 | * @param int $length |
||||
| 100 | * @param string $encoding |
||||
| 101 | * |
||||
| 102 | * @return string |
||||
| 103 | */ |
||||
| 104 | 2 | public function mbSubStrReplace($string, $replacement, $start, $length = null, $encoding = null) |
|||
| 105 | { |
||||
| 106 | 2 | $string_length = (true === \is_null($encoding)) ? \mb_strlen($string) : \mb_strlen($string, $encoding); |
|||
| 107 | 2 | if ($start < 0) { |
|||
| 108 | 1 | $start = \max(0, $string_length + $start); |
|||
| 109 | 2 | } elseif ($start > $string_length) { |
|||
| 110 | 1 | $start = $string_length; |
|||
| 111 | } |
||||
| 112 | 2 | if ($length < 0) { |
|||
| 113 | 1 | $length = \max(0, $string_length - $start + $length); |
|||
| 114 | 2 | } elseif ((true === \is_null($length)) || ($length > $string_length)) { |
|||
| 115 | 1 | $length = $string_length; |
|||
| 116 | } |
||||
| 117 | 2 | if (($start + $length) > $string_length) { |
|||
| 118 | 1 | $length = $string_length - $start; |
|||
| 119 | } |
||||
| 120 | 2 | if (true === \is_null($encoding)) { |
|||
| 121 | 2 | return \mb_substr($string, 0, $start).$replacement.\mb_substr($string, $start + $length, $string_length - $start - $length); |
|||
| 122 | } |
||||
| 123 | |||||
| 124 | 1 | return \mb_substr($string, 0, $start, $encoding).$replacement.\mb_substr($string, $start + $length, $string_length - $start - $length, $encoding); |
|||
| 125 | } |
||||
| 126 | |||||
| 127 | /** |
||||
| 128 | * Select character dictionary and unit dictionary. |
||||
| 129 | * |
||||
| 130 | * @param int $character |
||||
| 131 | * @param int $unit |
||||
| 132 | * |
||||
| 133 | * @throws DictionarySetException |
||||
| 134 | */ |
||||
| 135 | 5 | public function selectDictionaries($character, $unit) |
|||
| 136 | { |
||||
| 137 | switch ($character) { |
||||
| 138 | 5 | case 1: |
|||
| 139 | 4 | $dic = self::SIMPLE_DIC; |
|||
| 140 | 4 | break; |
|||
| 141 | 2 | case 2: |
|||
| 142 | 1 | $dic = self::SIMPLE_SPEC_DIC; |
|||
| 143 | 1 | break; |
|||
| 144 | 2 | case 3: |
|||
| 145 | 1 | $dic = self::UPPER_DIC; |
|||
| 146 | 1 | break; |
|||
| 147 | default: |
||||
| 148 | 1 | throw new DictionarySetException('Invalid dictionary type'); |
|||
| 149 | } |
||||
| 150 | 4 | $this->dic = $dic; |
|||
| 151 | switch ($unit) { |
||||
| 152 | 4 | case 1: |
|||
| 153 | 3 | $unit_dic = self::SIMPLE_UNIT_DIC; |
|||
| 154 | 3 | break; |
|||
| 155 | 2 | case 2: |
|||
| 156 | 1 | $unit_dic = self::TRADITION_UNIT_DIC; |
|||
| 157 | 1 | break; |
|||
| 158 | default: |
||||
| 159 | 1 | throw new DictionarySetException('Invalid unit dictionary type'); |
|||
| 160 | } |
||||
| 161 | 3 | $this->unit_dic = $unit_dic; |
|||
| 162 | } |
||||
| 163 | |||||
| 164 | /** |
||||
| 165 | * Check input data and format data into usable numbers. |
||||
| 166 | * |
||||
| 167 | * @param $number |
||||
| 168 | * |
||||
| 169 | * @return string |
||||
| 170 | * |
||||
| 171 | * @throws InvalidArgumentException |
||||
| 172 | */ |
||||
| 173 | 4 | public function formatNumber($number) |
|||
| 174 | { |
||||
| 175 | 4 | if (!\is_string($number)) { |
|||
| 176 | 1 | throw new InvalidArgumentException('Invalid number type, must be a string'); |
|||
| 177 | } |
||||
| 178 | 3 | $number = \trim(\str_replace(' ', '', $number), ' \t\n\r'); |
|||
| 179 | 3 | if (0 !== $number) { |
|||
| 180 | 3 | $number = \ltrim($number, '\0\x0B'); |
|||
| 181 | } |
||||
| 182 | 3 | $pos_dot = \strpos($number, '.'); |
|||
| 183 | 3 | if (false !== $pos_dot) { |
|||
| 184 | 2 | if (0 === $pos_dot) { |
|||
| 185 | 1 | $number = '0'.$number; |
|||
| 186 | } |
||||
| 187 | 2 | $number = \rtrim(\rtrim($number, '0'), '.'); |
|||
| 188 | } |
||||
| 189 | 3 | if (0 === \preg_match('/^\d+(\.{0,1}\d+){0,1}$/', $number)) { |
|||
| 190 | 1 | throw new InvalidArgumentException('Invalid value number: '.$number); |
|||
| 191 | } |
||||
| 192 | 2 | if (false === \strpos($number, '.')) { |
|||
| 193 | 2 | $this->type = 'int'; |
|||
| 194 | } else { |
||||
| 195 | 2 | $this->type = 'float'; |
|||
| 196 | } |
||||
| 197 | |||||
| 198 | 2 | return $number; |
|||
| 199 | } |
||||
| 200 | |||||
| 201 | /** |
||||
| 202 | * Convert integer part. |
||||
| 203 | * |
||||
| 204 | * @param $integer |
||||
| 205 | * |
||||
| 206 | * @return string |
||||
| 207 | * |
||||
| 208 | * @throws DictionarySetException |
||||
| 209 | */ |
||||
| 210 | 4 | public function convertInteger($integer) |
|||
| 211 | { |
||||
| 212 | 4 | if (empty($this->dic)) { |
|||
| 213 | 1 | throw new DictionarySetException('Dictionary is not set'); |
|||
| 214 | } |
||||
| 215 | 3 | if (empty($this->unit_dic)) { |
|||
| 216 | 1 | throw new DictionarySetException('Unit dictionary is not set'); |
|||
| 217 | } |
||||
| 218 | 2 | $num_arr_chunk = \array_chunk(\array_reverse(\str_split($integer)), 68); |
|||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
| 219 | 2 | $complete_str = ''; |
|||
| 220 | 2 | foreach ($num_arr_chunk as $cycle => $num_arr) { |
|||
| 221 | 2 | $length = \count($num_arr); |
|||
| 222 | 2 | $match_unit_digits = 0 == $length % 4 ? \bcdiv($length, 4) : \bcdiv($length, 4) + 1; |
|||
| 223 | 2 | $chunk_unit_dic = \array_slice($this->unit_dic, 0, $match_unit_digits); |
|||
|
0 ignored issues
–
show
It seems like
$match_unit_digits can also be of type string; however, parameter $length of array_slice() does only seem to accept integer|null, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 224 | 2 | if (0 == $cycle) { |
|||
| 225 | 2 | $chunk_unit_dic[0] = ''; |
|||
| 226 | } |
||||
| 227 | 2 | for ($a = 0; $a < $match_unit_digits; ++$a) { |
|||
| 228 | 2 | if ($this->dic == $this::UPPER_DIC) { |
|||
| 229 | 1 | \array_splice($chunk_unit_dic, $a * 4 + 1, 0, ['拾', '百', '千']); |
|||
| 230 | } else { |
||||
| 231 | 2 | \array_splice($chunk_unit_dic, $a * 4 + 1, 0, ['十', '百', '千']); |
|||
| 232 | } |
||||
| 233 | } |
||||
| 234 | 2 | $chinese_num = ''; |
|||
| 235 | 2 | foreach ($num_arr as $key => $value) { |
|||
| 236 | 2 | if (0 == $key % 4) { |
|||
| 237 | 2 | if ($key > 4 && $chunk_unit_dic[$key - 4] == \mb_substr($chinese_num, 0, 1)) { |
|||
| 238 | 1 | $chinese_num = $this->mbSubStrReplace($chinese_num, '', 0, 1); |
|||
| 239 | 1 | if (\mb_substr($chinese_num, 0, 1) != $this->dic[0]) { |
|||
| 240 | 1 | $chinese_num = $this->dic[0].$chinese_num; |
|||
| 241 | } |
||||
| 242 | } |
||||
| 243 | 2 | $chinese_num = (0 == $value && 1 != $length ? '' : $this->dic[$value]).$chunk_unit_dic[$key].$chinese_num; |
|||
| 244 | } else { |
||||
| 245 | 2 | if (0 == $value && '' == $chinese_num) { |
|||
| 246 | 1 | continue; |
|||
| 247 | 2 | } elseif (0 == $value && '' != $chinese_num) { |
|||
| 248 | 1 | if (0 != $num_arr[$key - 1]) { |
|||
| 249 | 1 | $chinese_num = $this->dic[0].$chinese_num; |
|||
| 250 | } |
||||
| 251 | } else { |
||||
| 252 | 2 | if (1 == $value && 2 == $length % 4 && $length == $key + 1) { |
|||
| 253 | 1 | $chinese_num = $chunk_unit_dic[$key].$chinese_num; |
|||
| 254 | } else { |
||||
| 255 | 2 | $chinese_num = $this->dic[$value].$chunk_unit_dic[$key].$chinese_num; |
|||
| 256 | } |
||||
| 257 | } |
||||
| 258 | } |
||||
| 259 | } |
||||
| 260 | 2 | $complete_str = $chinese_num.$complete_str; |
|||
| 261 | } |
||||
| 262 | |||||
| 263 | 2 | return $complete_str; |
|||
| 264 | } |
||||
| 265 | |||||
| 266 | /** |
||||
| 267 | * Convert decimal part. |
||||
| 268 | * |
||||
| 269 | * @param $decimal |
||||
| 270 | * |
||||
| 271 | * @return string |
||||
| 272 | * |
||||
| 273 | * @throws DictionarySetException |
||||
| 274 | */ |
||||
| 275 | 3 | public function convertDecimal($decimal) |
|||
| 276 | { |
||||
| 277 | 3 | if (empty($this->dic)) { |
|||
| 278 | 1 | throw new DictionarySetException('Dictionary is not set'); |
|||
| 279 | } |
||||
| 280 | 2 | $num_arr = \str_split($decimal); |
|||
| 281 | 2 | $converted_str = ''; |
|||
| 282 | 2 | foreach ($num_arr as $v) { |
|||
| 283 | 2 | $converted_str .= $this->dic[$v]; |
|||
| 284 | } |
||||
| 285 | |||||
| 286 | 2 | return $converted_str; |
|||
| 287 | } |
||||
| 288 | } |
||||
| 289 |