Skip to content

Commit e6aacf7

Browse files
authored
Merge pull request #4466 from oleibman/issue407b
Ods Handling of Ceiling and Floor
2 parents 2d1f4e8 + 48ed1bc commit e6aacf7

File tree

17 files changed

+332
-30
lines changed

17 files changed

+332
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
3333
- Removing Columns/Rows Containing Merged Cells. [Issue #282](https://github.com/PHPOffice/PhpSpreadsheet/issues/282) [PR #4465](https://github.com/PHPOffice/PhpSpreadsheet/pull/4465)
3434
- Print Area and Row Break. [Issue #1275](https://github.com/PHPOffice/PhpSpreadsheet/issues/1275) [PR #4450](https://github.com/PHPOffice/PhpSpreadsheet/pull/4450)
3535
- Xls Writer Treat Hyperlink Starting with # as Internal. [Issue #56](https://github.com/PHPOffice/PhpSpreadsheet/issues/56) [PR #4453](https://github.com/PHPOffice/PhpSpreadsheet/pull/4453)
36+
- ODS Handling of Ceiling and Floor. [Issue #407](https://github.com/PHPOffice/PhpSpreadsheet/issues/407) [PR #4466](https://github.com/PHPOffice/PhpSpreadsheet/pull/4466)
3637

3738
## 2025-04-16 - 4.2.0
3839

infra/DocumentGenerator.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99

1010
class DocumentGenerator
1111
{
12+
private const EXCLUDED_FUNCTIONS = [
13+
'CEILING.ODS',
14+
'CEILING.XCL',
15+
'FLOOR.ODS',
16+
'FLOOR.XCL',
17+
];
18+
1219
/**
1320
* @param array<string, array{category: string, functionCall: string|string[], argumentCount: string, passCellReference?: bool, passByReference?: bool[], custom?: bool}> $phpSpreadsheetFunctions
1421
*/
@@ -23,6 +30,9 @@ public static function generateFunctionListByCategory($phpSpreadsheetFunctions):
2330
$result .= self::tableRow($lengths, ['Excel Function', 'PhpSpreadsheet Function']) . "\n";
2431
$result .= self::tableRow($lengths, null) . "\n";
2532
foreach ($phpSpreadsheetFunctions as $excelFunction => $functionInfo) {
33+
if (in_array($excelFunction, self::EXCLUDED_FUNCTIONS, true)) {
34+
continue;
35+
}
2636
if ($category === $functionInfo['category']) {
2737
$phpFunction = self::getPhpSpreadsheetFunctionText($functionInfo['functionCall']);
2838
$result .= self::tableRow($lengths, [$excelFunction, $phpFunction]) . "\n";
@@ -87,6 +97,9 @@ public static function generateFunctionListByName(array $phpSpreadsheetFunctions
8797
$result = "# Function list by name\n";
8898
$lastAlphabet = null;
8999
foreach ($phpSpreadsheetFunctions as $excelFunction => $functionInfo) {
100+
if (in_array($excelFunction, self::EXCLUDED_FUNCTIONS, true)) {
101+
continue;
102+
}
90103
$lengths = [25, 31, 37];
91104
if ($lastAlphabet !== $excelFunction[0]) {
92105
$lastAlphabet = $excelFunction[0];

src/PhpSpreadsheet/Calculation/FunctionArray.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,11 +277,23 @@ class FunctionArray extends CalculationBase
277277
'functionCall' => [MathTrig\Ceiling::class, 'math'],
278278
'argumentCount' => '1-3',
279279
],
280+
// pseudo-function to help with Ods
281+
'CEILING.ODS' => [
282+
'category' => Category::CATEGORY_MATH_AND_TRIG,
283+
'functionCall' => [MathTrig\Ceiling::class, 'mathOds'],
284+
'argumentCount' => '1-3',
285+
],
280286
'CEILING.PRECISE' => [
281287
'category' => Category::CATEGORY_MATH_AND_TRIG,
282288
'functionCall' => [MathTrig\Ceiling::class, 'precise'],
283289
'argumentCount' => '1,2',
284290
],
291+
// pseudo-function implemented in Ods
292+
'CEILING.XCL' => [
293+
'category' => Category::CATEGORY_MATH_AND_TRIG,
294+
'functionCall' => [MathTrig\Ceiling::class, 'ceiling'],
295+
'argumentCount' => '2',
296+
],
285297
'CELL' => [
286298
'category' => Category::CATEGORY_INFORMATION,
287299
'functionCall' => [Functions::class, 'DUMMY'],
@@ -914,11 +926,23 @@ class FunctionArray extends CalculationBase
914926
'functionCall' => [MathTrig\Floor::class, 'math'],
915927
'argumentCount' => '1-3',
916928
],
929+
// pseudo-function to help with Ods
930+
'FLOOR.ODS' => [
931+
'category' => Category::CATEGORY_MATH_AND_TRIG,
932+
'functionCall' => [MathTrig\Floor::class, 'mathOds'],
933+
'argumentCount' => '1-3',
934+
],
917935
'FLOOR.PRECISE' => [
918936
'category' => Category::CATEGORY_MATH_AND_TRIG,
919937
'functionCall' => [MathTrig\Floor::class, 'precise'],
920938
'argumentCount' => '1-2',
921939
],
940+
// pseudo-function implemented in Ods
941+
'FLOOR.XCL' => [
942+
'category' => Category::CATEGORY_MATH_AND_TRIG,
943+
'functionCall' => [MathTrig\Floor::class, 'floor'],
944+
'argumentCount' => '2',
945+
],
922946
'FORECAST' => [
923947
'category' => Category::CATEGORY_STATISTICAL,
924948
'functionCall' => [Statistical\Trends::class, 'FORECAST'],

src/PhpSpreadsheet/Calculation/MathTrig/Ceiling.php

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ class Ceiling
2222
* Excel Function:
2323
* CEILING(number[,significance])
2424
*
25-
* @param array|float $number the number you want the ceiling
25+
* @param array<mixed>|float $number the number you want the ceiling
2626
* Or can be an array of values
27-
* @param array|float $significance the multiple to which you want to round
27+
* @param array<mixed>|float $significance the multiple to which you want to round
2828
* Or can be an array of values
2929
*
30-
* @return array|float|string Rounded Number, or a string containing an error
30+
* @return array<mixed>|float|string Rounded Number, or a string containing an error
3131
* If an array of numbers is passed as an argument, then the returned result will also be an array
3232
* with the same dimensions
3333
*/
@@ -63,14 +63,14 @@ public static function ceiling($number, $significance = null)
6363
* Or can be an array of values
6464
* @param mixed $significance Significance
6565
* Or can be an array of values
66-
* @param array|int $mode direction to round negative numbers
66+
* @param array<mixed>|int $mode direction to round negative numbers
6767
* Or can be an array of values
6868
*
69-
* @return array|float|string Rounded Number, or a string containing an error
69+
* @return array<mixed>|float|string Rounded Number, or a string containing an error
7070
* If an array of numbers is passed as an argument, then the returned result will also be an array
7171
* with the same dimensions
7272
*/
73-
public static function math(mixed $number, mixed $significance = null, $mode = 0): array|string|float
73+
public static function math(mixed $number, mixed $significance = null, $mode = 0, bool $checkSigns = false): array|string|float
7474
{
7575
if (is_array($number) || is_array($significance) || is_array($mode)) {
7676
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance, $mode);
@@ -87,6 +87,11 @@ public static function math(mixed $number, mixed $significance = null, $mode = 0
8787
if (empty($significance * $number)) {
8888
return 0.0;
8989
}
90+
if ($checkSigns) {
91+
if (($number > 0 && $significance < 0) || ($number < 0 && $significance > 0)) {
92+
return ExcelError::NAN();
93+
}
94+
}
9095
if (self::ceilingMathTest((float) $significance, (float) $number, (int) $mode)) {
9196
return floor($number / $significance) * $significance;
9297
}
@@ -104,10 +109,10 @@ public static function math(mixed $number, mixed $significance = null, $mode = 0
104109
*
105110
* @param mixed $number the number you want to round
106111
* Or can be an array of values
107-
* @param array|float $significance the multiple to which you want to round
112+
* @param array<mixed>|float $significance the multiple to which you want to round
108113
* Or can be an array of values
109114
*
110-
* @return array|float|string Rounded Number, or a string containing an error
115+
* @return array<mixed>|float|string Rounded Number, or a string containing an error
111116
* If an array of numbers is passed as an argument, then the returned result will also be an array
112117
* with the same dimensions
113118
*/
@@ -132,6 +137,23 @@ public static function precise(mixed $number, $significance = 1): array|string|f
132137
return ceil($result) * $significance * (($significance < 0) ? -1 : 1);
133138
}
134139

140+
/**
141+
* CEILING.ODS, pseudo-function - CEILING as implemented in ODS.
142+
*
143+
* ODS Function (theoretical):
144+
* CEILING.ODS(number[,significance[,mode]])
145+
*
146+
* @param mixed $number Number to round
147+
* @param mixed $significance Significance
148+
* @param array<mixed>|int $mode direction to round negative numbers
149+
*
150+
* @return array<mixed>|float|string Rounded Number, or a string containing an error
151+
*/
152+
public static function mathOds(mixed $number, mixed $significance = null, $mode = 0): array|string|float
153+
{
154+
return self::math($number, $significance, $mode, true);
155+
}
156+
135157
/**
136158
* Let CEILINGMATH complexity pass Scrutinizer.
137159
*/
@@ -148,7 +170,12 @@ private static function argumentsOk(float $number, float $significance): float|s
148170
if (empty($number * $significance)) {
149171
return 0.0;
150172
}
151-
if (Helpers::returnSign($number) == Helpers::returnSign($significance)) {
173+
$signSig = Helpers::returnSign($significance);
174+
$signNum = Helpers::returnSign($number);
175+
if (
176+
($signSig === 1 && ($signNum === 1 || Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_GNUMERIC))
177+
|| ($signSig === -1 && $signNum === -1)
178+
) {
152179
return ceil($number / $significance) * $significance;
153180
}
154181

src/PhpSpreadsheet/Calculation/MathTrig/Floor.php

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ private static function floorCheck1Arg(): void
3232
* @param mixed $significance Expect float. Significance
3333
* Or can be an array of values
3434
*
35-
* @return array|float|string Rounded Number, or a string containing an error
35+
* @return array<mixed>|float|string Rounded Number, or a string containing an error
3636
* If an array of numbers is passed as an argument, then the returned result will also be an array
3737
* with the same dimensions
3838
*/
@@ -71,11 +71,11 @@ public static function floor(mixed $number, mixed $significance = null)
7171
* @param mixed $mode direction to round negative numbers
7272
* Or can be an array of values
7373
*
74-
* @return array|float|string Rounded Number, or a string containing an error
74+
* @return array<mixed>|float|string Rounded Number, or a string containing an error
7575
* If an array of numbers is passed as an argument, then the returned result will also be an array
7676
* with the same dimensions
7777
*/
78-
public static function math(mixed $number, mixed $significance = null, mixed $mode = 0)
78+
public static function math(mixed $number, mixed $significance = null, mixed $mode = 0, bool $checkSigns = false)
7979
{
8080
if (is_array($number) || is_array($significance) || is_array($mode)) {
8181
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance, $mode);
@@ -89,9 +89,37 @@ public static function math(mixed $number, mixed $significance = null, mixed $mo
8989
return $e->getMessage();
9090
}
9191

92+
if (empty($significance * $number)) {
93+
return 0.0;
94+
}
95+
if ($checkSigns) {
96+
if (($number > 0 && $significance < 0) || ($number < 0 && $significance > 0)) {
97+
return ExcelError::NAN();
98+
}
99+
}
100+
92101
return self::argsOk((float) $number, (float) $significance, (int) $mode);
93102
}
94103

104+
/**
105+
* FLOOR.ODS, pseudo-function - FLOOR as implemented in ODS.
106+
*
107+
* Round a number down to the nearest integer or to the nearest multiple of significance.
108+
*
109+
* ODS Function (theoretical):
110+
* FLOOR.ODS(number[,significance[,mode]])
111+
*
112+
* @param mixed $number Number to round
113+
* @param mixed $significance Significance
114+
* @param array<mixed>|int $mode direction to round negative numbers
115+
*
116+
* @return array<mixed>|float|string Rounded Number, or a string containing an error
117+
*/
118+
public static function mathOds(mixed $number, mixed $significance = null, mixed $mode = 0)
119+
{
120+
return self::math($number, $significance, $mode, true);
121+
}
122+
95123
/**
96124
* FLOOR.PRECISE.
97125
*
@@ -100,12 +128,12 @@ public static function math(mixed $number, mixed $significance = null, mixed $mo
100128
* Excel Function:
101129
* FLOOR.PRECISE(number[,significance])
102130
*
103-
* @param array|float $number Number to round
131+
* @param array<mixed>|float $number Number to round
104132
* Or can be an array of values
105-
* @param array|float $significance Significance
133+
* @param array<mixed>|float $significance Significance
106134
* Or can be an array of values
107135
*
108-
* @return array|float|string Rounded Number, or a string containing an error
136+
* @return array<mixed>|float|string Rounded Number, or a string containing an error
109137
* If an array of numbers is passed as an argument, then the returned result will also be an array
110138
* with the same dimensions
111139
*/
@@ -121,6 +149,9 @@ public static function precise($number, $significance = 1)
121149
} catch (Exception $e) {
122150
return $e->getMessage();
123151
}
152+
if (!$significance) {
153+
return 0.0;
154+
}
124155

125156
return self::argumentsOkPrecise((float) $number, (float) $significance);
126157
}
@@ -179,10 +210,12 @@ private static function argumentsOk(float $number, float $significance): string|
179210
if ($number == 0.0) {
180211
return 0.0;
181212
}
182-
if (Helpers::returnSign($significance) == 1) {
183-
return floor($number / $significance) * $significance;
184-
}
185-
if (Helpers::returnSign($number) == -1 && Helpers::returnSign($significance) == -1) {
213+
$signSig = Helpers::returnSign($significance);
214+
$signNum = Helpers::returnSign($number);
215+
if (
216+
($signSig === 1 && ($signNum === 1 || Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_GNUMERIC))
217+
|| ($signNum === -1 && $signSig === -1)
218+
) {
186219
return floor($number / $significance) * $significance;
187220
}
188221

src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
44

5+
use Composer\Pcre\Preg;
56
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
67

78
class FormulaTranslator
@@ -27,7 +28,7 @@ public static function convertToExcelAddressValue(string $openOfficeAddress): st
2728
// Cell range 3-d reference
2829
// As we don't support 3-d ranges, we're just going to take a quick and dirty approach
2930
// and assume that the second worksheet reference is the same as the first
30-
$excelAddress = (string) preg_replace(
31+
$excelAddress = Preg::replace(
3132
[
3233
'/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu',
3334
'/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', // Cell range reference in another sheet
@@ -62,7 +63,7 @@ public static function convertToExcelFormulaValue(string $openOfficeFormula): st
6263
// so that conversion isn't done in string values
6364
$tKey = $tKey === false;
6465
if ($tKey) {
65-
$value = (string) preg_replace(
66+
$value = Preg::replace(
6667
[
6768
'/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', // Cell range reference in another sheet
6869
'/\[\$?([^\.]+)\.([^\.]+)\]/miu', // Cell reference in another sheet
@@ -103,7 +104,18 @@ public static function convertToExcelFormulaValue(string $openOfficeFormula): st
103104
Calculation::FORMULA_CLOSE_MATRIX_BRACE
104105
);
105106

106-
$value = (string) preg_replace('/COM\.MICROSOFT\./ui', '', $value);
107+
$value = Preg::replace(
108+
[
109+
'/\b(?<!com[.]microsoft[.])'
110+
. '(floor|ceiling)\s*[(]/ui',
111+
'/COM\.MICROSOFT\./ui',
112+
],
113+
[
114+
'$1.ODS(',
115+
'',
116+
],
117+
$value
118+
);
107119
}
108120
}
109121

src/PhpSpreadsheet/Writer/Ods/Formula.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public function convertFormula(string $formula, string $worksheetName = ''): str
2626
{
2727
$formula = $this->convertCellReferences($formula, $worksheetName);
2828
$formula = $this->convertDefinedNames($formula);
29+
$formula = $this->convertFunctionNames($formula);
2930

3031
if (!str_starts_with($formula, '=')) {
3132
$formula = '=' . $formula;
@@ -117,4 +118,22 @@ private function convertCellReferences(string $formula, string $worksheetName):
117118

118119
return $formula;
119120
}
121+
122+
private function convertFunctionNames(string $formula): string
123+
{
124+
return Preg::replace(
125+
[
126+
'/\b((CEILING|FLOOR)'
127+
. '([.](MATH|PRECISE))?)\s*[(]/ui',
128+
'/\b(CEILING|FLOOR)[.]XCL\s*[(]/ui',
129+
'/\b(CEILING|FLOOR)[.]ODS\s*[(]/ui',
130+
],
131+
[
132+
'COM.MICROSOFT.$1(',
133+
'COM.MICROSOFT.$1(',
134+
'$1(',
135+
],
136+
$formula
137+
);
138+
}
120139
}

src/PhpSpreadsheet/Writer/Xls/Parser.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,11 @@ class Parser
410410
'FISHER' => [283, 1, 1, 0],
411411
'FISHERINV' => [284, 1, 1, 0],
412412
'FLOOR' => [285, 2, 1, 0],
413+
'FLOOR.XCL' => [285, 2, 1, 0],
413414
'GAMMADIST' => [286, 4, 1, 0],
414415
'GAMMAINV' => [287, 3, 1, 0],
415416
'CEILING' => [288, 2, 1, 0],
417+
'CEILING.XCL' => [288, 2, 1, 0],
416418
'HYPGEOMDIST' => [289, 4, 1, 0],
417419
'LOGNORMDIST' => [290, 3, 1, 0],
418420
'LOGINV' => [291, 3, 1, 0],

src/PhpSpreadsheet/Writer/Xlsx/FunctionPrefix.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,18 @@ public static function addFunctionPrefix(string $functionString): string
216216
*/
217217
public static function addFunctionPrefixStripEquals(string $functionString): string
218218
{
219+
$functionString = Preg::replace(
220+
[
221+
'/\b(CEILING|FLOOR)[.]ODS\s*[(]/',
222+
'/\b(CEILING|FLOOR)[.]XCL\s*[(]/',
223+
],
224+
[
225+
'$1.MATH(',
226+
'$1(',
227+
],
228+
$functionString
229+
);
230+
219231
return self::addFunctionPrefix(substr($functionString, 1));
220232
}
221233
}

0 commit comments

Comments
 (0)