Skip to content

changed $trenType to $trendMethod in case TREND_BEST_FIT #4339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).

- Phpstan Version 2. [PR #4384](https://github.com/PHPOffice/PhpSpreadsheet/pull/4384)
- Start migration to Phpstan level 9. [PR #4396](https://github.com/PHPOffice/PhpSpreadsheet/pull/4396)
- TREND_POLYNOMIAL_* and TREND_BEST_FIT do not work, and are changed to throw Exceptions if attempted. (TREND_BEST_FIT_NO_POLY works.) An attempt to use an unknown trend type will now also throw an exception. [Issue #4400](https://github.com/PHPOffice/PhpSpreadsheet/issues/4400) [PR #4339](https://github.com/PHPOffice/PhpSpreadsheet/pull/4339)

### Moved

Expand All @@ -32,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Fixed

- BIN2DEC, OCT2DEC, and HEX2DEC return numbers rather than strings. [Issue #4383](https://github.com/PHPOffice/PhpSpreadsheet/issues/4383) [PR #4389](https://github.com/PHPOffice/PhpSpreadsheet/pull/4389)
- Fix TREND_BEST_FIT_NO_POLY. [Issue #4400](https://github.com/PHPOffice/PhpSpreadsheet/issues/4400) [PR #4339](https://github.com/PHPOffice/PhpSpreadsheet/pull/4339)

## 2025-03-02 - 4.1.0

Expand Down
84 changes: 0 additions & 84 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1434,90 +1434,6 @@ parameters:
count: 1
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getCorrelation\(\) on mixed\.$#'
identifier: method.nonObject
count: 1
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getCovariance\(\) on mixed\.$#'
identifier: method.nonObject
count: 1
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getDFResiduals\(\) on mixed\.$#'
identifier: method.nonObject
count: 2
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getF\(\) on mixed\.$#'
identifier: method.nonObject
count: 2
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getGoodnessOfFit\(\) on mixed\.$#'
identifier: method.nonObject
count: 3
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getIntersect\(\) on mixed\.$#'
identifier: method.nonObject
count: 5
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getIntersectSE\(\) on mixed\.$#'
identifier: method.nonObject
count: 2
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getSSRegression\(\) on mixed\.$#'
identifier: method.nonObject
count: 2
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getSSResiduals\(\) on mixed\.$#'
identifier: method.nonObject
count: 2
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getSlope\(\) on mixed\.$#'
identifier: method.nonObject
count: 5
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getSlopeSE\(\) on mixed\.$#'
identifier: method.nonObject
count: 2
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getStdevOfResiduals\(\) on mixed\.$#'
identifier: method.nonObject
count: 3
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getValueOfYForX\(\) on mixed\.$#'
identifier: method.nonObject
count: 3
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Cannot call method getXValues\(\) on mixed\.$#'
identifier: method.nonObject
count: 2
path: src/PhpSpreadsheet/Calculation/Statistical/Trends.php

-
message: '#^Parameter \#1 \$yValues of static method PhpOffice\\PhpSpreadsheet\\Calculation\\Statistical\\Trends\:\:validateTrendArrays\(\) expects array, mixed given\.$#'
identifier: argument.type
Expand Down
9 changes: 9 additions & 0 deletions src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;

use Matrix\Matrix;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;

// Phpstan and Scrutinizer seem to have legitimate complaints.
// $this->slope is specified where an array is expected in several places.
// But it seems that it should always be float.
// This code is probably not exercised at all in unit tests.
// Private bool property $implemented is set to indicate
// whether this implementation is correct.
class PolynomialBestFit extends BestFit
{
/**
Expand All @@ -21,6 +24,8 @@ class PolynomialBestFit extends BestFit
*/
protected int $order = 0;

private bool $implemented = false;

/**
* Return the order of this polynomial.
*/
Expand Down Expand Up @@ -187,6 +192,10 @@ private function polynomialRegression(int $order, array $yValues, array $xValues
*/
public function __construct(int $order, array $yValues, array $xValues = [])
{
if (!$this->implemented) {
throw new SpreadsheetException('Polynomial Best Fit not yet implemented');
}

parent::__construct($yValues, $xValues);

if (!$this->error) {
Expand Down
19 changes: 9 additions & 10 deletions src/PhpSpreadsheet/Shared/Trend/Trend.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace PhpOffice\PhpSpreadsheet\Shared\Trend;

use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;

class Trend
{
const TREND_LINEAR = 'Linear';
Expand All @@ -18,10 +20,8 @@ class Trend

/**
* Names of the best-fit Trend analysis methods.
*
* @var string[]
*/
private static array $trendTypes = [
private const TREND_TYPES = [
self::TREND_LINEAR,
self::TREND_LOGARITHMIC,
self::TREND_EXPONENTIAL,
Expand All @@ -48,7 +48,7 @@ class Trend
*/
private static array $trendCache = [];

public static function calculate(string $trendType = self::TREND_BEST_FIT, array $yValues = [], array $xValues = [], bool $const = true): mixed
public static function calculate(string $trendType = self::TREND_BEST_FIT, array $yValues = [], array $xValues = [], bool $const = true): BestFit
{
// Calculate number of points in each dataset
$nY = count($yValues);
Expand All @@ -59,7 +59,7 @@ public static function calculate(string $trendType = self::TREND_BEST_FIT, array
$xValues = range(1, $nY);
} elseif ($nY !== $nX) {
// Ensure both arrays of points are the same size
trigger_error('Trend(): Number of elements in coordinate arrays do not match.', E_USER_ERROR);
throw new SpreadsheetException('Trend(): Number of elements in coordinate arrays do not match.');
}

$key = md5($trendType . $const . serialize($yValues) . serialize($xValues));
Expand Down Expand Up @@ -93,13 +93,12 @@ public static function calculate(string $trendType = self::TREND_BEST_FIT, array
// Start by generating an instance of each available Trend method
$bestFit = [];
$bestFitValue = [];
foreach (self::$trendTypes as $trendMethod) {
$className = '\PhpOffice\PhpSpreadsheet\Shared\Trend\\' . $trendType . 'BestFit';
//* @phpstan-ignore-next-line
foreach (self::TREND_TYPES as $trendMethod) {
$className = '\PhpOffice\PhpSpreadsheet\Shared\Trend\\' . $trendMethod . 'BestFit';
$bestFit[$trendMethod] = new $className($yValues, $xValues, $const);
$bestFitValue[$trendMethod] = $bestFit[$trendMethod]->getGoodnessOfFit();
}
if ($trendType != self::TREND_BEST_FIT_NO_POLY) {
if ($trendType !== self::TREND_BEST_FIT_NO_POLY) {
foreach (self::$trendTypePolynomialOrders as $trendMethod) {
$order = (int) substr($trendMethod, -1);
$bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues);
Expand All @@ -116,7 +115,7 @@ public static function calculate(string $trendType = self::TREND_BEST_FIT, array

return $bestFit[$bestFitType];
default:
return false;
throw new SpreadsheetException("Unknown trend type $trendType");
}
}
}
88 changes: 88 additions & 0 deletions tests/PhpSpreadsheetTests/Shared/Trend/BestFitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace PhpOffice\PhpSpreadsheetTests\Shared\Trend;

use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\Shared\Trend\Trend;
use PHPUnit\Framework\TestCase;

class BestFitTest extends TestCase
{
private const LBF_PRECISION = 1.0E-4;

public function testBestFit(): void
{
$xValues = [45, 55, 47, 75, 90, 100, 100, 95, 88, 50, 45, 58];
$yValues = [15, 25, 17, 30, 41, 47, 50, 46, 37, 22, 20, 26];
$maxGoodness = -1000.0;
$maxType = '';

$type = Trend::TREND_LINEAR;
$result = Trend::calculate($type, $yValues, $xValues);
$goodness = $result->getGoodnessOfFit();
if ($maxGoodness < $goodness) {
$maxGoodness = $goodness;
$maxType = $type;
}
self::assertEqualsWithDelta(0.9628, $goodness, self::LBF_PRECISION);

$type = Trend::TREND_EXPONENTIAL;
$result = Trend::calculate($type, $yValues, $xValues);
$goodness = $result->getGoodnessOfFit();
if ($maxGoodness < $goodness) {
$maxGoodness = $goodness;
$maxType = $type;
}
self::assertEqualsWithDelta(0.9952, $goodness, self::LBF_PRECISION);

$type = Trend::TREND_LOGARITHMIC;
$result = Trend::calculate($type, $yValues, $xValues);
$goodness = $result->getGoodnessOfFit();
if ($maxGoodness < $goodness) {
$maxGoodness = $goodness;
$maxType = $type;
}
self::assertEqualsWithDelta(-0.0724, $goodness, self::LBF_PRECISION);

$type = Trend::TREND_POWER;
$result = Trend::calculate($type, $yValues, $xValues);
$goodness = $result->getGoodnessOfFit();
if ($maxGoodness < $goodness) {
$maxGoodness = $goodness;
$maxType = $type;
}
self::assertEqualsWithDelta(0.9946, $goodness, self::LBF_PRECISION);

$type = Trend::TREND_BEST_FIT_NO_POLY;
$result = Trend::calculate($type, $yValues, $xValues);
$goodness = $result->getGoodnessOfFit();
self::assertSame($maxGoodness, $goodness);
self::assertSame(lcfirst($maxType), $result->getBestFitType());

try {
$type = Trend::TREND_BEST_FIT;
Trend::calculate($type, $yValues, [0, 1, 2]);
self::fail('should have failed - mismatched number of elements');
} catch (SpreadsheetException $e) {
self::assertStringContainsString('Number of elements', $e->getMessage());
}

try {
$type = Trend::TREND_BEST_FIT;
Trend::calculate($type, $yValues, $xValues);
self::fail('should have failed - TREND_BEST_FIT includes polynomials which are not implemented yet');
} catch (SpreadsheetException $e) {
self::assertStringContainsString('not yet implemented', $e->getMessage());
}

try {
$type = 'unknown';
Trend::calculate($type, $yValues, $xValues);
self::fail('should have failed - invalid trend type');
} catch (SpreadsheetException $e) {
self::assertStringContainsString('Unknown trend type', $e->getMessage());
}
}
}