Skip to content

Commit 1c77e00

Browse files
authored
Merge pull request #4115 from oleibman/issue4113
New Algorithm for TRUNC, ROUNDUP, and ROUNDDOWN
2 parents 4da01c3 + b1e5e32 commit 1c77e00

File tree

7 files changed

+105
-12
lines changed

7 files changed

+105
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
2828
- Html Reader Preserve Unicode Whitespace. [Issue #1284](https://github.com/PHPOffice/PhpSpreadsheet/issues/1284) [PR #4106](https://github.com/PHPOffice/PhpSpreadsheet/pull/4106)
2929
- RATE Function Floating Point Number of Periods. [PR #4107](https://github.com/PHPOffice/PhpSpreadsheet/pull/4107)
3030
- Parameter Name Change Xlsx Writer Workbook. [Issue #4108](https://github.com/PHPOffice/PhpSpreadsheet/issues/4108) [PR #4111](https://github.com/PHPOffice/PhpSpreadsheet/pull/4111)
31+
- New Algorithm for TRUNC, ROUNDUP, ROUNDDOWN. [Issue #4113](https://github.com/PHPOffice/PhpSpreadsheet/issues/4113) [PR #4115](https://github.com/PHPOffice/PhpSpreadsheet/pull/4115)
3132

3233
## 2024-07-29 - 2.2.1
3334

src/PhpSpreadsheet/Calculation/MathTrig/Round.php

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ class Round
1010
{
1111
use ArrayEnabled;
1212

13-
private const ROUNDING_ADJUSTMENT = (PHP_VERSION_ID < 80400) ? 0 : 1e-14;
14-
1513
/**
1614
* ROUND.
1715
*
@@ -69,11 +67,22 @@ public static function up($number, $digits): array|string|float
6967
return 0.0;
7068
}
7169

70+
$digitsPlus1 = $digits + 1;
7271
if ($number < 0.0) {
73-
return round($number - 0.5 * 0.1 ** $digits + self::ROUNDING_ADJUSTMENT, $digits, PHP_ROUND_HALF_DOWN);
72+
if ($digitsPlus1 < 0) {
73+
return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN);
74+
}
75+
$result = sprintf("%.{$digitsPlus1}F", $number - 0.5 * 0.1 ** $digits);
76+
77+
return round((float) $result, $digits, PHP_ROUND_HALF_DOWN);
78+
}
79+
80+
if ($digitsPlus1 < 0) {
81+
return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN);
7482
}
83+
$result = sprintf("%.{$digitsPlus1}F", $number + 0.5 * 0.1 ** $digits);
7584

76-
return round($number + 0.5 * 0.1 ** $digits - self::ROUNDING_ADJUSTMENT, $digits, PHP_ROUND_HALF_DOWN);
85+
return round((float) $result, $digits, PHP_ROUND_HALF_DOWN);
7786
}
7887

7988
/**
@@ -105,11 +114,23 @@ public static function down($number, $digits): array|string|float
105114
return 0.0;
106115
}
107116

117+
$digitsPlus1 = $digits + 1;
108118
if ($number < 0.0) {
109-
return round($number + 0.5 * 0.1 ** $digits - self::ROUNDING_ADJUSTMENT, $digits, PHP_ROUND_HALF_UP);
119+
if ($digitsPlus1 < 0) {
120+
return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP);
121+
}
122+
$result = sprintf("%.{$digitsPlus1}F", $number + 0.5 * 0.1 ** $digits);
123+
124+
return round((float) $result, $digits, PHP_ROUND_HALF_UP);
125+
}
126+
127+
if ($digitsPlus1 < 0) {
128+
return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP);
110129
}
111130

112-
return round($number - 0.5 * 0.1 ** $digits + self::ROUNDING_ADJUSTMENT, $digits, PHP_ROUND_HALF_UP);
131+
$result = sprintf("%.{$digitsPlus1}F", $number - 0.5 * 0.1 ** $digits);
132+
133+
return round((float) $result, $digits, PHP_ROUND_HALF_UP);
113134
}
114135

115136
/**

src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ class Trunc
1313
* TRUNC.
1414
*
1515
* Truncates value to the number of fractional digits by number_digits.
16+
* This will probably not be the precise result in the unlikely
17+
* event that the number of digits to the left of the decimal
18+
* plus the number of digits to the right exceeds PHP_FLOAT_DIG
19+
* (or possibly that value minus 1).
20+
* Excel is unlikely to do any better.
1621
*
1722
* @param array|float $value Or can be an array of values
1823
* @param array|int $digits Or can be an array of values
@@ -34,15 +39,27 @@ public static function evaluate(array|float|string|null $value = 0, array|int|st
3439
return $e->getMessage();
3540
}
3641

37-
$digits = floor($digits);
42+
if ($value == 0) {
43+
return $value;
44+
}
3845

39-
// Truncate
40-
$adjust = 10 ** $digits;
46+
if ($value >= 0) {
47+
$minusSign = '';
48+
} else {
49+
$minusSign = '-';
50+
$value = -$value;
51+
}
52+
$digits = (int) floor($digits);
53+
if ($digits < 0) {
54+
$result = (float) (substr(sprintf('%.0F', $value), 0, $digits) . str_repeat('0', -$digits));
4155

42-
if (($digits > 0) && (rtrim((string) (int) ((abs($value) - abs((int) $value)) * $adjust), '0') < $adjust / 10)) {
43-
return $value;
56+
return ($minusSign === '') ? $result : -$result;
4457
}
58+
$decimals = (floor($value) == (int) $value) ? (PHP_FLOAT_DIG - strlen((string) (int) $value)) : $digits;
59+
$resultString = ($decimals < 0) ? sprintf('%F', $value) : sprintf('%.' . $decimals . 'F', $value);
60+
$regExp = '/([.]\\d{' . $digits . '})\\d+$/';
61+
$result = $minusSign . (preg_replace($regExp, '$1', $resultString) ?? $resultString);
4562

46-
return ((int) ($value * $adjust)) / $adjust;
63+
return (float) $result;
4764
}
4865
}

tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,36 @@ public static function providerTruncArray(): array
4747
'matrix' => [[[3.14, 3.141], [3.14159, 3.14159265]], '3.1415926536', '{2, 3; 5, 8}'],
4848
];
4949
}
50+
51+
/**
52+
* @dataProvider providerTooMuchPrecision
53+
*/
54+
public function testTooMuchPrecision(mixed $expectedResult, float|int|string $value, int $digits = 1): void
55+
{
56+
// This test is pretty screwy. Possibly shouldn't even attempt it.
57+
// At any rate, these results seem to indicate that PHP
58+
// maximum precision is PHP_FLOAT_DIG - 1 digits, not PHP_FLOAT_DIG.
59+
// If that changes, at least one of these tests will have to change.
60+
$sheet = $this->getSheet();
61+
$sheet->getCell('E1')->setValue($value);
62+
$sheet->getCell('E2')->setValue("=TRUNC(E1,$digits)");
63+
$result = $sheet->getCell('E2')->getCalculatedValue();
64+
self::assertSame($expectedResult, (string) $result);
65+
}
66+
67+
public static function providerTooMuchPrecision(): array
68+
{
69+
$max64Plus1 = 9223372036854775808;
70+
$stringMax = (string) $max64Plus1;
71+
72+
return [
73+
'2 digits less than PHP_FLOAT_DIG' => ['1' . str_repeat('0', PHP_FLOAT_DIG - 4) . '1.2', 10.0 ** (PHP_FLOAT_DIG - 3) + 1.2, 1],
74+
'1 digit less than PHP_FLOAT_DIG' => ['1' . str_repeat('0', PHP_FLOAT_DIG - 3) . '1', 10.0 ** (PHP_FLOAT_DIG - 2) + 1.2, 1],
75+
'PHP_FLOAT_DIG' => ['1.0E+' . (PHP_FLOAT_DIG - 1), 10.0 ** (PHP_FLOAT_DIG - 1) + 1.2, 1],
76+
'1 digit more than PHP_FLOAT_DIG' => ['1.0E+' . PHP_FLOAT_DIG, 10.0 ** PHP_FLOAT_DIG + 1.2, 1],
77+
'32bit exceed int max' => ['3123456780', 3123456789, -1],
78+
'64bit exceed int max neg decimals' => [$stringMax, $max64Plus1, -1],
79+
'64bit exceed int max pos decimals' => [$stringMax, $max64Plus1, 1],
80+
];
81+
}
5082
}

tests/data/Calculation/MathTrig/ROUNDDOWN.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@
3333
[0, 'B1, 0'],
3434
['exception', ''],
3535
['exception', '35.51'],
36+
'negative number and precision' => [-31400, '-31415.92654, -2'],
3637
];

tests/data/Calculation/MathTrig/ROUNDUP.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@
3333
[0, 'B1, 0'],
3434
['exception', ''],
3535
['exception', '35.51'],
36+
'negative number and precision' => [-31500, '-31415.92654, -2'],
3637
];

tests/data/Calculation/MathTrig/TRUNC.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
[-31415.92654, '-31415.92654, 10'],
1212
[31415.92, '31415.92654, 2'],
1313
[31400, '31415.92654, -2'],
14+
'negative number and precision' => [-31400, '-31415.92654, -2'],
1415
[0, '31415.92654, -10'],
1516
[0, '-31415.92654, -10'],
1617
[12000, '12345.6789, -3'],
@@ -32,4 +33,23 @@
3233
[-3, 'A4'],
3334
[-5, 'A5'],
3435
[0, 'B1'],
36+
'issue4113' => [1.0, '1.01, 1'],
37+
'issue4113 negative' => [-1.0, '-1.01, 1'],
38+
'issue4113 additional' => [10.04, '10.04, 2'],
39+
'issue4113 additional negative' => [-10.04, '-10.04, 2'],
40+
'issue4113 small fraction keep all' => [0.04, '0.04, 2'],
41+
'issue4113 small negative fraction keep all' => [-0.04, '-0.04, 2'],
42+
'issue4113 small fraction lose some' => [0.0, '0.01, 1'],
43+
'issue4113 small negative fraction lose some' => [0.0, '-0.001, 1'],
44+
'issue4113 example 3' => [-43747, '-43747.99122596, 0'],
45+
'issue4113 example 3 positive' => [43747, '43747.99122596, 0'],
46+
'issue4113 example 4' => [-9.11, '-9.1196419, 2'],
47+
'issue4113 example 5' => [-42300.65, '-42300.65099338, 3'],
48+
'issue4113 example 6 variant 1' => [0.000012, '0.0000123, 6'],
49+
'issue4113 example 6 variant 2' => [0.0000123, '0.0000123, 8'],
50+
'issue4113 example 6 variant 3' => [-0.000012, '-0.0000123, 6'],
51+
'issue4113 example 6 variant 4' => [-0.0000123, '-0.0000123, 8'],
52+
'issue4113 example 6 variant 5' => [0.000012, '1.23E-5, 6'],
53+
'issue4113 example 6 variant 6' => [-0.0000123, '-1.23E-5, 8'],
54+
'exceed 32-bit int max' => [3123456780, '3123456789, -1'],
3555
];

0 commit comments

Comments
 (0)