Passed
Push — master ( ea4374...2f7c83 )
by Lv
02:12
created

Convert::formatNumber()   B

Complexity

Conditions 7
Paths 19

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 17
c 1
b 1
f 0
dl 0
loc 26
rs 8.8333
cc 7
nc 19
nop 1
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
    public function __construct()
39
    {
40
        $extensions = [];
41
        foreach (self::EXTENSION as $name) {
42
            $extensions[$name] = true === \extension_loaded($name);
43
        }
44
        $this->extensionException($extensions);
45
    }
46
47
    /**
48
     * Check php extension.
49
     *
50
     * @param array $extensions
51
     *
52
     * @throws ExtensionException
53
     */
54
    public function extensionException($extensions)
55
    {
56
        foreach ($extensions as $name => $state) {
57
            if (false === $state) {
58
                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
    public function convertNumbers($number, $character = 1, $unit = 1)
77
    {
78
        $number = $this->formatNumber($number);
79
        $this->selectDictionaries($character, $unit);
80
        if ('float' == $this->type) {
81
            $number_arr = \explode('.', $number);
82
            $integer_part = $this->convertInteger($number_arr[0]);
83
            $decimal_part = $this->convertDecimal($number_arr[1]);
84
85
            return $integer_part.'点'.$decimal_part;
86
        } elseif ('int' == $this->type) {
87
            return $this->convertInteger($number);
88
        } else {
89
            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
    public function mbSubStrReplace($string, $replacement, $start, $length = null, $encoding = null)
105
    {
106
        $string_length = (true === \is_null($encoding)) ? \mb_strlen($string) : \mb_strlen($string, $encoding);
107
        if ($start < 0) {
108
            $start = \max(0, $string_length + $start);
109
        } elseif ($start > $string_length) {
110
            $start = $string_length;
111
        }
112
        if ($length < 0) {
113
            $length = \max(0, $string_length - $start + $length);
114
        } elseif ((true === \is_null($length)) || ($length > $string_length)) {
115
            $length = $string_length;
116
        }
117
        if (($start + $length) > $string_length) {
118
            $length = $string_length - $start;
119
        }
120
        if (true === \is_null($encoding)) {
121
            return \mb_substr($string, 0, $start).$replacement.\mb_substr($string, $start + $length, $string_length - $start - $length);
122
        }
123
124
        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
    public function selectDictionaries($character, $unit)
136
    {
137
        switch ($character) {
138
            case 1:
139
                $dic = self::SIMPLE_DIC;
140
                break;
141
            case 2:
142
                $dic = self::SIMPLE_SPEC_DIC;
143
                break;
144
            case 3:
145
                $dic = self::UPPER_DIC;
146
                break;
147
            default:
148
                throw new DictionarySetException('Invalid dictionary type');
149
        }
150
        $this->dic = $dic;
151
        switch ($unit) {
152
            case 1:
153
                $unit_dic = self::SIMPLE_UNIT_DIC;
154
                break;
155
            case 2:
156
                $unit_dic = self::TRADITION_UNIT_DIC;
157
                break;
158
            default:
159
                throw new DictionarySetException('Invalid unit dictionary type');
160
        }
161
        $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
    public function formatNumber($number)
174
    {
175
        if (!\is_string($number)) {
176
            throw new InvalidArgumentException('Invalid number type, must be a string');
177
        }
178
        $number = \trim(\str_replace(' ', '', $number), ' \t\n\r');
179
        if ($number !== 0) {
180
            $number = \ltrim($number,'\0\x0B');
181
        }
182
        $pos_dot = \strpos($number, '.');
183
        if (false !== $pos_dot) {
184
            if (0 === $pos_dot) {
185
                $number = '0'.$number;
186
            }
187
            $number = \rtrim(\rtrim($number, '0'), '.');
188
        }
189
        if (0 === \preg_match('/^\d+(\.{0,1}\d+){0,1}$/', $number)) {
190
            throw new InvalidArgumentException('Invalid value number: '.$number);
191
        }
192
        if (false === \strpos($number, '.')) {
193
            $this->type = 'int';
194
        } else {
195
            $this->type = 'float';
196
        }
197
198
        return $number;
199
    }
200
201
    /**
202
     * Convert integer part.
203
     *
204
     * @param $integer
205
     *
206
     * @return string
207
     *
208
     * @throws DictionarySetException
209
     */
210
    public function convertInteger($integer)
211
    {
212
        if (empty($this->dic)) {
213
            throw new DictionarySetException('Dictionary is not set');
214
        }
215
        if (empty($this->unit_dic)) {
216
            throw new DictionarySetException('Unit dictionary is not set');
217
        }
218
        $num_arr_chunk = \array_chunk(\array_reverse(\str_split($integer)), 68);
0 ignored issues
show
Bug introduced by
It seems like str_split($integer) can also be of type true; however, parameter $array of array_reverse() does only seem to accept array, 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 ignore-type  annotation

218
        $num_arr_chunk = \array_chunk(\array_reverse(/** @scrutinizer ignore-type */ \str_split($integer)), 68);
Loading history...
219
        $complete_str = '';
220
        foreach ($num_arr_chunk as $cycle => $num_arr) {
221
            $length = \count($num_arr);
222
            $match_unit_digits = 0 == $length % 4 ? \bcdiv($length, 4) : \bcdiv($length, 4) + 1;
223
            $chunk_unit_dic = \array_slice($this->unit_dic, 0, $match_unit_digits);
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

223
            $chunk_unit_dic = \array_slice($this->unit_dic, 0, /** @scrutinizer ignore-type */ $match_unit_digits);
Loading history...
224
            if (0 == $cycle) {
225
                $chunk_unit_dic[0] = '';
226
            }
227
            for ($a = 0; $a < $match_unit_digits; ++$a) {
228
                if ($this->dic == $this::UPPER_DIC) {
229
                    \array_splice($chunk_unit_dic, $a * 4 + 1, 0, ['拾', '百', '千']);
230
                } else {
231
                    \array_splice($chunk_unit_dic, $a * 4 + 1, 0, ['十', '百', '千']);
232
                }
233
            }
234
            $chinese_num = '';
235
            foreach ($num_arr as $key => $value) {
236
                if (0 == $key % 4) {
237
                    if ($key > 4 && $chunk_unit_dic[$key - 4] == \mb_substr($chinese_num, 0, 1)) {
238
                        $chinese_num = $this->mbSubStrReplace($chinese_num, '', 0, 1);
239
                        if (\mb_substr($chinese_num, 0, 1) != $this->dic[0]) {
240
                            $chinese_num = $this->dic[0].$chinese_num;
241
                        }
242
                    }
243
                    $chinese_num = (0 == $value && 1 != $length ? '' : $this->dic[$value]).$chunk_unit_dic[$key].$chinese_num;
244
                } else {
245
                    if (0 == $value && '' == $chinese_num) {
246
                        continue;
247
                    } elseif (0 == $value && '' != $chinese_num) {
248
                        if (0 != $num_arr[$key - 1]) {
249
                            $chinese_num = $this->dic[0].$chinese_num;
250
                        }
251
                    } else {
252
                        if (1 == $value && 2 == $length % 4 && $length == $key + 1) {
253
                            $chinese_num = $chunk_unit_dic[$key].$chinese_num;
254
                        } else {
255
                            $chinese_num = $this->dic[$value].$chunk_unit_dic[$key].$chinese_num;
256
                        }
257
                    }
258
                }
259
            }
260
            $complete_str = $chinese_num.$complete_str;
261
        }
262
263
        return $complete_str;
264
    }
265
266
    /**
267
     * Convert decimal part.
268
     *
269
     * @param $decimal
270
     *
271
     * @return string
272
     *
273
     * @throws DictionarySetException
274
     */
275
    public function convertDecimal($decimal)
276
    {
277
        if (empty($this->dic)) {
278
            throw new DictionarySetException('Dictionary is not set');
279
        }
280
        $num_arr = \str_split($decimal);
281
        $converted_str = '';
282
        foreach ($num_arr as $v) {
283
            $converted_str .= $this->dic[$v];
284
        }
285
286
        return $converted_str;
287
    }
288
}
289