|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace DataValues; |
|
4
|
|
|
|
|
5
|
|
|
use InvalidArgumentException; |
|
6
|
|
|
|
|
7
|
|
|
/** |
|
8
|
|
|
* Class for performing basic arithmetic and other transformations |
|
9
|
|
|
* on DecimalValues. |
|
10
|
|
|
* |
|
11
|
|
|
* This uses the bcmath library if available. Otherwise, it falls back on |
|
12
|
|
|
* using floating point operations. |
|
13
|
|
|
* |
|
14
|
|
|
* @note: this is not a genuine decimal arithmetics implementation, |
|
15
|
|
|
* and should not be used for financial computations, physical simulations, etc. |
|
16
|
|
|
* |
|
17
|
|
|
* @see DecimalValue |
|
18
|
|
|
* |
|
19
|
|
|
* @since 0.1 |
|
20
|
|
|
* |
|
21
|
|
|
* @license GPL-2.0+ |
|
22
|
|
|
* @author Daniel Kinzler |
|
23
|
|
|
*/ |
|
24
|
|
|
class DecimalMath { |
|
25
|
|
|
|
|
26
|
|
|
/** |
|
27
|
|
|
* Whether to use the bcmath library. |
|
28
|
|
|
* |
|
29
|
|
|
* @var bool |
|
30
|
|
|
*/ |
|
31
|
|
|
private $useBC; |
|
32
|
|
|
|
|
33
|
|
|
/** |
|
34
|
|
|
* @param bool|null $useBC Whether to use the bcmath library. If null, |
|
35
|
|
|
* bcmath will automatically be used if available. |
|
36
|
|
|
*/ |
|
37
|
|
|
public function __construct( $useBC = null ) { |
|
38
|
|
|
if ( $useBC === null ) { |
|
39
|
|
|
$useBC = function_exists( 'bcscale' ); |
|
40
|
|
|
} |
|
41
|
|
|
|
|
42
|
|
|
$this->useBC = $useBC; |
|
43
|
|
|
} |
|
44
|
|
|
|
|
45
|
|
|
/** |
|
46
|
|
|
* Whether this is using the bcmath library. |
|
47
|
|
|
* |
|
48
|
|
|
* @return bool |
|
49
|
|
|
*/ |
|
50
|
|
|
public function getUseBC() { |
|
51
|
|
|
return $this->useBC; |
|
52
|
|
|
} |
|
53
|
|
|
|
|
54
|
|
|
/** |
|
55
|
|
|
* Returns the product of the two values. |
|
56
|
|
|
* |
|
57
|
|
|
* @param DecimalValue $a |
|
58
|
|
|
* @param DecimalValue $b |
|
59
|
|
|
* |
|
60
|
|
|
* @return DecimalValue |
|
61
|
|
|
*/ |
|
62
|
|
|
public function product( DecimalValue $a, DecimalValue $b ) { |
|
63
|
|
|
if ( $this->useBC ) { |
|
64
|
|
|
$scale = strlen( $a->getFractionalPart() ) + strlen( $b->getFractionalPart() ); |
|
65
|
|
|
$product = bcmul( $a->getValue(), $b->getValue(), $scale ); |
|
66
|
|
|
} else { |
|
67
|
|
|
$product = $a->getValueFloat() * $b->getValueFloat(); |
|
68
|
|
|
} |
|
69
|
|
|
|
|
70
|
|
|
return DecimalValue::makeDecimalValue( $product ); |
|
71
|
|
|
} |
|
72
|
|
|
|
|
73
|
|
|
/** |
|
74
|
|
|
* Returns the sum of the two values. |
|
75
|
|
|
* |
|
76
|
|
|
* @param DecimalValue $a |
|
77
|
|
|
* @param DecimalValue $b |
|
78
|
|
|
* |
|
79
|
|
|
* @return DecimalValue |
|
80
|
|
|
*/ |
|
81
|
|
|
public function sum( DecimalValue $a, DecimalValue $b ) { |
|
82
|
|
|
if ( $this->useBC ) { |
|
83
|
|
|
$scale = max( strlen( $a->getFractionalPart() ), strlen( $b->getFractionalPart() ) ); |
|
84
|
|
|
$sum = bcadd( $a->getValue(), $b->getValue(), $scale ); |
|
85
|
|
|
} else { |
|
86
|
|
|
$sum = $a->getValueFloat() + $b->getValueFloat(); |
|
87
|
|
|
} |
|
88
|
|
|
|
|
89
|
|
|
return DecimalValue::makeDecimalValue( $sum ); |
|
90
|
|
|
} |
|
91
|
|
|
|
|
92
|
|
|
/** |
|
93
|
|
|
* Returns the minimum of the two values |
|
94
|
|
|
* |
|
95
|
|
|
* @param DecimalValue $a |
|
96
|
|
|
* @param DecimalValue $b |
|
97
|
|
|
* |
|
98
|
|
|
* @return DecimalValue |
|
99
|
|
|
*/ |
|
100
|
|
|
public function min( DecimalValue $a, DecimalValue $b ) { |
|
101
|
|
|
|
|
102
|
|
|
if ( $this->useBC ) { |
|
103
|
|
|
$scale = max( strlen( $a->getFractionalPart() ), strlen( $b->getFractionalPart() ) ); |
|
104
|
|
|
$comp = bccomp( $a->getValue(), $b->getValue(), $scale ); |
|
105
|
|
|
$min = $comp > 0 ? $b : $a; |
|
106
|
|
|
} else { |
|
107
|
|
|
$min = min( $a->getValueFloat(), $b->getValueFloat() ); |
|
108
|
|
|
$min = DecimalValue::makeDecimalValue( $min ); |
|
109
|
|
|
} |
|
110
|
|
|
|
|
111
|
|
|
return $min; |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
/** |
|
115
|
|
|
* Returns the maximum of the two values |
|
116
|
|
|
* |
|
117
|
|
|
* @param DecimalValue $a |
|
118
|
|
|
* @param DecimalValue $b |
|
119
|
|
|
* |
|
120
|
|
|
* @return DecimalValue |
|
121
|
|
|
*/ |
|
122
|
|
|
public function max( DecimalValue $a, DecimalValue $b ) { |
|
123
|
|
|
|
|
124
|
|
|
if ( $this->useBC ) { |
|
125
|
|
|
$scale = max( strlen( $a->getFractionalPart() ), strlen( $b->getFractionalPart() ) ); |
|
126
|
|
|
$comp = bccomp( $a->getValue(), $b->getValue(), $scale ); |
|
127
|
|
|
$max = $comp > 0 ? $a : $b; |
|
128
|
|
|
} else { |
|
129
|
|
|
$max = max( $a->getValueFloat(), $b->getValueFloat() ); |
|
130
|
|
|
$max = DecimalValue::makeDecimalValue( $max ); |
|
131
|
|
|
} |
|
132
|
|
|
|
|
133
|
|
|
return $max; |
|
134
|
|
|
} |
|
135
|
|
|
|
|
136
|
|
|
/** |
|
137
|
|
|
* Returns the given value, with any insignificant digits removed or zeroed. |
|
138
|
|
|
* |
|
139
|
|
|
* Rounding is applied using the "round half away from zero" rule (that is, +0.5 is |
|
140
|
|
|
* rounded to +1 and -0.5 is rounded to -1). |
|
141
|
|
|
* |
|
142
|
|
|
* @since 0.1 |
|
143
|
|
|
* |
|
144
|
|
|
* @param DecimalValue $decimal |
|
145
|
|
|
* @param int $significantDigits The number of digits to retain, counting the decimal point, |
|
146
|
|
|
* but not counting the leading sign. |
|
147
|
|
|
* |
|
148
|
|
|
* @throws InvalidArgumentException |
|
149
|
|
|
* @return DecimalValue |
|
150
|
|
|
*/ |
|
151
|
|
|
public function roundToDigit( DecimalValue $decimal, $significantDigits ) { |
|
152
|
|
|
$value = $decimal->getValue(); |
|
153
|
|
|
$rounded = $this->roundDigits( $value, $significantDigits ); |
|
154
|
|
|
return new DecimalValue( $rounded ); |
|
155
|
|
|
} |
|
156
|
|
|
|
|
157
|
|
|
/** |
|
158
|
|
|
* Returns the given value, with any insignificant digits removed or zeroed. |
|
159
|
|
|
* |
|
160
|
|
|
* Rounding is applied using the "round half away from zero" rule (that is, +0.5 is |
|
161
|
|
|
* rounded to +1 and -0.5 is rounded to -1). |
|
162
|
|
|
* |
|
163
|
|
|
* @since 0.1 |
|
164
|
|
|
* |
|
165
|
|
|
* @param DecimalValue $decimal |
|
166
|
|
|
* @param int $significantExponent The exponent of the last significant digit, |
|
167
|
|
|
* e.g. -1 for "keep the first digit after the decimal point", or 2 for |
|
168
|
|
|
* "zero the last two digits before the decimal point". |
|
169
|
|
|
* |
|
170
|
|
|
* @throws InvalidArgumentException |
|
171
|
|
|
* @return DecimalValue |
|
172
|
|
|
*/ |
|
173
|
|
|
public function roundToExponent( DecimalValue $decimal, $significantExponent ) { |
|
174
|
|
|
//NOTE: the number of digits to keep (without the leading sign) |
|
175
|
|
|
// is the same as the exponent's offset (with the leaqding sign). |
|
176
|
|
|
$digits = $this->getPositionForExponent( $significantExponent, $decimal ); |
|
177
|
|
|
return $this->roundToDigit( $decimal, $digits ); |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
|
|
/** |
|
181
|
|
|
* Returns the (zero based) position for the given exponent in |
|
182
|
|
|
* the given decimal string, counting the decimal point and the leading sign. |
|
183
|
|
|
* |
|
184
|
|
|
* @example: the position of exponent 0 in "+10.03" is 2. |
|
185
|
|
|
* @example: the position of exponent 1 in "+210.03" is 2. |
|
186
|
|
|
* @example: the position of exponent -2 in "+1.037" is 4. |
|
187
|
|
|
* |
|
188
|
|
|
* @param int $exponent |
|
189
|
|
|
* @param DecimalValue $decimal |
|
190
|
|
|
* |
|
191
|
|
|
* @return int |
|
192
|
|
|
*/ |
|
193
|
|
|
public function getPositionForExponent( $exponent, DecimalValue $decimal ) { |
|
194
|
|
|
$decimal = $decimal->getValue(); |
|
195
|
|
|
|
|
196
|
|
|
$pointPos = strpos( $decimal, '.' ); |
|
197
|
|
|
if ( $pointPos === false ) { |
|
198
|
|
|
$pointPos = strlen( $decimal ); |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
|
|
// account for leading sign |
|
202
|
|
|
$pointPos--; |
|
203
|
|
|
|
|
204
|
|
|
if ( $exponent < 0 ) { |
|
205
|
|
|
// account for decimal point |
|
206
|
|
|
$position = $pointPos +1 - $exponent; |
|
207
|
|
|
} else { |
|
208
|
|
|
// make sure we don't remove more digits than are there |
|
209
|
|
|
$position = max( 0, $pointPos - $exponent ); |
|
210
|
|
|
} |
|
211
|
|
|
|
|
212
|
|
|
return $position; |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
/** |
|
216
|
|
|
* Returns the given value, with any insignificant digits removed or zeroed. |
|
217
|
|
|
* |
|
218
|
|
|
* Rounding is applied using the "round half away from zero" rule (that is, +0.5 is |
|
219
|
|
|
* rounded to +1 and -0.5 is rounded to -1). |
|
220
|
|
|
* |
|
221
|
|
|
* @see round() |
|
222
|
|
|
* |
|
223
|
|
|
* @param string $value |
|
224
|
|
|
* @param int $significantDigits |
|
225
|
|
|
* |
|
226
|
|
|
* @throws InvalidArgumentException if $significantDigits is smaller than 0 |
|
227
|
|
|
* @return string |
|
228
|
|
|
*/ |
|
229
|
|
|
private function roundDigits( $value, $significantDigits ) { |
|
230
|
|
|
if ( !is_int( $significantDigits ) ) { |
|
231
|
|
|
throw new InvalidArgumentException( '$significantDigits must be an integer' ); |
|
232
|
|
|
} |
|
233
|
|
|
|
|
234
|
|
|
// keeping no digits results in zero. |
|
235
|
|
|
if ( $significantDigits === 0 ) { |
|
236
|
|
|
return '+0'; |
|
237
|
|
|
} |
|
238
|
|
|
|
|
239
|
|
|
if ( $significantDigits < 0 ) { |
|
240
|
|
|
throw new InvalidArgumentException( '$significantDigits must be larger than zero.' ); |
|
241
|
|
|
} |
|
242
|
|
|
|
|
243
|
|
|
// whether the last character is already part of the integer part of the decimal value |
|
244
|
|
|
$inIntPart = ( strpos( $value, '.' ) === false ); |
|
245
|
|
|
|
|
246
|
|
|
$rounded = ''; |
|
247
|
|
|
|
|
248
|
|
|
// Iterate over characters from right to left and build the result back to front. |
|
249
|
|
|
for ( $i = strlen( $value ) -1; $i > 0 && $i > $significantDigits; $i-- ) { |
|
250
|
|
|
|
|
251
|
|
|
list( $value, $i, $inIntPart, $next ) = $this->roundNextDigit( $value, $i, $inIntPart ); |
|
252
|
|
|
|
|
253
|
|
|
$rounded = $next . $rounded; |
|
254
|
|
|
} |
|
255
|
|
|
|
|
256
|
|
|
// just keep the remainder of the value as is (this includes the sign) |
|
257
|
|
|
$rounded = substr( $value, 0, $i +1 ) . $rounded; |
|
258
|
|
|
|
|
259
|
|
|
if ( strlen( $rounded ) < $significantDigits + 1 ) { |
|
260
|
|
|
if ( $inIntPart ) { |
|
261
|
|
|
$rounded .= '.'; |
|
262
|
|
|
} |
|
263
|
|
|
|
|
264
|
|
|
$rounded = str_pad( $rounded, $significantDigits+1, '0', STR_PAD_RIGHT ); |
|
265
|
|
|
} |
|
266
|
|
|
|
|
267
|
|
|
// strip trailing decimal point |
|
268
|
|
|
$rounded = rtrim( $rounded, '.' ); |
|
269
|
|
|
|
|
270
|
|
|
return $rounded; |
|
271
|
|
|
} |
|
272
|
|
|
|
|
273
|
|
|
/** |
|
274
|
|
|
* Extracts the next character to add to the result of a rounding run: |
|
275
|
|
|
* $value[$] will be examined and processed in order to determine the next |
|
276
|
|
|
* character to prepend to the result (returned in the $nextCharacter field). |
|
277
|
|
|
* |
|
278
|
|
|
* Updated values for the parameters are returned as well as the next |
|
279
|
|
|
* character. |
|
280
|
|
|
* |
|
281
|
|
|
* @param string $value |
|
282
|
|
|
* @param int $i |
|
283
|
|
|
* @param bool $inIntPart |
|
284
|
|
|
* |
|
285
|
|
|
* @return array ( $value, $i, $inIntPart, $nextCharacter ) |
|
286
|
|
|
*/ |
|
287
|
|
|
private function roundNextDigit( $value, $i, $inIntPart ) { |
|
288
|
|
|
// next digit |
|
289
|
|
|
$ch = $value[$i]; |
|
290
|
|
|
|
|
291
|
|
|
if ( $ch === '.' ) { |
|
292
|
|
|
// just transition from the fractional to the integer part |
|
293
|
|
|
$inIntPart = true; |
|
294
|
|
|
$nextCharacter = '.'; |
|
295
|
|
|
} else { |
|
296
|
|
|
if ( $inIntPart ) { |
|
297
|
|
|
// in the integer part, zero out insignificant digits |
|
298
|
|
|
$nextCharacter = '0'; |
|
299
|
|
|
} else { |
|
300
|
|
|
// in the fractional part, strip insignificant digits |
|
301
|
|
|
$nextCharacter = ''; |
|
302
|
|
|
} |
|
303
|
|
|
|
|
304
|
|
|
if ( ord( $ch ) >= ord( '5' ) ) { |
|
305
|
|
|
// when stripping a character >= 5, bump up the next digit to the left. |
|
306
|
|
|
list( $value, $i, $inIntPart ) = $this->bumpDigitsForRounding( $value, $i, $inIntPart ); |
|
307
|
|
|
} |
|
308
|
|
|
} |
|
309
|
|
|
|
|
310
|
|
|
return array( $value, $i, $inIntPart, $nextCharacter ); |
|
311
|
|
|
} |
|
312
|
|
|
|
|
313
|
|
|
/** |
|
314
|
|
|
* Bumps the last digit of a value that is being processed for rounding while taking |
|
315
|
|
|
* care of edge cases and updating the state of the rounding process. |
|
316
|
|
|
* |
|
317
|
|
|
* - $value is truncated to $i digits, so we can safely increment (bump) the last digit. |
|
318
|
|
|
* - if the last character of $value is '.', it's trimmed (and $inIntPart is set to true) |
|
319
|
|
|
* to handle the transition from the fractional to the integer part of $value. |
|
320
|
|
|
* - the last digit of $value is bumped using bumpDigits() - this is where the magic happens. |
|
321
|
|
|
* - $i is set to strln( $value ) to make the index consistent in case a trailing decimal |
|
322
|
|
|
* point got removed. |
|
323
|
|
|
* |
|
324
|
|
|
* Updated values for the parameters are returned. |
|
325
|
|
|
* Note: when returning, $i is always one greater than the greatest valid index in $value. |
|
326
|
|
|
* |
|
327
|
|
|
* @param string $value |
|
328
|
|
|
* @param int $i |
|
329
|
|
|
* @param bool $inIntPart |
|
330
|
|
|
* |
|
331
|
|
|
* @return array ( $value, $i, $inIntPart, $next ) |
|
332
|
|
|
*/ |
|
333
|
|
|
private function bumpDigitsForRounding( $value, $i, $inIntPart ) { |
|
334
|
|
|
$remaining = substr( $value, 0, $i ); |
|
335
|
|
|
|
|
336
|
|
|
// If there's a '.' at the end, strip it and note that we are in the |
|
337
|
|
|
// integer part of $value now. |
|
338
|
|
|
if ( $remaining[ strlen( $remaining ) -1 ] === '.' ) { |
|
339
|
|
|
$remaining = rtrim( $remaining, '.' ); |
|
340
|
|
|
$inIntPart = true; |
|
341
|
|
|
} |
|
342
|
|
|
|
|
343
|
|
|
// Rounding may add digits, adjust $i for that. |
|
344
|
|
|
$value = $this->bumpDigits( $remaining ); |
|
345
|
|
|
$i = strlen( $value ); |
|
346
|
|
|
|
|
347
|
|
|
return array( $value, $i, $inIntPart ); |
|
348
|
|
|
} |
|
349
|
|
|
|
|
350
|
|
|
/** |
|
351
|
|
|
* Increment the least significant digit by one if it is less than 9, and |
|
352
|
|
|
* set it to zero and continue to the next more significant digit if it is 9. |
|
353
|
|
|
* Exception: bump( 0 ) == 1; |
|
354
|
|
|
* |
|
355
|
|
|
* E.g.: bump( 0.2 ) == 0.3, bump( -0.09 ) == -0.10, bump( 9.99 ) == 10.00 |
|
356
|
|
|
* |
|
357
|
|
|
* This is the inverse of @see slump() |
|
358
|
|
|
* |
|
359
|
|
|
* @since 0.1 |
|
360
|
|
|
* |
|
361
|
|
|
* @param DecimalValue $decimal |
|
362
|
|
|
* |
|
363
|
|
|
* @return DecimalValue |
|
364
|
|
|
*/ |
|
365
|
|
|
public function bump( DecimalValue $decimal ) { |
|
366
|
|
|
$value = $decimal->getValue(); |
|
367
|
|
|
$bumped = $this->bumpDigits( $value ); |
|
368
|
|
|
return new DecimalValue( $bumped ); |
|
369
|
|
|
} |
|
370
|
|
|
|
|
371
|
|
|
/** |
|
372
|
|
|
* Increment the least significant digit by one if it is less than 9, and |
|
373
|
|
|
* set it to zero and continue to the next more significant digit if it is 9. |
|
374
|
|
|
* |
|
375
|
|
|
* @see bump() |
|
376
|
|
|
* |
|
377
|
|
|
* @param string $value |
|
378
|
|
|
* @return string |
|
379
|
|
|
*/ |
|
380
|
|
|
private function bumpDigits( $value ) { |
|
381
|
|
|
if ( $value === '+0' ) { |
|
382
|
|
|
return '+1'; |
|
383
|
|
|
} |
|
384
|
|
|
|
|
385
|
|
|
$bumped = ''; |
|
386
|
|
|
|
|
387
|
|
|
for ( $i = strlen( $value ) -1; $i >= 0; $i-- ) { |
|
388
|
|
|
$ch = $value[$i]; |
|
389
|
|
|
|
|
390
|
|
|
if ( $ch === '.' ) { |
|
391
|
|
|
$bumped = '.' . $bumped; |
|
392
|
|
|
continue; |
|
393
|
|
|
} elseif ( $ch === '9' ) { |
|
394
|
|
|
$bumped = '0' . $bumped; |
|
395
|
|
|
continue; |
|
396
|
|
|
} elseif ( $ch === '+' || $ch === '-' ) { |
|
397
|
|
|
$bumped = $ch . '1' . $bumped; |
|
398
|
|
|
break; |
|
399
|
|
|
} else { |
|
400
|
|
|
$bumped = chr( ord( $ch ) + 1 ) . $bumped; |
|
401
|
|
|
break; |
|
402
|
|
|
} |
|
403
|
|
|
} |
|
404
|
|
|
|
|
405
|
|
|
$bumped = substr( $value, 0, $i ) . $bumped; |
|
406
|
|
|
return $bumped; |
|
407
|
|
|
} |
|
408
|
|
|
|
|
409
|
|
|
/** |
|
410
|
|
|
* Decrement the least significant digit by one if it is more than 0, and |
|
411
|
|
|
* set it to 9 and continue to the next more significant digit if it is 0. |
|
412
|
|
|
* Exception: slump( 0 ) == -1; |
|
413
|
|
|
* |
|
414
|
|
|
* E.g.: slump( 0.2 ) == 0.1, slump( -0.10 ) == -0.01, slump( 0.0 ) == -1.0 |
|
415
|
|
|
* |
|
416
|
|
|
* This is the inverse of @see bump() |
|
417
|
|
|
* |
|
418
|
|
|
* @since 0.1 |
|
419
|
|
|
* |
|
420
|
|
|
* @param DecimalValue $decimal |
|
421
|
|
|
* |
|
422
|
|
|
* @return DecimalValue |
|
423
|
|
|
*/ |
|
424
|
|
|
public function slump( DecimalValue $decimal ) { |
|
425
|
|
|
$value = $decimal->getValue(); |
|
426
|
|
|
$slumped = $this->slumpDigits( $value ); |
|
427
|
|
|
return new DecimalValue( $slumped ); |
|
428
|
|
|
} |
|
429
|
|
|
|
|
430
|
|
|
/** |
|
431
|
|
|
* Decrement the least significant digit by one if it is more than 0, and |
|
432
|
|
|
* set it to 9 and continue to the next more significant digit if it is 0. |
|
433
|
|
|
* |
|
434
|
|
|
* @see slump() |
|
435
|
|
|
* |
|
436
|
|
|
* @param string $value |
|
437
|
|
|
* @return string |
|
438
|
|
|
*/ |
|
439
|
|
|
private function slumpDigits( $value ) { |
|
440
|
|
|
if ( $value === '+0' ) { |
|
441
|
|
|
return '-1'; |
|
442
|
|
|
} |
|
443
|
|
|
|
|
444
|
|
|
// a "precise zero" will become negative |
|
445
|
|
|
if ( preg_match( '/^\+0\.(0*)0$/', $value, $m ) ) { |
|
446
|
|
|
return '-0.' . $m[1] . '1'; |
|
447
|
|
|
} |
|
448
|
|
|
|
|
449
|
|
|
$slumped = ''; |
|
450
|
|
|
|
|
451
|
|
|
for ( $i = strlen( $value ) -1; $i >= 0; $i-- ) { |
|
452
|
|
|
$ch = substr( $value, $i, 1 ); |
|
453
|
|
|
|
|
454
|
|
|
if ( $ch === '.' ) { |
|
455
|
|
|
$slumped = '.' . $slumped; |
|
456
|
|
|
continue; |
|
457
|
|
|
} elseif ( $ch === '0' ) { |
|
458
|
|
|
$slumped = '9' . $slumped; |
|
459
|
|
|
continue; |
|
460
|
|
|
} elseif ( $ch === '+' || $ch === '-' ) { |
|
461
|
|
|
$slumped = '0'; |
|
462
|
|
|
break; |
|
463
|
|
|
} else { |
|
464
|
|
|
$slumped = chr( ord( $ch ) - 1 ) . $slumped; |
|
465
|
|
|
break; |
|
466
|
|
|
} |
|
467
|
|
|
} |
|
468
|
|
|
|
|
469
|
|
|
// preserve prefix |
|
470
|
|
|
$slumped = substr( $value, 0, $i ) . $slumped; |
|
471
|
|
|
|
|
472
|
|
|
$slumped = $this->stripLeadingZeros( $slumped ); |
|
473
|
|
|
|
|
474
|
|
|
if ( $slumped === '-0' ) { |
|
475
|
|
|
$slumped = '+0'; |
|
476
|
|
|
} |
|
477
|
|
|
|
|
478
|
|
|
return $slumped; |
|
479
|
|
|
} |
|
480
|
|
|
|
|
481
|
|
|
/** |
|
482
|
|
|
* @param string $digits |
|
483
|
|
|
* |
|
484
|
|
|
* @return string |
|
485
|
|
|
*/ |
|
486
|
|
|
private function stripLeadingZeros( $digits ) { |
|
487
|
|
|
$digits = preg_replace( '/^([-+])0+(?=\d)/', '\1', $digits ); |
|
488
|
|
|
return $digits; |
|
489
|
|
|
} |
|
490
|
|
|
|
|
491
|
|
|
/** |
|
492
|
|
|
* Shift the decimal point according to the given exponent. |
|
493
|
|
|
* |
|
494
|
|
|
* @param DecimalValue $decimal |
|
495
|
|
|
* @param int $exponent The exponent to apply (digits to shift by). A Positive exponent |
|
496
|
|
|
* shifts the decimal point to the right, a negative exponent shifts to the left. |
|
497
|
|
|
* |
|
498
|
|
|
* @throws InvalidArgumentException |
|
499
|
|
|
* @return DecimalValue |
|
500
|
|
|
*/ |
|
501
|
|
|
public function shift( DecimalValue $decimal, $exponent ) { |
|
502
|
|
|
if ( !is_int( $exponent ) ) { |
|
503
|
|
|
throw new InvalidArgumentException( '$exponent must be an integer' ); |
|
504
|
|
|
} |
|
505
|
|
|
|
|
506
|
|
|
if ( $exponent == 0 ) { |
|
507
|
|
|
return $decimal; |
|
508
|
|
|
} |
|
509
|
|
|
|
|
510
|
|
|
$sign = $decimal->getSign(); |
|
511
|
|
|
$intPart = $decimal->getIntegerPart(); |
|
512
|
|
|
$fractPart = $decimal->getFractionalPart(); |
|
513
|
|
|
|
|
514
|
|
|
if ( $exponent < 0 ) { |
|
515
|
|
|
$intPart = $this->shiftLeft( $intPart, $exponent ); |
|
516
|
|
|
} else { |
|
517
|
|
|
$fractPart = $this->shiftRight( $fractPart, $exponent ); |
|
518
|
|
|
} |
|
519
|
|
|
|
|
520
|
|
|
$digits = $sign . $intPart . $fractPart; |
|
521
|
|
|
$digits = $this->stripLeadingZeros( $digits ); |
|
522
|
|
|
|
|
523
|
|
|
return new DecimalValue( $digits ); |
|
524
|
|
|
} |
|
525
|
|
|
|
|
526
|
|
|
/** |
|
527
|
|
|
* @param string $intPart |
|
528
|
|
|
* @param int $exponent must be negative |
|
529
|
|
|
* |
|
530
|
|
|
* @return string |
|
531
|
|
|
*/ |
|
532
|
|
|
private function shiftLeft( $intPart, $exponent ) { |
|
533
|
|
|
//note: $exponent is negative! |
|
534
|
|
|
if ( -$exponent < strlen( $intPart ) ) { |
|
535
|
|
|
$intPart = substr( $intPart, 0, $exponent ) . '.' . substr( $intPart, $exponent ); |
|
536
|
|
|
} else { |
|
537
|
|
|
$intPart = '0.' . str_pad( $intPart, -$exponent, '0', STR_PAD_LEFT ); |
|
538
|
|
|
} |
|
539
|
|
|
|
|
540
|
|
|
return $intPart; |
|
541
|
|
|
} |
|
542
|
|
|
|
|
543
|
|
|
/** |
|
544
|
|
|
* @param string $fractPart |
|
545
|
|
|
* @param int $exponent must be positive |
|
546
|
|
|
* |
|
547
|
|
|
* @return string |
|
548
|
|
|
*/ |
|
549
|
|
|
private function shiftRight( $fractPart, $exponent ) { |
|
550
|
|
|
//note: $exponent is positive. |
|
551
|
|
|
if ( $exponent < strlen( $fractPart ) ) { |
|
552
|
|
|
$fractPart = substr( $fractPart, 0, $exponent ) . '.' . substr( $fractPart, $exponent ); |
|
553
|
|
|
} else { |
|
554
|
|
|
$fractPart = str_pad( $fractPart, $exponent, '0', STR_PAD_RIGHT ); |
|
555
|
|
|
} |
|
556
|
|
|
|
|
557
|
|
|
return $fractPart; |
|
558
|
|
|
} |
|
559
|
|
|
|
|
560
|
|
|
} |
|
561
|
|
|
|