248 changed files with 75001 additions and 0 deletions
@ -0,0 +1,30 @@ |
|||||
|
# PhpSpreadsheet |
||||
|
|
||||
|
[](https://github.com/PHPOffice/PhpSpreadsheet/actions) |
||||
|
[](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/?branch=master) |
||||
|
[](https://scrutinizer-ci.com/g/PHPOffice/PhpSpreadsheet/?branch=master) |
||||
|
[](https://packagist.org/packages/phpoffice/phpspreadsheet) |
||||
|
[](https://packagist.org/packages/phpoffice/phpspreadsheet) |
||||
|
[](https://packagist.org/packages/phpoffice/phpspreadsheet) |
||||
|
[](https://gitter.im/PHPOffice/PhpSpreadsheet) |
||||
|
|
||||
|
PhpSpreadsheet is a library written in pure PHP and offers a set of classes that |
||||
|
allow you to read and write various spreadsheet file formats such as Excel and LibreOffice Calc. |
||||
|
|
||||
|
## Documentation |
||||
|
|
||||
|
Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet). |
||||
|
|
||||
|
Please ask your support questions on [StackOverflow](https://stackoverflow.com/questions/tagged/phpspreadsheet), or have a quick chat on [Gitter](https://gitter.im/PHPOffice/PhpSpreadsheet). |
||||
|
|
||||
|
## PHPExcel vs PhpSpreadsheet ? |
||||
|
|
||||
|
PhpSpreadsheet is the next version of PHPExcel. It breaks compatibility to dramatically improve the code base quality (namespaces, PSR compliance, use of latest PHP language features, etc.). |
||||
|
|
||||
|
Because all efforts have shifted to PhpSpreadsheet, PHPExcel will no longer be maintained. All contributions for PHPExcel, patches and new features, should target PhpSpreadsheet `master` branch. |
||||
|
|
||||
|
Do you need to migrate? There is [an automated tool](/docs/topics/migration-from-PHPExcel.md) for that. |
||||
|
|
||||
|
## License |
||||
|
|
||||
|
PhpSpreadsheet is licensed under [MIT](https://github.com/PHPOffice/PhpSpreadsheet/blob/master/LICENSE). |
||||
File diff suppressed because it is too large
@ -0,0 +1,51 @@ |
|||||
|
<?php |
||||
|
|
||||
|
$config = []; |
||||
|
|
||||
|
if (PHP_VERSION_ID < 80000) { |
||||
|
// GdImage not available before PHP8 |
||||
|
$config['parameters']['ignoreErrors'][] = [ |
||||
|
'message' => '~^Method .* has invalid return type GdImage\.$~', |
||||
|
'path' => __DIR__ . '/src/PhpSpreadsheet/Shared/Drawing.php', |
||||
|
'count' => 1, |
||||
|
]; |
||||
|
$config['parameters']['ignoreErrors'][] = [ |
||||
|
'message' => '~^Property .* has unknown class GdImage as its type\.$~', |
||||
|
'path' => __DIR__ . '/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php', |
||||
|
'count' => 1, |
||||
|
]; |
||||
|
$config['parameters']['ignoreErrors'][] = [ |
||||
|
'message' => '~^Method .* has invalid return type GdImage\.$~', |
||||
|
'path' => __DIR__ . '/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php', |
||||
|
'count' => 1, |
||||
|
]; |
||||
|
$config['parameters']['ignoreErrors'][] = [ |
||||
|
'message' => '~^Parameter .* of method .* has invalid type GdImage\.$~', |
||||
|
'path' => __DIR__ . '/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php', |
||||
|
'count' => 1, |
||||
|
]; |
||||
|
$config['parameters']['ignoreErrors'][] = [ |
||||
|
'message' => '~^Class GdImage not found\.$~', |
||||
|
'path' => __DIR__ . '/src/PhpSpreadsheet/Writer/Xls/Worksheet.php', |
||||
|
'count' => 1, |
||||
|
]; |
||||
|
$config['parameters']['ignoreErrors'][] = [ |
||||
|
'message' => '~^Parameter .* of method .* has invalid type GdImage\.$~', |
||||
|
'path' => __DIR__ . '/src/PhpSpreadsheet/Writer/Xls/Worksheet.php', |
||||
|
'count' => 1, |
||||
|
]; |
||||
|
// Erroneous analysis by Phpstan before PHP8 - 3rd parameter is nullable |
||||
|
$config['parameters']['ignoreErrors'][] = [ |
||||
|
'message' => '#^Parameter \\#3 \\$namespace of method XMLWriter\\:\\:startElementNs\\(\\) expects string, null given\\.$#', |
||||
|
'path' => __DIR__ . '/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php', |
||||
|
'count' => 8, |
||||
|
]; |
||||
|
// Erroneous analysis by Phpstan before PHP8 - mb_strlen does not return false |
||||
|
$config['parameters']['ignoreErrors'][] = [ |
||||
|
'message' => '#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:countCharacters\\(\\) should return int but returns int(<0, max>)?\\|false\\.$#', |
||||
|
'path' => __DIR__ . '/src/PhpSpreadsheet/Shared/StringHelper.php', |
||||
|
'count' => 1, |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
return $config; |
||||
@ -0,0 +1,26 @@ |
|||||
|
includes: |
||||
|
- phpstan-baseline.neon |
||||
|
- phpstan-conditional.php |
||||
|
- vendor/phpstan/phpstan-phpunit/extension.neon |
||||
|
- vendor/phpstan/phpstan-phpunit/rules.neon |
||||
|
|
||||
|
parameters: |
||||
|
level: 8 |
||||
|
paths: |
||||
|
- src/ |
||||
|
- tests/ |
||||
|
parallel: |
||||
|
processTimeout: 300.0 |
||||
|
checkMissingIterableValueType: false |
||||
|
ignoreErrors: |
||||
|
- '~^Parameter \#1 \$im(age)? of function (imagedestroy|imageistruecolor|imagealphablending|imagesavealpha|imagecolortransparent|imagecolorsforindex|imagesavealpha|imagesx|imagesy) expects (GdImage|resource), GdImage\|resource given\.$~' |
||||
|
- '~^Parameter \#2 \$src_im(age)? of function imagecopy expects (GdImage|resource), GdImage\|resource given\.$~' |
||||
|
# Accept a bit anything for assert methods |
||||
|
- '~^Parameter \#2 .* of static method PHPUnit\\Framework\\Assert\:\:assert\w+\(\) expects .*, .* given\.$~' |
||||
|
- '~^Method PhpOffice\\PhpSpreadsheetTests\\.*\:\:test.*\(\) has parameter \$args with no type specified\.$~' |
||||
|
|
||||
|
# Ignore all JpGraph issues |
||||
|
- '~^Constant (MARK_CIRCLE|MARK_CROSS|MARK_DIAMOND|MARK_DTRIANGLE|MARK_FILLEDCIRCLE|MARK_SQUARE|MARK_STAR|MARK_UTRIANGLE|MARK_X|SIDE_RIGHT) not found\.$~' |
||||
|
- '~^Instantiated class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot) not found\.$~' |
||||
|
- '~^Call to method .*\(\) on an unknown class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot)\.$~' |
||||
|
- '~^Access to property .* on an unknown class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot)\.$~' |
||||
@ -0,0 +1,129 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
|
||||
|
use DateTime; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
|
||||
|
class Time |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* TIME. |
||||
|
* |
||||
|
* The TIME function returns a value that represents a particular time. |
||||
|
* |
||||
|
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time |
||||
|
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* TIME(hour,minute,second) |
||||
|
* |
||||
|
* @param array|int $hour A number from 0 (zero) to 32767 representing the hour. |
||||
|
* Any value greater than 23 will be divided by 24 and the remainder |
||||
|
* will be treated as the hour value. For example, TIME(27,0,0) = |
||||
|
* TIME(3,0,0) = .125 or 3:00 AM. |
||||
|
* @param array|int $minute A number from 0 to 32767 representing the minute. |
||||
|
* Any value greater than 59 will be converted to hours and minutes. |
||||
|
* For example, TIME(0,750,0) = TIME(12,30,0) = .520833 or 12:30 PM. |
||||
|
* @param array|int $second A number from 0 to 32767 representing the second. |
||||
|
* Any value greater than 59 will be converted to hours, minutes, |
||||
|
* and seconds. For example, TIME(0,0,2000) = TIME(0,33,22) = .023148 |
||||
|
* or 12:33:20 AM |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
* |
||||
|
* @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
|
* depending on the value of the ReturnDateType flag |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function fromHMS($hour, $minute, $second) |
||||
|
{ |
||||
|
if (is_array($hour) || is_array($minute) || is_array($second)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $hour, $minute, $second); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$hour = self::toIntWithNullBool($hour); |
||||
|
$minute = self::toIntWithNullBool($minute); |
||||
|
$second = self::toIntWithNullBool($second); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
self::adjustSecond($second, $minute); |
||||
|
self::adjustMinute($minute, $hour); |
||||
|
|
||||
|
if ($hour > 23) { |
||||
|
$hour = $hour % 24; |
||||
|
} elseif ($hour < 0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
// Execute function |
||||
|
$retType = Functions::getReturnDateType(); |
||||
|
if ($retType === Functions::RETURNDATE_EXCEL) { |
||||
|
$calendar = SharedDateHelper::getExcelCalendar(); |
||||
|
$date = (int) ($calendar !== SharedDateHelper::CALENDAR_WINDOWS_1900); |
||||
|
|
||||
|
return (float) SharedDateHelper::formattedPHPToExcel($calendar, 1, $date, $hour, $minute, $second); |
||||
|
} |
||||
|
if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { |
||||
|
return (int) SharedDateHelper::excelToTimestamp(SharedDateHelper::formattedPHPToExcel(1970, 1, 1, $hour, $minute, $second)); // -2147468400; // -2147472000 + 3600 |
||||
|
} |
||||
|
// RETURNDATE_PHP_DATETIME_OBJECT |
||||
|
// Hour has already been normalized (0-23) above |
||||
|
$phpDateObject = new DateTime('1900-01-01 ' . $hour . ':' . $minute . ':' . $second); |
||||
|
|
||||
|
return $phpDateObject; |
||||
|
} |
||||
|
|
||||
|
private static function adjustSecond(int &$second, int &$minute): void |
||||
|
{ |
||||
|
if ($second < 0) { |
||||
|
$minute += floor($second / 60); |
||||
|
$second = 60 - abs($second % 60); |
||||
|
if ($second == 60) { |
||||
|
$second = 0; |
||||
|
} |
||||
|
} elseif ($second >= 60) { |
||||
|
$minute += floor($second / 60); |
||||
|
$second = $second % 60; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static function adjustMinute(int &$minute, int &$hour): void |
||||
|
{ |
||||
|
if ($minute < 0) { |
||||
|
$hour += floor($minute / 60); |
||||
|
$minute = 60 - abs($minute % 60); |
||||
|
if ($minute == 60) { |
||||
|
$minute = 0; |
||||
|
} |
||||
|
} elseif ($minute >= 60) { |
||||
|
$hour += floor($minute / 60); |
||||
|
$minute = $minute % 60; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $value expect int |
||||
|
*/ |
||||
|
private static function toIntWithNullBool($value): int |
||||
|
{ |
||||
|
$value = $value ?? 0; |
||||
|
if (is_bool($value)) { |
||||
|
$value = (int) $value; |
||||
|
} |
||||
|
if (!is_numeric($value)) { |
||||
|
throw new Exception(Functions::VALUE()); |
||||
|
} |
||||
|
|
||||
|
return (int) $value; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
|
||||
|
class TimeParts |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* HOUROFDAY. |
||||
|
* |
||||
|
* Returns the hour of a time value. |
||||
|
* The hour is given as an integer, ranging from 0 (12:00 A.M.) to 23 (11:00 P.M.). |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* HOUR(timeValue) |
||||
|
* |
||||
|
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), |
||||
|
* PHP DateTime object, or a standard time string |
||||
|
* Or can be an array of date/time values |
||||
|
* |
||||
|
* @return array|int|string Hour |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function hour($timeValue) |
||||
|
{ |
||||
|
if (is_array($timeValue)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
Helpers::nullFalseTrueToNumber($timeValue); |
||||
|
if (!is_numeric($timeValue)) { |
||||
|
$timeValue = Helpers::getTimeValue($timeValue); |
||||
|
} |
||||
|
Helpers::validateNotNegative($timeValue); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
// Execute function |
||||
|
$timeValue = fmod($timeValue, 1); |
||||
|
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); |
||||
|
|
||||
|
return (int) $timeValue->format('H'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* MINUTE. |
||||
|
* |
||||
|
* Returns the minutes of a time value. |
||||
|
* The minute is given as an integer, ranging from 0 to 59. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* MINUTE(timeValue) |
||||
|
* |
||||
|
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), |
||||
|
* PHP DateTime object, or a standard time string |
||||
|
* Or can be an array of date/time values |
||||
|
* |
||||
|
* @return array|int|string Minute |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function minute($timeValue) |
||||
|
{ |
||||
|
if (is_array($timeValue)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
Helpers::nullFalseTrueToNumber($timeValue); |
||||
|
if (!is_numeric($timeValue)) { |
||||
|
$timeValue = Helpers::getTimeValue($timeValue); |
||||
|
} |
||||
|
Helpers::validateNotNegative($timeValue); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
// Execute function |
||||
|
$timeValue = fmod($timeValue, 1); |
||||
|
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); |
||||
|
|
||||
|
return (int) $timeValue->format('i'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SECOND. |
||||
|
* |
||||
|
* Returns the seconds of a time value. |
||||
|
* The minute is given as an integer, ranging from 0 to 59. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* SECOND(timeValue) |
||||
|
* |
||||
|
* @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), |
||||
|
* PHP DateTime object, or a standard time string |
||||
|
* Or can be an array of date/time values |
||||
|
* |
||||
|
* @return array|int|string Second |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function second($timeValue) |
||||
|
{ |
||||
|
if (is_array($timeValue)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
Helpers::nullFalseTrueToNumber($timeValue); |
||||
|
if (!is_numeric($timeValue)) { |
||||
|
$timeValue = Helpers::getTimeValue($timeValue); |
||||
|
} |
||||
|
Helpers::validateNotNegative($timeValue); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
// Execute function |
||||
|
$timeValue = fmod($timeValue, 1); |
||||
|
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); |
||||
|
|
||||
|
return (int) $timeValue->format('s'); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
|
||||
|
use Datetime; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
|
||||
|
class TimeValue |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* TIMEVALUE. |
||||
|
* |
||||
|
* Returns a value that represents a particular time. |
||||
|
* Use TIMEVALUE to convert a time represented by a text string to an Excel or PHP date/time stamp |
||||
|
* value. |
||||
|
* |
||||
|
* NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time |
||||
|
* format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* TIMEVALUE(timeValue) |
||||
|
* |
||||
|
* @param array|string $timeValue A text string that represents a time in any one of the Microsoft |
||||
|
* Excel time formats; for example, "6:45 PM" and "18:45" text strings |
||||
|
* within quotation marks that represent time. |
||||
|
* Date information in time_text is ignored. |
||||
|
* Or can be an array of date/time values |
||||
|
* |
||||
|
* @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
|
* depending on the value of the ReturnDateType flag |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function fromString($timeValue) |
||||
|
{ |
||||
|
if (is_array($timeValue)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); |
||||
|
} |
||||
|
|
||||
|
$timeValue = trim($timeValue ?? '', '"'); |
||||
|
$timeValue = str_replace(['/', '.'], '-', $timeValue); |
||||
|
|
||||
|
$arraySplit = preg_split('/[\/:\-\s]/', $timeValue) ?: []; |
||||
|
if ((count($arraySplit) == 2 || count($arraySplit) == 3) && $arraySplit[0] > 24) { |
||||
|
$arraySplit[0] = ($arraySplit[0] % 24); |
||||
|
$timeValue = implode(':', $arraySplit); |
||||
|
} |
||||
|
|
||||
|
$PHPDateArray = Helpers::dateParse($timeValue); |
||||
|
$retValue = Functions::VALUE(); |
||||
|
if (Helpers::dateParseSucceeded($PHPDateArray)) { |
||||
|
/** @var int */ |
||||
|
$hour = $PHPDateArray['hour']; |
||||
|
/** @var int */ |
||||
|
$minute = $PHPDateArray['minute']; |
||||
|
/** @var int */ |
||||
|
$second = $PHPDateArray['second']; |
||||
|
// OpenOffice-specific code removed - it works just like Excel |
||||
|
$excelDateValue = SharedDateHelper::formattedPHPToExcel(1900, 1, 1, $hour, $minute, $second) - 1; |
||||
|
|
||||
|
$retType = Functions::getReturnDateType(); |
||||
|
if ($retType === Functions::RETURNDATE_EXCEL) { |
||||
|
$retValue = (float) $excelDateValue; |
||||
|
} elseif ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { |
||||
|
$retValue = (int) $phpDateValue = SharedDateHelper::excelToTimestamp($excelDateValue + 25569) - 3600; |
||||
|
} else { |
||||
|
$retValue = new DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $retValue; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,278 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
|
||||
|
use DateTime; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
|
||||
|
class Week |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* WEEKNUM. |
||||
|
* |
||||
|
* Returns the week of the year for a specified date. |
||||
|
* The WEEKNUM function considers the week containing January 1 to be the first week of the year. |
||||
|
* However, there is a European standard that defines the first week as the one with the majority |
||||
|
* of days (four or more) falling in the new year. This means that for years in which there are |
||||
|
* three days or less in the first week of January, the WEEKNUM function returns week numbers |
||||
|
* that are incorrect according to the European standard. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* WEEKNUM(dateValue[,style]) |
||||
|
* |
||||
|
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
|
* PHP DateTime object, or a standard date string |
||||
|
* Or can be an array of date values |
||||
|
* @param array|int $method Week begins on Sunday or Monday |
||||
|
* 1 or omitted Week begins on Sunday. |
||||
|
* 2 Week begins on Monday. |
||||
|
* 11 Week begins on Monday. |
||||
|
* 12 Week begins on Tuesday. |
||||
|
* 13 Week begins on Wednesday. |
||||
|
* 14 Week begins on Thursday. |
||||
|
* 15 Week begins on Friday. |
||||
|
* 16 Week begins on Saturday. |
||||
|
* 17 Week begins on Sunday. |
||||
|
* 21 ISO (Jan. 4 is week 1, begins on Monday). |
||||
|
* Or can be an array of methods |
||||
|
* |
||||
|
* @return array|int|string Week Number |
||||
|
* If an array of values is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function number($dateValue, $method = Constants::STARTWEEK_SUNDAY) |
||||
|
{ |
||||
|
if (is_array($dateValue) || is_array($method)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $method); |
||||
|
} |
||||
|
|
||||
|
$origDateValueNull = empty($dateValue); |
||||
|
|
||||
|
try { |
||||
|
$method = self::validateMethod($method); |
||||
|
if ($dateValue === null) { // boolean not allowed |
||||
|
$dateValue = (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904 || $method === Constants::DOW_SUNDAY) ? 0 : 1; |
||||
|
} |
||||
|
$dateValue = self::validateDateValue($dateValue); |
||||
|
if (!$dateValue && self::buggyWeekNum1900($method)) { |
||||
|
// This seems to be an additional Excel bug. |
||||
|
return 0; |
||||
|
} |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
// Execute function |
||||
|
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); |
||||
|
if ($method == Constants::STARTWEEK_MONDAY_ISO) { |
||||
|
Helpers::silly1900($PHPDateObject); |
||||
|
|
||||
|
return (int) $PHPDateObject->format('W'); |
||||
|
} |
||||
|
if (self::buggyWeekNum1904($method, $origDateValueNull, $PHPDateObject)) { |
||||
|
return 0; |
||||
|
} |
||||
|
Helpers::silly1900($PHPDateObject, '+ 5 years'); // 1905 calendar matches |
||||
|
$dayOfYear = (int) $PHPDateObject->format('z'); |
||||
|
$PHPDateObject->modify('-' . $dayOfYear . ' days'); |
||||
|
$firstDayOfFirstWeek = (int) $PHPDateObject->format('w'); |
||||
|
$daysInFirstWeek = (6 - $firstDayOfFirstWeek + $method) % 7; |
||||
|
$daysInFirstWeek += 7 * !$daysInFirstWeek; |
||||
|
$endFirstWeek = $daysInFirstWeek - 1; |
||||
|
$weekOfYear = floor(($dayOfYear - $endFirstWeek + 13) / 7); |
||||
|
|
||||
|
return (int) $weekOfYear; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ISOWEEKNUM. |
||||
|
* |
||||
|
* Returns the ISO 8601 week number of the year for a specified date. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* ISOWEEKNUM(dateValue) |
||||
|
* |
||||
|
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
|
* PHP DateTime object, or a standard date string |
||||
|
* Or can be an array of date values |
||||
|
* |
||||
|
* @return array|int|string Week Number |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function isoWeekNumber($dateValue) |
||||
|
{ |
||||
|
if (is_array($dateValue)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); |
||||
|
} |
||||
|
|
||||
|
if (self::apparentBug($dateValue)) { |
||||
|
return 52; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$dateValue = Helpers::getDateValue($dateValue); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
// Execute function |
||||
|
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); |
||||
|
Helpers::silly1900($PHPDateObject); |
||||
|
|
||||
|
return (int) $PHPDateObject->format('W'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* WEEKDAY. |
||||
|
* |
||||
|
* Returns the day of the week for a specified date. The day is given as an integer |
||||
|
* ranging from 0 to 7 (dependent on the requested style). |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* WEEKDAY(dateValue[,style]) |
||||
|
* |
||||
|
* @param null|array|float|int|string $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
|
* PHP DateTime object, or a standard date string |
||||
|
* Or can be an array of date values |
||||
|
* @param mixed $style A number that determines the type of return value |
||||
|
* 1 or omitted Numbers 1 (Sunday) through 7 (Saturday). |
||||
|
* 2 Numbers 1 (Monday) through 7 (Sunday). |
||||
|
* 3 Numbers 0 (Monday) through 6 (Sunday). |
||||
|
* Or can be an array of styles |
||||
|
* |
||||
|
* @return array|int|string Day of the week value |
||||
|
* If an array of values is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function day($dateValue, $style = 1) |
||||
|
{ |
||||
|
if (is_array($dateValue) || is_array($style)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $style); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$dateValue = Helpers::getDateValue($dateValue); |
||||
|
$style = self::validateStyle($style); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
// Execute function |
||||
|
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); |
||||
|
Helpers::silly1900($PHPDateObject); |
||||
|
$DoW = (int) $PHPDateObject->format('w'); |
||||
|
|
||||
|
switch ($style) { |
||||
|
case 1: |
||||
|
++$DoW; |
||||
|
|
||||
|
break; |
||||
|
case 2: |
||||
|
$DoW = self::dow0Becomes7($DoW); |
||||
|
|
||||
|
break; |
||||
|
case 3: |
||||
|
$DoW = self::dow0Becomes7($DoW) - 1; |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
return $DoW; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $style expect int |
||||
|
*/ |
||||
|
private static function validateStyle($style): int |
||||
|
{ |
||||
|
if (!is_numeric($style)) { |
||||
|
throw new Exception(Functions::VALUE()); |
||||
|
} |
||||
|
$style = (int) $style; |
||||
|
if (($style < 1) || ($style > 3)) { |
||||
|
throw new Exception(Functions::NAN()); |
||||
|
} |
||||
|
|
||||
|
return $style; |
||||
|
} |
||||
|
|
||||
|
private static function dow0Becomes7(int $DoW): int |
||||
|
{ |
||||
|
return ($DoW === 0) ? 7 : $DoW; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), |
||||
|
* PHP DateTime object, or a standard date string |
||||
|
*/ |
||||
|
private static function apparentBug($dateValue): bool |
||||
|
{ |
||||
|
if (SharedDateHelper::getExcelCalendar() !== SharedDateHelper::CALENDAR_MAC_1904) { |
||||
|
if (is_bool($dateValue)) { |
||||
|
return true; |
||||
|
} |
||||
|
if (is_numeric($dateValue) && !((int) $dateValue)) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Validate dateValue parameter. |
||||
|
* |
||||
|
* @param mixed $dateValue |
||||
|
*/ |
||||
|
private static function validateDateValue($dateValue): float |
||||
|
{ |
||||
|
if (is_bool($dateValue)) { |
||||
|
throw new Exception(Functions::VALUE()); |
||||
|
} |
||||
|
|
||||
|
return Helpers::getDateValue($dateValue); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Validate method parameter. |
||||
|
* |
||||
|
* @param mixed $method |
||||
|
*/ |
||||
|
private static function validateMethod($method): int |
||||
|
{ |
||||
|
if ($method === null) { |
||||
|
$method = Constants::STARTWEEK_SUNDAY; |
||||
|
} |
||||
|
|
||||
|
if (!is_numeric($method)) { |
||||
|
throw new Exception(Functions::VALUE()); |
||||
|
} |
||||
|
|
||||
|
$method = (int) $method; |
||||
|
if (!array_key_exists($method, Constants::METHODARR)) { |
||||
|
throw new Exception(Functions::NAN()); |
||||
|
} |
||||
|
$method = Constants::METHODARR[$method]; |
||||
|
|
||||
|
return $method; |
||||
|
} |
||||
|
|
||||
|
private static function buggyWeekNum1900(int $method): bool |
||||
|
{ |
||||
|
return $method === Constants::DOW_SUNDAY && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_WINDOWS_1900; |
||||
|
} |
||||
|
|
||||
|
private static function buggyWeekNum1904(int $method, bool $origNull, DateTime $dateObject): bool |
||||
|
{ |
||||
|
// This appears to be another Excel bug. |
||||
|
|
||||
|
return $method === Constants::DOW_SUNDAY && SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904 && |
||||
|
!$origNull && $dateObject->format('Y-m-d') === '1904-01-01'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,201 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class WorkDay |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* WORKDAY. |
||||
|
* |
||||
|
* Returns the date that is the indicated number of working days before or after a date (the |
||||
|
* starting date). Working days exclude weekends and any dates identified as holidays. |
||||
|
* Use WORKDAY to exclude weekends or holidays when you calculate invoice due dates, expected |
||||
|
* delivery times, or the number of days of work performed. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* WORKDAY(startDate,endDays[,holidays[,holiday[,...]]]) |
||||
|
* |
||||
|
* @param array|mixed $startDate Excel date serial value (float), PHP date timestamp (integer), |
||||
|
* PHP DateTime object, or a standard date string |
||||
|
* Or can be an array of date values |
||||
|
* @param array|int $endDays The number of nonweekend and nonholiday days before or after |
||||
|
* startDate. A positive value for days yields a future date; a |
||||
|
* negative value yields a past date. |
||||
|
* Or can be an array of int values |
||||
|
* @param null|mixed $dateArgs An array of dates (such as holidays) to exclude from the calculation |
||||
|
* |
||||
|
* @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
||||
|
* depending on the value of the ReturnDateType flag |
||||
|
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result |
||||
|
* will also be an array with matching dimensions |
||||
|
*/ |
||||
|
public static function date($startDate, $endDays, ...$dateArgs) |
||||
|
{ |
||||
|
if (is_array($startDate) || is_array($endDays)) { |
||||
|
return self::evaluateArrayArgumentsSubset( |
||||
|
[self::class, __FUNCTION__], |
||||
|
2, |
||||
|
$startDate, |
||||
|
$endDays, |
||||
|
...$dateArgs |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// Retrieve the mandatory start date and days that are referenced in the function definition |
||||
|
try { |
||||
|
$startDate = Helpers::getDateValue($startDate); |
||||
|
$endDays = Helpers::validateNumericNull($endDays); |
||||
|
$holidayArray = array_map([Helpers::class, 'getDateValue'], Functions::flattenArray($dateArgs)); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$startDate = (float) floor($startDate); |
||||
|
$endDays = (int) floor($endDays); |
||||
|
// If endDays is 0, we always return startDate |
||||
|
if ($endDays == 0) { |
||||
|
return $startDate; |
||||
|
} |
||||
|
if ($endDays < 0) { |
||||
|
return self::decrementing($startDate, $endDays, $holidayArray); |
||||
|
} |
||||
|
|
||||
|
return self::incrementing($startDate, $endDays, $holidayArray); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Use incrementing logic to determine Workday. |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
private static function incrementing(float $startDate, int $endDays, array $holidayArray) |
||||
|
{ |
||||
|
// Adjust the start date if it falls over a weekend |
||||
|
$startDoW = self::getWeekDay($startDate, 3); |
||||
|
if ($startDoW >= 5) { |
||||
|
$startDate += 7 - $startDoW; |
||||
|
--$endDays; |
||||
|
} |
||||
|
|
||||
|
// Add endDays |
||||
|
$endDate = (float) $startDate + ((int) ($endDays / 5) * 7); |
||||
|
$endDays = $endDays % 5; |
||||
|
while ($endDays > 0) { |
||||
|
++$endDate; |
||||
|
// Adjust the calculated end date if it falls over a weekend |
||||
|
$endDow = self::getWeekDay($endDate, 3); |
||||
|
if ($endDow >= 5) { |
||||
|
$endDate += 7 - $endDow; |
||||
|
} |
||||
|
--$endDays; |
||||
|
} |
||||
|
|
||||
|
// Test any extra holiday parameters |
||||
|
if (!empty($holidayArray)) { |
||||
|
$endDate = self::incrementingArray($startDate, $endDate, $holidayArray); |
||||
|
} |
||||
|
|
||||
|
return Helpers::returnIn3FormatsFloat($endDate); |
||||
|
} |
||||
|
|
||||
|
private static function incrementingArray(float $startDate, float $endDate, array $holidayArray): float |
||||
|
{ |
||||
|
$holidayCountedArray = $holidayDates = []; |
||||
|
foreach ($holidayArray as $holidayDate) { |
||||
|
if (self::getWeekDay($holidayDate, 3) < 5) { |
||||
|
$holidayDates[] = $holidayDate; |
||||
|
} |
||||
|
} |
||||
|
sort($holidayDates, SORT_NUMERIC); |
||||
|
foreach ($holidayDates as $holidayDate) { |
||||
|
if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) { |
||||
|
if (!in_array($holidayDate, $holidayCountedArray)) { |
||||
|
++$endDate; |
||||
|
$holidayCountedArray[] = $holidayDate; |
||||
|
} |
||||
|
} |
||||
|
// Adjust the calculated end date if it falls over a weekend |
||||
|
$endDoW = self::getWeekDay($endDate, 3); |
||||
|
if ($endDoW >= 5) { |
||||
|
$endDate += 7 - $endDoW; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $endDate; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Use decrementing logic to determine Workday. |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
private static function decrementing(float $startDate, int $endDays, array $holidayArray) |
||||
|
{ |
||||
|
// Adjust the start date if it falls over a weekend |
||||
|
$startDoW = self::getWeekDay($startDate, 3); |
||||
|
if ($startDoW >= 5) { |
||||
|
$startDate += -$startDoW + 4; |
||||
|
++$endDays; |
||||
|
} |
||||
|
|
||||
|
// Add endDays |
||||
|
$endDate = (float) $startDate + ((int) ($endDays / 5) * 7); |
||||
|
$endDays = $endDays % 5; |
||||
|
while ($endDays < 0) { |
||||
|
--$endDate; |
||||
|
// Adjust the calculated end date if it falls over a weekend |
||||
|
$endDow = self::getWeekDay($endDate, 3); |
||||
|
if ($endDow >= 5) { |
||||
|
$endDate += 4 - $endDow; |
||||
|
} |
||||
|
++$endDays; |
||||
|
} |
||||
|
|
||||
|
// Test any extra holiday parameters |
||||
|
if (!empty($holidayArray)) { |
||||
|
$endDate = self::decrementingArray($startDate, $endDate, $holidayArray); |
||||
|
} |
||||
|
|
||||
|
return Helpers::returnIn3FormatsFloat($endDate); |
||||
|
} |
||||
|
|
||||
|
private static function decrementingArray(float $startDate, float $endDate, array $holidayArray): float |
||||
|
{ |
||||
|
$holidayCountedArray = $holidayDates = []; |
||||
|
foreach ($holidayArray as $holidayDate) { |
||||
|
if (self::getWeekDay($holidayDate, 3) < 5) { |
||||
|
$holidayDates[] = $holidayDate; |
||||
|
} |
||||
|
} |
||||
|
rsort($holidayDates, SORT_NUMERIC); |
||||
|
foreach ($holidayDates as $holidayDate) { |
||||
|
if (($holidayDate <= $startDate) && ($holidayDate >= $endDate)) { |
||||
|
if (!in_array($holidayDate, $holidayCountedArray)) { |
||||
|
--$endDate; |
||||
|
$holidayCountedArray[] = $holidayDate; |
||||
|
} |
||||
|
} |
||||
|
// Adjust the calculated end date if it falls over a weekend |
||||
|
$endDoW = self::getWeekDay($endDate, 3); |
||||
|
/** int $endDoW */ |
||||
|
if ($endDoW >= 5) { |
||||
|
$endDate += -$endDoW + 4; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $endDate; |
||||
|
} |
||||
|
|
||||
|
private static function getWeekDay(float $date, int $wd): int |
||||
|
{ |
||||
|
$result = Functions::scalar(Week::day($date, $wd)); |
||||
|
|
||||
|
return is_int($result) ? $result : -1; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; |
||||
|
|
||||
|
class YearFrac |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* YEARFRAC. |
||||
|
* |
||||
|
* Calculates the fraction of the year represented by the number of whole days between two dates |
||||
|
* (the start_date and the end_date). |
||||
|
* Use the YEARFRAC worksheet function to identify the proportion of a whole year's benefits or |
||||
|
* obligations to assign to a specific term. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* YEARFRAC(startDate,endDate[,method]) |
||||
|
* See https://lists.oasis-open.org/archives/office-formula/200806/msg00039.html |
||||
|
* for description of algorithm used in Excel |
||||
|
* |
||||
|
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), |
||||
|
* PHP DateTime object, or a standard date string |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer), |
||||
|
* PHP DateTime object, or a standard date string |
||||
|
* Or can be an array of methods |
||||
|
* @param array|int $method Method used for the calculation |
||||
|
* 0 or omitted US (NASD) 30/360 |
||||
|
* 1 Actual/actual |
||||
|
* 2 Actual/360 |
||||
|
* 3 Actual/365 |
||||
|
* 4 European 30/360 |
||||
|
* Or can be an array of methods |
||||
|
* |
||||
|
* @return array|float|string fraction of the year, or a string containing an error |
||||
|
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result |
||||
|
* will also be an array with matching dimensions |
||||
|
*/ |
||||
|
public static function fraction($startDate, $endDate, $method = 0) |
||||
|
{ |
||||
|
if (is_array($startDate) || is_array($endDate) || is_array($method)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $method); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$method = (int) Helpers::validateNumericNull($method); |
||||
|
$sDate = Helpers::getDateValue($startDate); |
||||
|
$eDate = Helpers::getDateValue($endDate); |
||||
|
$sDate = self::excelBug($sDate, $startDate, $endDate, $method); |
||||
|
$eDate = self::excelBug($eDate, $endDate, $startDate, $method); |
||||
|
$startDate = min($sDate, $eDate); |
||||
|
$endDate = max($sDate, $eDate); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
switch ($method) { |
||||
|
case 0: |
||||
|
return Functions::scalar(Days360::between($startDate, $endDate)) / 360; |
||||
|
case 1: |
||||
|
return self::method1($startDate, $endDate); |
||||
|
case 2: |
||||
|
return Functions::scalar(Difference::interval($startDate, $endDate)) / 360; |
||||
|
case 3: |
||||
|
return Functions::scalar(Difference::interval($startDate, $endDate)) / 365; |
||||
|
case 4: |
||||
|
return Functions::scalar(Days360::between($startDate, $endDate, true)) / 360; |
||||
|
} |
||||
|
|
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Excel 1900 calendar treats date argument of null as 1900-01-00. Really. |
||||
|
* |
||||
|
* @param mixed $startDate |
||||
|
* @param mixed $endDate |
||||
|
*/ |
||||
|
private static function excelBug(float $sDate, $startDate, $endDate, int $method): float |
||||
|
{ |
||||
|
if (Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE && SharedDateHelper::getExcelCalendar() !== SharedDateHelper::CALENDAR_MAC_1904) { |
||||
|
if ($endDate === null && $startDate !== null) { |
||||
|
if (DateParts::month($sDate) == 12 && DateParts::day($sDate) === 31 && $method === 0) { |
||||
|
$sDate += 2; |
||||
|
} else { |
||||
|
++$sDate; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $sDate; |
||||
|
} |
||||
|
|
||||
|
private static function method1(float $startDate, float $endDate): float |
||||
|
{ |
||||
|
$days = Functions::scalar(Difference::interval($startDate, $endDate)); |
||||
|
$startYear = (int) DateParts::year($startDate); |
||||
|
$endYear = (int) DateParts::year($endDate); |
||||
|
$years = $endYear - $startYear + 1; |
||||
|
$startMonth = (int) DateParts::month($startDate); |
||||
|
$startDay = (int) DateParts::day($startDate); |
||||
|
$endMonth = (int) DateParts::month($endDate); |
||||
|
$endDay = (int) DateParts::day($endDate); |
||||
|
$startMonthDay = 100 * $startMonth + $startDay; |
||||
|
$endMonthDay = 100 * $endMonth + $endDay; |
||||
|
if ($years == 1) { |
||||
|
$tmpCalcAnnualBasis = 365 + (int) Helpers::isLeapYear($endYear); |
||||
|
} elseif ($years == 2 && $startMonthDay >= $endMonthDay) { |
||||
|
if (Helpers::isLeapYear($startYear)) { |
||||
|
$tmpCalcAnnualBasis = 365 + (int) ($startMonthDay <= 229); |
||||
|
} elseif (Helpers::isLeapYear($endYear)) { |
||||
|
$tmpCalcAnnualBasis = 365 + (int) ($endMonthDay >= 229); |
||||
|
} else { |
||||
|
$tmpCalcAnnualBasis = 365; |
||||
|
} |
||||
|
} else { |
||||
|
$tmpCalcAnnualBasis = 0; |
||||
|
for ($year = $startYear; $year <= $endYear; ++$year) { |
||||
|
$tmpCalcAnnualBasis += 365 + (int) Helpers::isLeapYear($year); |
||||
|
} |
||||
|
$tmpCalcAnnualBasis /= $years; |
||||
|
} |
||||
|
|
||||
|
return $days / $tmpCalcAnnualBasis; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,108 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Single |
||||
|
{ |
||||
|
/** |
||||
|
* FVSCHEDULE. |
||||
|
* |
||||
|
* Returns the future value of an initial principal after applying a series of compound interest rates. |
||||
|
* Use FVSCHEDULE to calculate the future value of an investment with a variable or adjustable rate. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* FVSCHEDULE(principal,schedule) |
||||
|
* |
||||
|
* @param mixed $principal the present value |
||||
|
* @param float[] $schedule an array of interest rates to apply |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function futureValue($principal, $schedule) |
||||
|
{ |
||||
|
$principal = Functions::flattenSingleValue($principal); |
||||
|
$schedule = Functions::flattenArray($schedule); |
||||
|
|
||||
|
try { |
||||
|
$principal = CashFlowValidations::validateFloat($principal); |
||||
|
|
||||
|
foreach ($schedule as $rate) { |
||||
|
$rate = CashFlowValidations::validateFloat($rate); |
||||
|
$principal *= 1 + $rate; |
||||
|
} |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return $principal; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* PDURATION. |
||||
|
* |
||||
|
* Calculates the number of periods required for an investment to reach a specified value. |
||||
|
* |
||||
|
* @param mixed $rate Interest rate per period |
||||
|
* @param mixed $presentValue Present Value |
||||
|
* @param mixed $futureValue Future Value |
||||
|
* |
||||
|
* @return float|string Result, or a string containing an error |
||||
|
*/ |
||||
|
public static function periods($rate, $presentValue, $futureValue) |
||||
|
{ |
||||
|
$rate = Functions::flattenSingleValue($rate); |
||||
|
$presentValue = Functions::flattenSingleValue($presentValue); |
||||
|
$futureValue = Functions::flattenSingleValue($futureValue); |
||||
|
|
||||
|
try { |
||||
|
$rate = CashFlowValidations::validateRate($rate); |
||||
|
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
||||
|
$futureValue = CashFlowValidations::validateFutureValue($futureValue); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
// Validate parameters |
||||
|
if ($rate <= 0.0 || $presentValue <= 0.0 || $futureValue <= 0.0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
return (log($futureValue) - log($presentValue)) / log(1 + $rate); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* RRI. |
||||
|
* |
||||
|
* Calculates the interest rate required for an investment to grow to a specified future value . |
||||
|
* |
||||
|
* @param float $periods The number of periods over which the investment is made |
||||
|
* @param float $presentValue Present Value |
||||
|
* @param float $futureValue Future Value |
||||
|
* |
||||
|
* @return float|string Result, or a string containing an error |
||||
|
*/ |
||||
|
public static function interestRate($periods = 0.0, $presentValue = 0.0, $futureValue = 0.0) |
||||
|
{ |
||||
|
$periods = Functions::flattenSingleValue($periods); |
||||
|
$presentValue = Functions::flattenSingleValue($presentValue); |
||||
|
$futureValue = Functions::flattenSingleValue($futureValue); |
||||
|
|
||||
|
try { |
||||
|
$periods = CashFlowValidations::validateFloat($periods); |
||||
|
$presentValue = CashFlowValidations::validatePresentValue($presentValue); |
||||
|
$futureValue = CashFlowValidations::validateFutureValue($futureValue); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
// Validate parameters |
||||
|
if ($periods <= 0.0 || $presentValue <= 0.0 || $futureValue < 0.0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
return ($futureValue / $presentValue) ** (1 / $periods) - 1; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,160 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Variable; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Periodic |
||||
|
{ |
||||
|
const FINANCIAL_MAX_ITERATIONS = 128; |
||||
|
|
||||
|
const FINANCIAL_PRECISION = 1.0e-08; |
||||
|
|
||||
|
/** |
||||
|
* IRR. |
||||
|
* |
||||
|
* Returns the internal rate of return for a series of cash flows represented by the numbers in values. |
||||
|
* These cash flows do not have to be even, as they would be for an annuity. However, the cash flows must occur |
||||
|
* at regular intervals, such as monthly or annually. The internal rate of return is the interest rate received |
||||
|
* for an investment consisting of payments (negative values) and income (positive values) that occur at regular |
||||
|
* periods. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* IRR(values[,guess]) |
||||
|
* |
||||
|
* @param mixed $values An array or a reference to cells that contain numbers for which you want |
||||
|
* to calculate the internal rate of return. |
||||
|
* Values must contain at least one positive value and one negative value to |
||||
|
* calculate the internal rate of return. |
||||
|
* @param mixed $guess A number that you guess is close to the result of IRR |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function rate($values, $guess = 0.1) |
||||
|
{ |
||||
|
if (!is_array($values)) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
$values = Functions::flattenArray($values); |
||||
|
$guess = Functions::flattenSingleValue($guess); |
||||
|
|
||||
|
// create an initial range, with a root somewhere between 0 and guess |
||||
|
$x1 = 0.0; |
||||
|
$x2 = $guess; |
||||
|
$f1 = self::presentValue($x1, $values); |
||||
|
$f2 = self::presentValue($x2, $values); |
||||
|
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { |
||||
|
if (($f1 * $f2) < 0.0) { |
||||
|
break; |
||||
|
} |
||||
|
if (abs($f1) < abs($f2)) { |
||||
|
$f1 = self::presentValue($x1 += 1.6 * ($x1 - $x2), $values); |
||||
|
} else { |
||||
|
$f2 = self::presentValue($x2 += 1.6 * ($x2 - $x1), $values); |
||||
|
} |
||||
|
} |
||||
|
if (($f1 * $f2) > 0.0) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
$f = self::presentValue($x1, $values); |
||||
|
if ($f < 0.0) { |
||||
|
$rtb = $x1; |
||||
|
$dx = $x2 - $x1; |
||||
|
} else { |
||||
|
$rtb = $x2; |
||||
|
$dx = $x1 - $x2; |
||||
|
} |
||||
|
|
||||
|
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { |
||||
|
$dx *= 0.5; |
||||
|
$x_mid = $rtb + $dx; |
||||
|
$f_mid = self::presentValue($x_mid, $values); |
||||
|
if ($f_mid <= 0.0) { |
||||
|
$rtb = $x_mid; |
||||
|
} |
||||
|
if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) { |
||||
|
return $x_mid; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* MIRR. |
||||
|
* |
||||
|
* Returns the modified internal rate of return for a series of periodic cash flows. MIRR considers both |
||||
|
* the cost of the investment and the interest received on reinvestment of cash. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* MIRR(values,finance_rate, reinvestment_rate) |
||||
|
* |
||||
|
* @param mixed $values An array or a reference to cells that contain a series of payments and |
||||
|
* income occurring at regular intervals. |
||||
|
* Payments are negative value, income is positive values. |
||||
|
* @param mixed $financeRate The interest rate you pay on the money used in the cash flows |
||||
|
* @param mixed $reinvestmentRate The interest rate you receive on the cash flows as you reinvest them |
||||
|
* |
||||
|
* @return float|string Result, or a string containing an error |
||||
|
*/ |
||||
|
public static function modifiedRate($values, $financeRate, $reinvestmentRate) |
||||
|
{ |
||||
|
if (!is_array($values)) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
$values = Functions::flattenArray($values); |
||||
|
$financeRate = Functions::flattenSingleValue($financeRate); |
||||
|
$reinvestmentRate = Functions::flattenSingleValue($reinvestmentRate); |
||||
|
$n = count($values); |
||||
|
|
||||
|
$rr = 1.0 + $reinvestmentRate; |
||||
|
$fr = 1.0 + $financeRate; |
||||
|
|
||||
|
$npvPos = $npvNeg = 0.0; |
||||
|
foreach ($values as $i => $v) { |
||||
|
if ($v >= 0) { |
||||
|
$npvPos += $v / $rr ** $i; |
||||
|
} else { |
||||
|
$npvNeg += $v / $fr ** $i; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (($npvNeg === 0.0) || ($npvPos === 0.0) || ($reinvestmentRate <= -1.0)) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
$mirr = ((-$npvPos * $rr ** $n) |
||||
|
/ ($npvNeg * ($rr))) ** (1.0 / ($n - 1)) - 1.0; |
||||
|
|
||||
|
return is_finite($mirr) ? $mirr : Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* NPV. |
||||
|
* |
||||
|
* Returns the Net Present Value of a cash flow series given a discount rate. |
||||
|
* |
||||
|
* @param mixed $rate |
||||
|
* |
||||
|
* @return float |
||||
|
*/ |
||||
|
public static function presentValue($rate, ...$args) |
||||
|
{ |
||||
|
$returnValue = 0; |
||||
|
|
||||
|
$rate = Functions::flattenSingleValue($rate); |
||||
|
$aArgs = Functions::flattenArray($args); |
||||
|
|
||||
|
// Calculate |
||||
|
$countArgs = count($aArgs); |
||||
|
for ($i = 1; $i <= $countArgs; ++$i) { |
||||
|
// Is it a numeric value? |
||||
|
if (is_numeric($aArgs[$i - 1])) { |
||||
|
$returnValue += $aArgs[$i - 1] / (1 + $rate) ** $i; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $returnValue; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,283 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Coupons; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Helpers; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Price |
||||
|
{ |
||||
|
/** |
||||
|
* PRICE. |
||||
|
* |
||||
|
* Returns the price per $100 face value of a security that pays periodic interest. |
||||
|
* |
||||
|
* @param mixed $settlement The security's settlement date. |
||||
|
* The security settlement date is the date after the issue date when the security |
||||
|
* is traded to the buyer. |
||||
|
* @param mixed $maturity The security's maturity date. |
||||
|
* The maturity date is the date when the security expires. |
||||
|
* @param mixed $rate the security's annual coupon rate |
||||
|
* @param mixed $yield the security's annual yield |
||||
|
* @param mixed $redemption The number of coupon payments per year. |
||||
|
* For annual payments, frequency = 1; |
||||
|
* for semiannual, frequency = 2; |
||||
|
* for quarterly, frequency = 4. |
||||
|
* @param mixed $frequency |
||||
|
* @param mixed $basis The type of day count to use. |
||||
|
* 0 or omitted US (NASD) 30/360 |
||||
|
* 1 Actual/actual |
||||
|
* 2 Actual/360 |
||||
|
* 3 Actual/365 |
||||
|
* 4 European 30/360 |
||||
|
* |
||||
|
* @return float|string Result, or a string containing an error |
||||
|
*/ |
||||
|
public static function price( |
||||
|
$settlement, |
||||
|
$maturity, |
||||
|
$rate, |
||||
|
$yield, |
||||
|
$redemption, |
||||
|
$frequency, |
||||
|
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
) { |
||||
|
$settlement = Functions::flattenSingleValue($settlement); |
||||
|
$maturity = Functions::flattenSingleValue($maturity); |
||||
|
$rate = Functions::flattenSingleValue($rate); |
||||
|
$yield = Functions::flattenSingleValue($yield); |
||||
|
$redemption = Functions::flattenSingleValue($redemption); |
||||
|
$frequency = Functions::flattenSingleValue($frequency); |
||||
|
$basis = ($basis === null) |
||||
|
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
: Functions::flattenSingleValue($basis); |
||||
|
|
||||
|
try { |
||||
|
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
|
$maturity = SecurityValidations::validateMaturityDate($maturity); |
||||
|
SecurityValidations::validateSecurityPeriod($settlement, $maturity); |
||||
|
$rate = SecurityValidations::validateRate($rate); |
||||
|
$yield = SecurityValidations::validateYield($yield); |
||||
|
$redemption = SecurityValidations::validateRedemption($redemption); |
||||
|
$frequency = SecurityValidations::validateFrequency($frequency); |
||||
|
$basis = SecurityValidations::validateBasis($basis); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$dsc = Coupons::COUPDAYSNC($settlement, $maturity, $frequency, $basis); |
||||
|
$e = Coupons::COUPDAYS($settlement, $maturity, $frequency, $basis); |
||||
|
$n = Coupons::COUPNUM($settlement, $maturity, $frequency, $basis); |
||||
|
$a = Coupons::COUPDAYBS($settlement, $maturity, $frequency, $basis); |
||||
|
|
||||
|
$baseYF = 1.0 + ($yield / $frequency); |
||||
|
$rfp = 100 * ($rate / $frequency); |
||||
|
$de = $dsc / $e; |
||||
|
|
||||
|
$result = $redemption / $baseYF ** (--$n + $de); |
||||
|
for ($k = 0; $k <= $n; ++$k) { |
||||
|
$result += $rfp / ($baseYF ** ($k + $de)); |
||||
|
} |
||||
|
$result -= $rfp * ($a / $e); |
||||
|
|
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* PRICEDISC. |
||||
|
* |
||||
|
* Returns the price per $100 face value of a discounted security. |
||||
|
* |
||||
|
* @param mixed $settlement The security's settlement date. |
||||
|
* The security settlement date is the date after the issue date when the security |
||||
|
* is traded to the buyer. |
||||
|
* @param mixed $maturity The security's maturity date. |
||||
|
* The maturity date is the date when the security expires. |
||||
|
* @param mixed $discount The security's discount rate |
||||
|
* @param mixed $redemption The security's redemption value per $100 face value |
||||
|
* @param mixed $basis The type of day count to use. |
||||
|
* 0 or omitted US (NASD) 30/360 |
||||
|
* 1 Actual/actual |
||||
|
* 2 Actual/360 |
||||
|
* 3 Actual/365 |
||||
|
* 4 European 30/360 |
||||
|
* |
||||
|
* @return float|string Result, or a string containing an error |
||||
|
*/ |
||||
|
public static function priceDiscounted( |
||||
|
$settlement, |
||||
|
$maturity, |
||||
|
$discount, |
||||
|
$redemption, |
||||
|
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
) { |
||||
|
$settlement = Functions::flattenSingleValue($settlement); |
||||
|
$maturity = Functions::flattenSingleValue($maturity); |
||||
|
$discount = Functions::flattenSingleValue($discount); |
||||
|
$redemption = Functions::flattenSingleValue($redemption); |
||||
|
$basis = ($basis === null) |
||||
|
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
: Functions::flattenSingleValue($basis); |
||||
|
|
||||
|
try { |
||||
|
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
|
$maturity = SecurityValidations::validateMaturityDate($maturity); |
||||
|
SecurityValidations::validateSecurityPeriod($settlement, $maturity); |
||||
|
$discount = SecurityValidations::validateDiscount($discount); |
||||
|
$redemption = SecurityValidations::validateRedemption($redemption); |
||||
|
$basis = SecurityValidations::validateBasis($basis); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); |
||||
|
if (!is_numeric($daysBetweenSettlementAndMaturity)) { |
||||
|
// return date error |
||||
|
return $daysBetweenSettlementAndMaturity; |
||||
|
} |
||||
|
|
||||
|
return $redemption * (1 - $discount * $daysBetweenSettlementAndMaturity); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* PRICEMAT. |
||||
|
* |
||||
|
* Returns the price per $100 face value of a security that pays interest at maturity. |
||||
|
* |
||||
|
* @param mixed $settlement The security's settlement date. |
||||
|
* The security's settlement date is the date after the issue date when the |
||||
|
* security is traded to the buyer. |
||||
|
* @param mixed $maturity The security's maturity date. |
||||
|
* The maturity date is the date when the security expires. |
||||
|
* @param mixed $issue The security's issue date |
||||
|
* @param mixed $rate The security's interest rate at date of issue |
||||
|
* @param mixed $yield The security's annual yield |
||||
|
* @param mixed $basis The type of day count to use. |
||||
|
* 0 or omitted US (NASD) 30/360 |
||||
|
* 1 Actual/actual |
||||
|
* 2 Actual/360 |
||||
|
* 3 Actual/365 |
||||
|
* 4 European 30/360 |
||||
|
* |
||||
|
* @return float|string Result, or a string containing an error |
||||
|
*/ |
||||
|
public static function priceAtMaturity( |
||||
|
$settlement, |
||||
|
$maturity, |
||||
|
$issue, |
||||
|
$rate, |
||||
|
$yield, |
||||
|
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
) { |
||||
|
$settlement = Functions::flattenSingleValue($settlement); |
||||
|
$maturity = Functions::flattenSingleValue($maturity); |
||||
|
$issue = Functions::flattenSingleValue($issue); |
||||
|
$rate = Functions::flattenSingleValue($rate); |
||||
|
$yield = Functions::flattenSingleValue($yield); |
||||
|
$basis = ($basis === null) |
||||
|
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
: Functions::flattenSingleValue($basis); |
||||
|
|
||||
|
try { |
||||
|
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
|
$maturity = SecurityValidations::validateMaturityDate($maturity); |
||||
|
SecurityValidations::validateSecurityPeriod($settlement, $maturity); |
||||
|
$issue = SecurityValidations::validateIssueDate($issue); |
||||
|
$rate = SecurityValidations::validateRate($rate); |
||||
|
$yield = SecurityValidations::validateYield($yield); |
||||
|
$basis = SecurityValidations::validateBasis($basis); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$daysPerYear = Functions::scalar(Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis)); |
||||
|
if (!is_numeric($daysPerYear)) { |
||||
|
return $daysPerYear; |
||||
|
} |
||||
|
$daysBetweenIssueAndSettlement = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $settlement, $basis)); |
||||
|
if (!is_numeric($daysBetweenIssueAndSettlement)) { |
||||
|
// return date error |
||||
|
return $daysBetweenIssueAndSettlement; |
||||
|
} |
||||
|
$daysBetweenIssueAndSettlement *= $daysPerYear; |
||||
|
$daysBetweenIssueAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $maturity, $basis)); |
||||
|
if (!is_numeric($daysBetweenIssueAndMaturity)) { |
||||
|
// return date error |
||||
|
return $daysBetweenIssueAndMaturity; |
||||
|
} |
||||
|
$daysBetweenIssueAndMaturity *= $daysPerYear; |
||||
|
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); |
||||
|
if (!is_numeric($daysBetweenSettlementAndMaturity)) { |
||||
|
// return date error |
||||
|
return $daysBetweenSettlementAndMaturity; |
||||
|
} |
||||
|
$daysBetweenSettlementAndMaturity *= $daysPerYear; |
||||
|
|
||||
|
return (100 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate * 100)) / |
||||
|
(1 + (($daysBetweenSettlementAndMaturity / $daysPerYear) * $yield)) - |
||||
|
(($daysBetweenIssueAndSettlement / $daysPerYear) * $rate * 100); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* RECEIVED. |
||||
|
* |
||||
|
* Returns the amount received at maturity for a fully invested Security. |
||||
|
* |
||||
|
* @param mixed $settlement The security's settlement date. |
||||
|
* The security settlement date is the date after the issue date when the security |
||||
|
* is traded to the buyer. |
||||
|
* @param mixed $maturity The security's maturity date. |
||||
|
* The maturity date is the date when the security expires. |
||||
|
* @param mixed $investment The amount invested in the security |
||||
|
* @param mixed $discount The security's discount rate |
||||
|
* @param mixed $basis The type of day count to use. |
||||
|
* 0 or omitted US (NASD) 30/360 |
||||
|
* 1 Actual/actual |
||||
|
* 2 Actual/360 |
||||
|
* 3 Actual/365 |
||||
|
* 4 European 30/360 |
||||
|
* |
||||
|
* @return float|string Result, or a string containing an error |
||||
|
*/ |
||||
|
public static function received( |
||||
|
$settlement, |
||||
|
$maturity, |
||||
|
$investment, |
||||
|
$discount, |
||||
|
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
) { |
||||
|
$settlement = Functions::flattenSingleValue($settlement); |
||||
|
$maturity = Functions::flattenSingleValue($maturity); |
||||
|
$investment = Functions::flattenSingleValue($investment); |
||||
|
$discount = Functions::flattenSingleValue($discount); |
||||
|
$basis = ($basis === null) |
||||
|
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
: Functions::flattenSingleValue($basis); |
||||
|
|
||||
|
try { |
||||
|
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
|
$maturity = SecurityValidations::validateMaturityDate($maturity); |
||||
|
SecurityValidations::validateSecurityPeriod($settlement, $maturity); |
||||
|
$investment = SecurityValidations::validateFloat($investment); |
||||
|
$discount = SecurityValidations::validateDiscount($discount); |
||||
|
$basis = SecurityValidations::validateBasis($basis); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if ($investment <= 0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
$daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis); |
||||
|
if (!is_numeric($daysBetweenSettlementAndMaturity)) { |
||||
|
// return date error |
||||
|
return $daysBetweenSettlementAndMaturity; |
||||
|
} |
||||
|
|
||||
|
return $investment / (1 - ($discount * $daysBetweenSettlementAndMaturity)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,137 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Rates |
||||
|
{ |
||||
|
/** |
||||
|
* DISC. |
||||
|
* |
||||
|
* Returns the discount rate for a security. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* DISC(settlement,maturity,price,redemption[,basis]) |
||||
|
* |
||||
|
* @param mixed $settlement The security's settlement date. |
||||
|
* The security settlement date is the date after the issue |
||||
|
* date when the security is traded to the buyer. |
||||
|
* @param mixed $maturity The security's maturity date. |
||||
|
* The maturity date is the date when the security expires. |
||||
|
* @param mixed $price The security's price per $100 face value |
||||
|
* @param mixed $redemption The security's redemption value per $100 face value |
||||
|
* @param mixed $basis The type of day count to use. |
||||
|
* 0 or omitted US (NASD) 30/360 |
||||
|
* 1 Actual/actual |
||||
|
* 2 Actual/360 |
||||
|
* 3 Actual/365 |
||||
|
* 4 European 30/360 |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function discount( |
||||
|
$settlement, |
||||
|
$maturity, |
||||
|
$price, |
||||
|
$redemption, |
||||
|
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
) { |
||||
|
$settlement = Functions::flattenSingleValue($settlement); |
||||
|
$maturity = Functions::flattenSingleValue($maturity); |
||||
|
$price = Functions::flattenSingleValue($price); |
||||
|
$redemption = Functions::flattenSingleValue($redemption); |
||||
|
$basis = ($basis === null) |
||||
|
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
: Functions::flattenSingleValue($basis); |
||||
|
|
||||
|
try { |
||||
|
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
|
$maturity = SecurityValidations::validateMaturityDate($maturity); |
||||
|
SecurityValidations::validateSecurityPeriod($settlement, $maturity); |
||||
|
$price = SecurityValidations::validatePrice($price); |
||||
|
$redemption = SecurityValidations::validateRedemption($redemption); |
||||
|
$basis = SecurityValidations::validateBasis($basis); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if ($price <= 0.0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); |
||||
|
if (!is_numeric($daysBetweenSettlementAndMaturity)) { |
||||
|
// return date error |
||||
|
return $daysBetweenSettlementAndMaturity; |
||||
|
} |
||||
|
|
||||
|
return (1 - $price / $redemption) / $daysBetweenSettlementAndMaturity; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* INTRATE. |
||||
|
* |
||||
|
* Returns the interest rate for a fully invested security. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* INTRATE(settlement,maturity,investment,redemption[,basis]) |
||||
|
* |
||||
|
* @param mixed $settlement The security's settlement date. |
||||
|
* The security settlement date is the date after the issue date when the security |
||||
|
* is traded to the buyer. |
||||
|
* @param mixed $maturity The security's maturity date. |
||||
|
* The maturity date is the date when the security expires. |
||||
|
* @param mixed $investment the amount invested in the security |
||||
|
* @param mixed $redemption the amount to be received at maturity |
||||
|
* @param mixed $basis The type of day count to use. |
||||
|
* 0 or omitted US (NASD) 30/360 |
||||
|
* 1 Actual/actual |
||||
|
* 2 Actual/360 |
||||
|
* 3 Actual/365 |
||||
|
* 4 European 30/360 |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function interest( |
||||
|
$settlement, |
||||
|
$maturity, |
||||
|
$investment, |
||||
|
$redemption, |
||||
|
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
) { |
||||
|
$settlement = Functions::flattenSingleValue($settlement); |
||||
|
$maturity = Functions::flattenSingleValue($maturity); |
||||
|
$investment = Functions::flattenSingleValue($investment); |
||||
|
$redemption = Functions::flattenSingleValue($redemption); |
||||
|
$basis = ($basis === null) |
||||
|
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
: Functions::flattenSingleValue($basis); |
||||
|
|
||||
|
try { |
||||
|
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
|
$maturity = SecurityValidations::validateMaturityDate($maturity); |
||||
|
SecurityValidations::validateSecurityPeriod($settlement, $maturity); |
||||
|
$investment = SecurityValidations::validateFloat($investment); |
||||
|
$redemption = SecurityValidations::validateRedemption($redemption); |
||||
|
$basis = SecurityValidations::validateBasis($basis); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if ($investment <= 0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); |
||||
|
if (!is_numeric($daysBetweenSettlementAndMaturity)) { |
||||
|
// return date error |
||||
|
return $daysBetweenSettlementAndMaturity; |
||||
|
} |
||||
|
|
||||
|
return (($redemption / $investment) - 1) / ($daysBetweenSettlementAndMaturity); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Financial\FinancialValidations; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class SecurityValidations extends FinancialValidations |
||||
|
{ |
||||
|
/** |
||||
|
* @param mixed $issue |
||||
|
*/ |
||||
|
public static function validateIssueDate($issue): float |
||||
|
{ |
||||
|
return self::validateDate($issue); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $settlement |
||||
|
* @param mixed $maturity |
||||
|
*/ |
||||
|
public static function validateSecurityPeriod($settlement, $maturity): void |
||||
|
{ |
||||
|
if ($settlement >= $maturity) { |
||||
|
throw new Exception(Functions::NAN()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $redemption |
||||
|
*/ |
||||
|
public static function validateRedemption($redemption): float |
||||
|
{ |
||||
|
$redemption = self::validateFloat($redemption); |
||||
|
if ($redemption <= 0.0) { |
||||
|
throw new Exception(Functions::NAN()); |
||||
|
} |
||||
|
|
||||
|
return $redemption; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,153 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Helpers; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Yields |
||||
|
{ |
||||
|
/** |
||||
|
* YIELDDISC. |
||||
|
* |
||||
|
* Returns the annual yield of a security that pays interest at maturity. |
||||
|
* |
||||
|
* @param mixed $settlement The security's settlement date. |
||||
|
* The security's settlement date is the date after the issue date when the security |
||||
|
* is traded to the buyer. |
||||
|
* @param mixed $maturity The security's maturity date. |
||||
|
* The maturity date is the date when the security expires. |
||||
|
* @param mixed $price The security's price per $100 face value |
||||
|
* @param mixed $redemption The security's redemption value per $100 face value |
||||
|
* @param mixed $basis The type of day count to use. |
||||
|
* 0 or omitted US (NASD) 30/360 |
||||
|
* 1 Actual/actual |
||||
|
* 2 Actual/360 |
||||
|
* 3 Actual/365 |
||||
|
* 4 European 30/360 |
||||
|
* |
||||
|
* @return float|string Result, or a string containing an error |
||||
|
*/ |
||||
|
public static function yieldDiscounted( |
||||
|
$settlement, |
||||
|
$maturity, |
||||
|
$price, |
||||
|
$redemption, |
||||
|
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
) { |
||||
|
$settlement = Functions::flattenSingleValue($settlement); |
||||
|
$maturity = Functions::flattenSingleValue($maturity); |
||||
|
$price = Functions::flattenSingleValue($price); |
||||
|
$redemption = Functions::flattenSingleValue($redemption); |
||||
|
$basis = ($basis === null) |
||||
|
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
: Functions::flattenSingleValue($basis); |
||||
|
|
||||
|
try { |
||||
|
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
|
$maturity = SecurityValidations::validateMaturityDate($maturity); |
||||
|
SecurityValidations::validateSecurityPeriod($settlement, $maturity); |
||||
|
$price = SecurityValidations::validatePrice($price); |
||||
|
$redemption = SecurityValidations::validateRedemption($redemption); |
||||
|
$basis = SecurityValidations::validateBasis($basis); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$daysPerYear = Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis); |
||||
|
if (!is_numeric($daysPerYear)) { |
||||
|
return $daysPerYear; |
||||
|
} |
||||
|
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); |
||||
|
if (!is_numeric($daysBetweenSettlementAndMaturity)) { |
||||
|
// return date error |
||||
|
return $daysBetweenSettlementAndMaturity; |
||||
|
} |
||||
|
$daysBetweenSettlementAndMaturity *= $daysPerYear; |
||||
|
|
||||
|
return (($redemption - $price) / $price) * ($daysPerYear / $daysBetweenSettlementAndMaturity); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* YIELDMAT. |
||||
|
* |
||||
|
* Returns the annual yield of a security that pays interest at maturity. |
||||
|
* |
||||
|
* @param mixed $settlement The security's settlement date. |
||||
|
* The security's settlement date is the date after the issue date when the security |
||||
|
* is traded to the buyer. |
||||
|
* @param mixed $maturity The security's maturity date. |
||||
|
* The maturity date is the date when the security expires. |
||||
|
* @param mixed $issue The security's issue date |
||||
|
* @param mixed $rate The security's interest rate at date of issue |
||||
|
* @param mixed $price The security's price per $100 face value |
||||
|
* @param mixed $basis The type of day count to use. |
||||
|
* 0 or omitted US (NASD) 30/360 |
||||
|
* 1 Actual/actual |
||||
|
* 2 Actual/360 |
||||
|
* 3 Actual/365 |
||||
|
* 4 European 30/360 |
||||
|
* |
||||
|
* @return float|string Result, or a string containing an error |
||||
|
*/ |
||||
|
public static function yieldAtMaturity( |
||||
|
$settlement, |
||||
|
$maturity, |
||||
|
$issue, |
||||
|
$rate, |
||||
|
$price, |
||||
|
$basis = FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
) { |
||||
|
$settlement = Functions::flattenSingleValue($settlement); |
||||
|
$maturity = Functions::flattenSingleValue($maturity); |
||||
|
$issue = Functions::flattenSingleValue($issue); |
||||
|
$rate = Functions::flattenSingleValue($rate); |
||||
|
$price = Functions::flattenSingleValue($price); |
||||
|
$basis = ($basis === null) |
||||
|
? FinancialConstants::BASIS_DAYS_PER_YEAR_NASD |
||||
|
: Functions::flattenSingleValue($basis); |
||||
|
|
||||
|
try { |
||||
|
$settlement = SecurityValidations::validateSettlementDate($settlement); |
||||
|
$maturity = SecurityValidations::validateMaturityDate($maturity); |
||||
|
SecurityValidations::validateSecurityPeriod($settlement, $maturity); |
||||
|
$issue = SecurityValidations::validateIssueDate($issue); |
||||
|
$rate = SecurityValidations::validateRate($rate); |
||||
|
$price = SecurityValidations::validatePrice($price); |
||||
|
$basis = SecurityValidations::validateBasis($basis); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$daysPerYear = Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis); |
||||
|
if (!is_numeric($daysPerYear)) { |
||||
|
return $daysPerYear; |
||||
|
} |
||||
|
$daysBetweenIssueAndSettlement = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $settlement, $basis)); |
||||
|
if (!is_numeric($daysBetweenIssueAndSettlement)) { |
||||
|
// return date error |
||||
|
return $daysBetweenIssueAndSettlement; |
||||
|
} |
||||
|
$daysBetweenIssueAndSettlement *= $daysPerYear; |
||||
|
$daysBetweenIssueAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $maturity, $basis)); |
||||
|
if (!is_numeric($daysBetweenIssueAndMaturity)) { |
||||
|
// return date error |
||||
|
return $daysBetweenIssueAndMaturity; |
||||
|
} |
||||
|
$daysBetweenIssueAndMaturity *= $daysPerYear; |
||||
|
$daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); |
||||
|
if (!is_numeric($daysBetweenSettlementAndMaturity)) { |
||||
|
// return date error |
||||
|
return $daysBetweenSettlementAndMaturity; |
||||
|
} |
||||
|
$daysBetweenSettlementAndMaturity *= $daysPerYear; |
||||
|
|
||||
|
return ((1 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate) - |
||||
|
(($price / 100) + (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate))) / |
||||
|
(($price / 100) + (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate))) * |
||||
|
($daysPerYear / $daysBetweenSettlementAndMaturity); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,147 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class TreasuryBill |
||||
|
{ |
||||
|
/** |
||||
|
* TBILLEQ. |
||||
|
* |
||||
|
* Returns the bond-equivalent yield for a Treasury bill. |
||||
|
* |
||||
|
* @param mixed $settlement The Treasury bill's settlement date. |
||||
|
* The Treasury bill's settlement date is the date after the issue date |
||||
|
* when the Treasury bill is traded to the buyer. |
||||
|
* @param mixed $maturity The Treasury bill's maturity date. |
||||
|
* The maturity date is the date when the Treasury bill expires. |
||||
|
* @param mixed $discount The Treasury bill's discount rate |
||||
|
* |
||||
|
* @return float|string Result, or a string containing an error |
||||
|
*/ |
||||
|
public static function bondEquivalentYield($settlement, $maturity, $discount) |
||||
|
{ |
||||
|
$settlement = Functions::flattenSingleValue($settlement); |
||||
|
$maturity = Functions::flattenSingleValue($maturity); |
||||
|
$discount = Functions::flattenSingleValue($discount); |
||||
|
|
||||
|
try { |
||||
|
$settlement = FinancialValidations::validateSettlementDate($settlement); |
||||
|
$maturity = FinancialValidations::validateMaturityDate($maturity); |
||||
|
$discount = FinancialValidations::validateFloat($discount); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if ($discount <= 0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
$daysBetweenSettlementAndMaturity = $maturity - $settlement; |
||||
|
$daysPerYear = Helpers::daysPerYear( |
||||
|
Functions::scalar(DateTimeExcel\DateParts::year($maturity)), |
||||
|
FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL |
||||
|
); |
||||
|
|
||||
|
if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
return (365 * $discount) / (360 - $discount * $daysBetweenSettlementAndMaturity); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TBILLPRICE. |
||||
|
* |
||||
|
* Returns the price per $100 face value for a Treasury bill. |
||||
|
* |
||||
|
* @param mixed $settlement The Treasury bill's settlement date. |
||||
|
* The Treasury bill's settlement date is the date after the issue date |
||||
|
* when the Treasury bill is traded to the buyer. |
||||
|
* @param mixed $maturity The Treasury bill's maturity date. |
||||
|
* The maturity date is the date when the Treasury bill expires. |
||||
|
* @param mixed $discount The Treasury bill's discount rate |
||||
|
* |
||||
|
* @return float|string Result, or a string containing an error |
||||
|
*/ |
||||
|
public static function price($settlement, $maturity, $discount) |
||||
|
{ |
||||
|
$settlement = Functions::flattenSingleValue($settlement); |
||||
|
$maturity = Functions::flattenSingleValue($maturity); |
||||
|
$discount = Functions::flattenSingleValue($discount); |
||||
|
|
||||
|
try { |
||||
|
$settlement = FinancialValidations::validateSettlementDate($settlement); |
||||
|
$maturity = FinancialValidations::validateMaturityDate($maturity); |
||||
|
$discount = FinancialValidations::validateFloat($discount); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if ($discount <= 0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
$daysBetweenSettlementAndMaturity = $maturity - $settlement; |
||||
|
$daysPerYear = Helpers::daysPerYear( |
||||
|
Functions::scalar(DateTimeExcel\DateParts::year($maturity)), |
||||
|
FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL |
||||
|
); |
||||
|
|
||||
|
if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
$price = 100 * (1 - (($discount * $daysBetweenSettlementAndMaturity) / 360)); |
||||
|
if ($price < 0.0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
return $price; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TBILLYIELD. |
||||
|
* |
||||
|
* Returns the yield for a Treasury bill. |
||||
|
* |
||||
|
* @param mixed $settlement The Treasury bill's settlement date. |
||||
|
* The Treasury bill's settlement date is the date after the issue date when |
||||
|
* the Treasury bill is traded to the buyer. |
||||
|
* @param mixed $maturity The Treasury bill's maturity date. |
||||
|
* The maturity date is the date when the Treasury bill expires. |
||||
|
* @param mixed $price The Treasury bill's price per $100 face value |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function yield($settlement, $maturity, $price) |
||||
|
{ |
||||
|
$settlement = Functions::flattenSingleValue($settlement); |
||||
|
$maturity = Functions::flattenSingleValue($maturity); |
||||
|
$price = Functions::flattenSingleValue($price); |
||||
|
|
||||
|
try { |
||||
|
$settlement = FinancialValidations::validateSettlementDate($settlement); |
||||
|
$maturity = FinancialValidations::validateMaturityDate($maturity); |
||||
|
$price = FinancialValidations::validatePrice($price); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$daysBetweenSettlementAndMaturity = $maturity - $settlement; |
||||
|
$daysPerYear = Helpers::daysPerYear( |
||||
|
Functions::scalar(DateTimeExcel\DateParts::year($maturity)), |
||||
|
FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL |
||||
|
); |
||||
|
|
||||
|
if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
return ((100 - $price) / $price) * (360 / $daysBetweenSettlementAndMaturity); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Internal; |
||||
|
|
||||
|
class WildcardMatch |
||||
|
{ |
||||
|
private const SEARCH_SET = [ |
||||
|
'~~', // convert double tilde to unprintable value |
||||
|
'~\\*', // convert tilde backslash asterisk to [*] (matches literal asterisk in regexp) |
||||
|
'\\*', // convert backslash asterisk to .* (matches string of any length in regexp) |
||||
|
'~\\?', // convert tilde backslash question to [?] (matches literal question mark in regexp) |
||||
|
'\\?', // convert backslash question to . (matches one character in regexp) |
||||
|
"\x1c", // convert original double tilde to single tilde |
||||
|
]; |
||||
|
|
||||
|
private const REPLACEMENT_SET = [ |
||||
|
"\x1c", |
||||
|
'[*]', |
||||
|
'.*', |
||||
|
'[?]', |
||||
|
'.', |
||||
|
'~', |
||||
|
]; |
||||
|
|
||||
|
public static function wildcard(string $wildcard): string |
||||
|
{ |
||||
|
// Preg Escape the wildcard, but protecting the Excel * and ? search characters |
||||
|
return str_replace(self::SEARCH_SET, self::REPLACEMENT_SET, preg_quote($wildcard, '/')); |
||||
|
} |
||||
|
|
||||
|
public static function compare(?string $value, string $wildcard): bool |
||||
|
{ |
||||
|
if ($value === '' || $value === null) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return (bool) preg_match("/^{$wildcard}\$/mui", $value); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,209 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Calculation; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Cell\Cell; |
||||
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; |
||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; |
||||
|
|
||||
|
class RowColumnInformation |
||||
|
{ |
||||
|
/** |
||||
|
* Test if cellAddress is null or whitespace string. |
||||
|
* |
||||
|
* @param null|array|string $cellAddress A reference to a range of cells |
||||
|
*/ |
||||
|
private static function cellAddressNullOrWhitespace($cellAddress): bool |
||||
|
{ |
||||
|
return $cellAddress === null || (!is_array($cellAddress) && trim($cellAddress) === ''); |
||||
|
} |
||||
|
|
||||
|
private static function cellColumn(?Cell $cell): int |
||||
|
{ |
||||
|
return ($cell !== null) ? (int) Coordinate::columnIndexFromString($cell->getColumn()) : 1; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* COLUMN. |
||||
|
* |
||||
|
* Returns the column number of the given cell reference |
||||
|
* If the cell reference is a range of cells, COLUMN returns the column numbers of each column |
||||
|
* in the reference as a horizontal array. |
||||
|
* If cell reference is omitted, and the function is being called through the calculation engine, |
||||
|
* then it is assumed to be the reference of the cell in which the COLUMN function appears; |
||||
|
* otherwise this function returns 1. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* =COLUMN([cellAddress]) |
||||
|
* |
||||
|
* @param null|array|string $cellAddress A reference to a range of cells for which you want the column numbers |
||||
|
* |
||||
|
* @return int|int[] |
||||
|
*/ |
||||
|
public static function COLUMN($cellAddress = null, ?Cell $cell = null) |
||||
|
{ |
||||
|
if (self::cellAddressNullOrWhitespace($cellAddress)) { |
||||
|
return self::cellColumn($cell); |
||||
|
} |
||||
|
|
||||
|
if (is_array($cellAddress)) { |
||||
|
foreach ($cellAddress as $columnKey => $value) { |
||||
|
$columnKey = preg_replace('/[^a-z]/i', '', $columnKey); |
||||
|
|
||||
|
return (int) Coordinate::columnIndexFromString($columnKey); |
||||
|
} |
||||
|
|
||||
|
return self::cellColumn($cell); |
||||
|
} |
||||
|
|
||||
|
$cellAddress = $cellAddress ?? ''; |
||||
|
if ($cell != null) { |
||||
|
[,, $sheetName] = Helpers::extractWorksheet($cellAddress, $cell); |
||||
|
[,, $cellAddress] = Helpers::extractCellAddresses($cellAddress, true, $cell->getWorksheet(), $sheetName); |
||||
|
} |
||||
|
[, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); |
||||
|
if (strpos($cellAddress, ':') !== false) { |
||||
|
[$startAddress, $endAddress] = explode(':', $cellAddress); |
||||
|
$startAddress = preg_replace('/[^a-z]/i', '', $startAddress); |
||||
|
$endAddress = preg_replace('/[^a-z]/i', '', $endAddress); |
||||
|
|
||||
|
return range( |
||||
|
(int) Coordinate::columnIndexFromString($startAddress), |
||||
|
(int) Coordinate::columnIndexFromString($endAddress) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
$cellAddress = preg_replace('/[^a-z]/i', '', $cellAddress); |
||||
|
|
||||
|
return (int) Coordinate::columnIndexFromString($cellAddress); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* COLUMNS. |
||||
|
* |
||||
|
* Returns the number of columns in an array or reference. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* =COLUMNS(cellAddress) |
||||
|
* |
||||
|
* @param null|array|string $cellAddress An array or array formula, or a reference to a range of cells |
||||
|
* for which you want the number of columns |
||||
|
* |
||||
|
* @return int|string The number of columns in cellAddress, or a string if arguments are invalid |
||||
|
*/ |
||||
|
public static function COLUMNS($cellAddress = null) |
||||
|
{ |
||||
|
if (self::cellAddressNullOrWhitespace($cellAddress)) { |
||||
|
return 1; |
||||
|
} |
||||
|
if (!is_array($cellAddress)) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
reset($cellAddress); |
||||
|
$isMatrix = (is_numeric(key($cellAddress))); |
||||
|
[$columns, $rows] = Calculation::getMatrixDimensions($cellAddress); |
||||
|
|
||||
|
if ($isMatrix) { |
||||
|
return $rows; |
||||
|
} |
||||
|
|
||||
|
return $columns; |
||||
|
} |
||||
|
|
||||
|
private static function cellRow(?Cell $cell): int |
||||
|
{ |
||||
|
return ($cell !== null) ? $cell->getRow() : 1; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ROW. |
||||
|
* |
||||
|
* Returns the row number of the given cell reference |
||||
|
* If the cell reference is a range of cells, ROW returns the row numbers of each row in the reference |
||||
|
* as a vertical array. |
||||
|
* If cell reference is omitted, and the function is being called through the calculation engine, |
||||
|
* then it is assumed to be the reference of the cell in which the ROW function appears; |
||||
|
* otherwise this function returns 1. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* =ROW([cellAddress]) |
||||
|
* |
||||
|
* @param null|array|string $cellAddress A reference to a range of cells for which you want the row numbers |
||||
|
* |
||||
|
* @return int|mixed[]|string |
||||
|
*/ |
||||
|
public static function ROW($cellAddress = null, ?Cell $cell = null) |
||||
|
{ |
||||
|
if (self::cellAddressNullOrWhitespace($cellAddress)) { |
||||
|
return self::cellRow($cell); |
||||
|
} |
||||
|
|
||||
|
if (is_array($cellAddress)) { |
||||
|
foreach ($cellAddress as $rowKey => $rowValue) { |
||||
|
foreach ($rowValue as $columnKey => $cellValue) { |
||||
|
return (int) preg_replace('/\D/', '', $rowKey); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return self::cellRow($cell); |
||||
|
} |
||||
|
|
||||
|
$cellAddress = $cellAddress ?? ''; |
||||
|
if ($cell !== null) { |
||||
|
[,, $sheetName] = Helpers::extractWorksheet($cellAddress, $cell); |
||||
|
[,, $cellAddress] = Helpers::extractCellAddresses($cellAddress, true, $cell->getWorksheet(), $sheetName); |
||||
|
} |
||||
|
[, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); |
||||
|
if (strpos($cellAddress, ':') !== false) { |
||||
|
[$startAddress, $endAddress] = explode(':', $cellAddress); |
||||
|
$startAddress = preg_replace('/\D/', '', $startAddress); |
||||
|
$endAddress = preg_replace('/\D/', '', $endAddress); |
||||
|
|
||||
|
return array_map( |
||||
|
function ($value) { |
||||
|
return [$value]; |
||||
|
}, |
||||
|
range($startAddress, $endAddress) |
||||
|
); |
||||
|
} |
||||
|
[$cellAddress] = explode(':', $cellAddress); |
||||
|
|
||||
|
return (int) preg_replace('/\D/', '', $cellAddress); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ROWS. |
||||
|
* |
||||
|
* Returns the number of rows in an array or reference. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* =ROWS(cellAddress) |
||||
|
* |
||||
|
* @param null|array|string $cellAddress An array or array formula, or a reference to a range of cells |
||||
|
* for which you want the number of rows |
||||
|
* |
||||
|
* @return int|string The number of rows in cellAddress, or a string if arguments are invalid |
||||
|
*/ |
||||
|
public static function ROWS($cellAddress = null) |
||||
|
{ |
||||
|
if (self::cellAddressNullOrWhitespace($cellAddress)) { |
||||
|
return 1; |
||||
|
} |
||||
|
if (!is_array($cellAddress)) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
reset($cellAddress); |
||||
|
$isMatrix = (is_numeric(key($cellAddress))); |
||||
|
[$columns, $rows] = Calculation::getMatrixDimensions($cellAddress); |
||||
|
|
||||
|
if ($isMatrix) { |
||||
|
return $columns; |
||||
|
} |
||||
|
|
||||
|
return $rows; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Selection |
||||
|
{ |
||||
|
/** |
||||
|
* CHOOSE. |
||||
|
* |
||||
|
* Uses lookup_value to return a value from the list of value arguments. |
||||
|
* Use CHOOSE to select one of up to 254 values based on the lookup_value. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* =CHOOSE(index_num, value1, [value2], ...) |
||||
|
* |
||||
|
* @param mixed ...$chooseArgs Data values |
||||
|
* |
||||
|
* @return mixed The selected value |
||||
|
*/ |
||||
|
public static function choose(...$chooseArgs) |
||||
|
{ |
||||
|
$chosenEntry = Functions::flattenArray(array_shift($chooseArgs)); |
||||
|
$entryCount = count($chooseArgs) - 1; |
||||
|
|
||||
|
if (is_array($chosenEntry)) { |
||||
|
$chosenEntry = array_shift($chosenEntry); |
||||
|
} |
||||
|
if (is_numeric($chosenEntry)) { |
||||
|
--$chosenEntry; |
||||
|
} else { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
$chosenEntry = floor($chosenEntry); |
||||
|
if (($chosenEntry < 0) || ($chosenEntry > $entryCount)) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
if (is_array($chooseArgs[$chosenEntry])) { |
||||
|
return Functions::flattenArray($chooseArgs[$chosenEntry]); |
||||
|
} |
||||
|
|
||||
|
return $chooseArgs[$chosenEntry]; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,105 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; |
||||
|
|
||||
|
class VLookup extends LookupBase |
||||
|
{ |
||||
|
/** |
||||
|
* VLOOKUP |
||||
|
* The VLOOKUP function searches for value in the left-most column of lookup_array and returns the value |
||||
|
* in the same row based on the index_number. |
||||
|
* |
||||
|
* @param mixed $lookupValue The value that you want to match in lookup_array |
||||
|
* @param mixed $lookupArray The range of cells being searched |
||||
|
* @param mixed $indexNumber The column number in table_array from which the matching value must be returned. |
||||
|
* The first column is 1. |
||||
|
* @param mixed $notExactMatch determines if you are looking for an exact match based on lookup_value |
||||
|
* |
||||
|
* @return mixed The value of the found cell |
||||
|
*/ |
||||
|
public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true) |
||||
|
{ |
||||
|
$lookupValue = Functions::flattenSingleValue($lookupValue); |
||||
|
$indexNumber = Functions::flattenSingleValue($indexNumber); |
||||
|
$notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch); |
||||
|
|
||||
|
try { |
||||
|
$indexNumber = self::validateIndexLookup($lookupArray, $indexNumber); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$f = array_keys($lookupArray); |
||||
|
$firstRow = array_pop($f); |
||||
|
if ((!is_array($lookupArray[$firstRow])) || ($indexNumber > count($lookupArray[$firstRow]))) { |
||||
|
return Functions::REF(); |
||||
|
} |
||||
|
$columnKeys = array_keys($lookupArray[$firstRow]); |
||||
|
$returnColumn = $columnKeys[--$indexNumber]; |
||||
|
$firstColumn = array_shift($columnKeys); |
||||
|
|
||||
|
if (!$notExactMatch) { |
||||
|
uasort($lookupArray, ['self', 'vlookupSort']); |
||||
|
} |
||||
|
|
||||
|
$rowNumber = self::vLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch); |
||||
|
|
||||
|
if ($rowNumber !== null) { |
||||
|
// return the appropriate value |
||||
|
return $lookupArray[$rowNumber][$returnColumn]; |
||||
|
} |
||||
|
|
||||
|
return Functions::NA(); |
||||
|
} |
||||
|
|
||||
|
private static function vlookupSort($a, $b) |
||||
|
{ |
||||
|
reset($a); |
||||
|
$firstColumn = key($a); |
||||
|
$aLower = StringHelper::strToLower($a[$firstColumn]); |
||||
|
$bLower = StringHelper::strToLower($b[$firstColumn]); |
||||
|
|
||||
|
if ($aLower == $bLower) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
return ($aLower < $bLower) ? -1 : 1; |
||||
|
} |
||||
|
|
||||
|
private static function vLookupSearch($lookupValue, $lookupArray, $column, $notExactMatch) |
||||
|
{ |
||||
|
$lookupLower = StringHelper::strToLower($lookupValue); |
||||
|
|
||||
|
$rowNumber = null; |
||||
|
foreach ($lookupArray as $rowKey => $rowData) { |
||||
|
$bothNumeric = is_numeric($lookupValue) && is_numeric($rowData[$column]); |
||||
|
$bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData[$column]); |
||||
|
$cellDataLower = StringHelper::strToLower($rowData[$column]); |
||||
|
|
||||
|
// break if we have passed possible keys |
||||
|
if ( |
||||
|
$notExactMatch && |
||||
|
(($bothNumeric && ($rowData[$column] > $lookupValue)) || |
||||
|
($bothNotNumeric && ($cellDataLower > $lookupLower))) |
||||
|
) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
$rowNumber = self::checkMatch( |
||||
|
$bothNumeric, |
||||
|
$bothNotNumeric, |
||||
|
$notExactMatch, |
||||
|
$rowKey, |
||||
|
$cellDataLower, |
||||
|
$lookupLower, |
||||
|
$rowNumber |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return $rowNumber; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,99 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Random |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* RAND. |
||||
|
* |
||||
|
* @return float Random number |
||||
|
*/ |
||||
|
public static function rand() |
||||
|
{ |
||||
|
return (mt_rand(0, 10000000)) / 10000000; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* RANDBETWEEN. |
||||
|
* |
||||
|
* @param mixed $min Minimal value |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $max Maximal value |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|float|int|string Random number |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function randBetween($min, $max) |
||||
|
{ |
||||
|
if (is_array($min) || is_array($max)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $min, $max); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$min = (int) Helpers::validateNumericNullBool($min); |
||||
|
$max = (int) Helpers::validateNumericNullBool($max); |
||||
|
Helpers::validateNotNegative($max - $min); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return mt_rand($min, $max); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* RANDARRAY. |
||||
|
* |
||||
|
* Generates a list of sequential numbers in an array. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* RANDARRAY([rows],[columns],[start],[step]) |
||||
|
* |
||||
|
* @param mixed $rows the number of rows to return, defaults to 1 |
||||
|
* @param mixed $columns the number of columns to return, defaults to 1 |
||||
|
* @param mixed $min the minimum number to be returned, defaults to 0 |
||||
|
* @param mixed $max the maximum number to be returned, defaults to 1 |
||||
|
* @param bool $wholeNumber the type of numbers to return: |
||||
|
* False - Decimal numbers to 15 decimal places. (default) |
||||
|
* True - Whole (integer) numbers |
||||
|
* |
||||
|
* @return array|string The resulting array, or a string containing an error |
||||
|
*/ |
||||
|
public static function randArray($rows = 1, $columns = 1, $min = 0, $max = 1, $wholeNumber = false) |
||||
|
{ |
||||
|
try { |
||||
|
$rows = (int) Helpers::validateNumericNullSubstitution($rows, 1); |
||||
|
Helpers::validatePositive($rows); |
||||
|
$columns = (int) Helpers::validateNumericNullSubstitution($columns, 1); |
||||
|
Helpers::validatePositive($columns); |
||||
|
$min = Helpers::validateNumericNullSubstitution($min, 1); |
||||
|
$max = Helpers::validateNumericNullSubstitution($max, 1); |
||||
|
|
||||
|
if ($max <= $min) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return array_chunk( |
||||
|
array_map( |
||||
|
function () use ($min, $max, $wholeNumber) { |
||||
|
return $wholeNumber |
||||
|
? mt_rand((int) $min, (int) $max) |
||||
|
: (mt_rand() / mt_getrandmax()) * ($max - $min) + $min; |
||||
|
}, |
||||
|
array_fill(0, $rows * $columns, $min) |
||||
|
), |
||||
|
max($columns, 1) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,846 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Roman |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
private const VALUES = [ |
||||
|
45 => ['VL'], |
||||
|
46 => ['VLI'], |
||||
|
47 => ['VLII'], |
||||
|
48 => ['VLIII'], |
||||
|
49 => ['VLIV', 'IL'], |
||||
|
95 => ['VC'], |
||||
|
96 => ['VCI'], |
||||
|
97 => ['VCII'], |
||||
|
98 => ['VCIII'], |
||||
|
99 => ['VCIV', 'IC'], |
||||
|
145 => ['CVL'], |
||||
|
146 => ['CVLI'], |
||||
|
147 => ['CVLII'], |
||||
|
148 => ['CVLIII'], |
||||
|
149 => ['CVLIV', 'CIL'], |
||||
|
195 => ['CVC'], |
||||
|
196 => ['CVCI'], |
||||
|
197 => ['CVCII'], |
||||
|
198 => ['CVCIII'], |
||||
|
199 => ['CVCIV', 'CIC'], |
||||
|
245 => ['CCVL'], |
||||
|
246 => ['CCVLI'], |
||||
|
247 => ['CCVLII'], |
||||
|
248 => ['CCVLIII'], |
||||
|
249 => ['CCVLIV', 'CCIL'], |
||||
|
295 => ['CCVC'], |
||||
|
296 => ['CCVCI'], |
||||
|
297 => ['CCVCII'], |
||||
|
298 => ['CCVCIII'], |
||||
|
299 => ['CCVCIV', 'CCIC'], |
||||
|
345 => ['CCCVL'], |
||||
|
346 => ['CCCVLI'], |
||||
|
347 => ['CCCVLII'], |
||||
|
348 => ['CCCVLIII'], |
||||
|
349 => ['CCCVLIV', 'CCCIL'], |
||||
|
395 => ['CCCVC'], |
||||
|
396 => ['CCCVCI'], |
||||
|
397 => ['CCCVCII'], |
||||
|
398 => ['CCCVCIII'], |
||||
|
399 => ['CCCVCIV', 'CCCIC'], |
||||
|
445 => ['CDVL'], |
||||
|
446 => ['CDVLI'], |
||||
|
447 => ['CDVLII'], |
||||
|
448 => ['CDVLIII'], |
||||
|
449 => ['CDVLIV', 'CDIL'], |
||||
|
450 => ['LD'], |
||||
|
451 => ['LDI'], |
||||
|
452 => ['LDII'], |
||||
|
453 => ['LDIII'], |
||||
|
454 => ['LDIV'], |
||||
|
455 => ['LDV'], |
||||
|
456 => ['LDVI'], |
||||
|
457 => ['LDVII'], |
||||
|
458 => ['LDVIII'], |
||||
|
459 => ['LDIX'], |
||||
|
460 => ['LDX'], |
||||
|
461 => ['LDXI'], |
||||
|
462 => ['LDXII'], |
||||
|
463 => ['LDXIII'], |
||||
|
464 => ['LDXIV'], |
||||
|
465 => ['LDXV'], |
||||
|
466 => ['LDXVI'], |
||||
|
467 => ['LDXVII'], |
||||
|
468 => ['LDXVIII'], |
||||
|
469 => ['LDXIX'], |
||||
|
470 => ['LDXX'], |
||||
|
471 => ['LDXXI'], |
||||
|
472 => ['LDXXII'], |
||||
|
473 => ['LDXXIII'], |
||||
|
474 => ['LDXXIV'], |
||||
|
475 => ['LDXXV'], |
||||
|
476 => ['LDXXVI'], |
||||
|
477 => ['LDXXVII'], |
||||
|
478 => ['LDXXVIII'], |
||||
|
479 => ['LDXXIX'], |
||||
|
480 => ['LDXXX'], |
||||
|
481 => ['LDXXXI'], |
||||
|
482 => ['LDXXXII'], |
||||
|
483 => ['LDXXXIII'], |
||||
|
484 => ['LDXXXIV'], |
||||
|
485 => ['LDXXXV'], |
||||
|
486 => ['LDXXXVI'], |
||||
|
487 => ['LDXXXVII'], |
||||
|
488 => ['LDXXXVIII'], |
||||
|
489 => ['LDXXXIX'], |
||||
|
490 => ['LDXL', 'XD'], |
||||
|
491 => ['LDXLI', 'XDI'], |
||||
|
492 => ['LDXLII', 'XDII'], |
||||
|
493 => ['LDXLIII', 'XDIII'], |
||||
|
494 => ['LDXLIV', 'XDIV'], |
||||
|
495 => ['LDVL', 'XDV', 'VD'], |
||||
|
496 => ['LDVLI', 'XDVI', 'VDI'], |
||||
|
497 => ['LDVLII', 'XDVII', 'VDII'], |
||||
|
498 => ['LDVLIII', 'XDVIII', 'VDIII'], |
||||
|
499 => ['LDVLIV', 'XDIX', 'VDIV', 'ID'], |
||||
|
545 => ['DVL'], |
||||
|
546 => ['DVLI'], |
||||
|
547 => ['DVLII'], |
||||
|
548 => ['DVLIII'], |
||||
|
549 => ['DVLIV', 'DIL'], |
||||
|
595 => ['DVC'], |
||||
|
596 => ['DVCI'], |
||||
|
597 => ['DVCII'], |
||||
|
598 => ['DVCIII'], |
||||
|
599 => ['DVCIV', 'DIC'], |
||||
|
645 => ['DCVL'], |
||||
|
646 => ['DCVLI'], |
||||
|
647 => ['DCVLII'], |
||||
|
648 => ['DCVLIII'], |
||||
|
649 => ['DCVLIV', 'DCIL'], |
||||
|
695 => ['DCVC'], |
||||
|
696 => ['DCVCI'], |
||||
|
697 => ['DCVCII'], |
||||
|
698 => ['DCVCIII'], |
||||
|
699 => ['DCVCIV', 'DCIC'], |
||||
|
745 => ['DCCVL'], |
||||
|
746 => ['DCCVLI'], |
||||
|
747 => ['DCCVLII'], |
||||
|
748 => ['DCCVLIII'], |
||||
|
749 => ['DCCVLIV', 'DCCIL'], |
||||
|
795 => ['DCCVC'], |
||||
|
796 => ['DCCVCI'], |
||||
|
797 => ['DCCVCII'], |
||||
|
798 => ['DCCVCIII'], |
||||
|
799 => ['DCCVCIV', 'DCCIC'], |
||||
|
845 => ['DCCCVL'], |
||||
|
846 => ['DCCCVLI'], |
||||
|
847 => ['DCCCVLII'], |
||||
|
848 => ['DCCCVLIII'], |
||||
|
849 => ['DCCCVLIV', 'DCCCIL'], |
||||
|
895 => ['DCCCVC'], |
||||
|
896 => ['DCCCVCI'], |
||||
|
897 => ['DCCCVCII'], |
||||
|
898 => ['DCCCVCIII'], |
||||
|
899 => ['DCCCVCIV', 'DCCCIC'], |
||||
|
945 => ['CMVL'], |
||||
|
946 => ['CMVLI'], |
||||
|
947 => ['CMVLII'], |
||||
|
948 => ['CMVLIII'], |
||||
|
949 => ['CMVLIV', 'CMIL'], |
||||
|
950 => ['LM'], |
||||
|
951 => ['LMI'], |
||||
|
952 => ['LMII'], |
||||
|
953 => ['LMIII'], |
||||
|
954 => ['LMIV'], |
||||
|
955 => ['LMV'], |
||||
|
956 => ['LMVI'], |
||||
|
957 => ['LMVII'], |
||||
|
958 => ['LMVIII'], |
||||
|
959 => ['LMIX'], |
||||
|
960 => ['LMX'], |
||||
|
961 => ['LMXI'], |
||||
|
962 => ['LMXII'], |
||||
|
963 => ['LMXIII'], |
||||
|
964 => ['LMXIV'], |
||||
|
965 => ['LMXV'], |
||||
|
966 => ['LMXVI'], |
||||
|
967 => ['LMXVII'], |
||||
|
968 => ['LMXVIII'], |
||||
|
969 => ['LMXIX'], |
||||
|
970 => ['LMXX'], |
||||
|
971 => ['LMXXI'], |
||||
|
972 => ['LMXXII'], |
||||
|
973 => ['LMXXIII'], |
||||
|
974 => ['LMXXIV'], |
||||
|
975 => ['LMXXV'], |
||||
|
976 => ['LMXXVI'], |
||||
|
977 => ['LMXXVII'], |
||||
|
978 => ['LMXXVIII'], |
||||
|
979 => ['LMXXIX'], |
||||
|
980 => ['LMXXX'], |
||||
|
981 => ['LMXXXI'], |
||||
|
982 => ['LMXXXII'], |
||||
|
983 => ['LMXXXIII'], |
||||
|
984 => ['LMXXXIV'], |
||||
|
985 => ['LMXXXV'], |
||||
|
986 => ['LMXXXVI'], |
||||
|
987 => ['LMXXXVII'], |
||||
|
988 => ['LMXXXVIII'], |
||||
|
989 => ['LMXXXIX'], |
||||
|
990 => ['LMXL', 'XM'], |
||||
|
991 => ['LMXLI', 'XMI'], |
||||
|
992 => ['LMXLII', 'XMII'], |
||||
|
993 => ['LMXLIII', 'XMIII'], |
||||
|
994 => ['LMXLIV', 'XMIV'], |
||||
|
995 => ['LMVL', 'XMV', 'VM'], |
||||
|
996 => ['LMVLI', 'XMVI', 'VMI'], |
||||
|
997 => ['LMVLII', 'XMVII', 'VMII'], |
||||
|
998 => ['LMVLIII', 'XMVIII', 'VMIII'], |
||||
|
999 => ['LMVLIV', 'XMIX', 'VMIV', 'IM'], |
||||
|
1045 => ['MVL'], |
||||
|
1046 => ['MVLI'], |
||||
|
1047 => ['MVLII'], |
||||
|
1048 => ['MVLIII'], |
||||
|
1049 => ['MVLIV', 'MIL'], |
||||
|
1095 => ['MVC'], |
||||
|
1096 => ['MVCI'], |
||||
|
1097 => ['MVCII'], |
||||
|
1098 => ['MVCIII'], |
||||
|
1099 => ['MVCIV', 'MIC'], |
||||
|
1145 => ['MCVL'], |
||||
|
1146 => ['MCVLI'], |
||||
|
1147 => ['MCVLII'], |
||||
|
1148 => ['MCVLIII'], |
||||
|
1149 => ['MCVLIV', 'MCIL'], |
||||
|
1195 => ['MCVC'], |
||||
|
1196 => ['MCVCI'], |
||||
|
1197 => ['MCVCII'], |
||||
|
1198 => ['MCVCIII'], |
||||
|
1199 => ['MCVCIV', 'MCIC'], |
||||
|
1245 => ['MCCVL'], |
||||
|
1246 => ['MCCVLI'], |
||||
|
1247 => ['MCCVLII'], |
||||
|
1248 => ['MCCVLIII'], |
||||
|
1249 => ['MCCVLIV', 'MCCIL'], |
||||
|
1295 => ['MCCVC'], |
||||
|
1296 => ['MCCVCI'], |
||||
|
1297 => ['MCCVCII'], |
||||
|
1298 => ['MCCVCIII'], |
||||
|
1299 => ['MCCVCIV', 'MCCIC'], |
||||
|
1345 => ['MCCCVL'], |
||||
|
1346 => ['MCCCVLI'], |
||||
|
1347 => ['MCCCVLII'], |
||||
|
1348 => ['MCCCVLIII'], |
||||
|
1349 => ['MCCCVLIV', 'MCCCIL'], |
||||
|
1395 => ['MCCCVC'], |
||||
|
1396 => ['MCCCVCI'], |
||||
|
1397 => ['MCCCVCII'], |
||||
|
1398 => ['MCCCVCIII'], |
||||
|
1399 => ['MCCCVCIV', 'MCCCIC'], |
||||
|
1445 => ['MCDVL'], |
||||
|
1446 => ['MCDVLI'], |
||||
|
1447 => ['MCDVLII'], |
||||
|
1448 => ['MCDVLIII'], |
||||
|
1449 => ['MCDVLIV', 'MCDIL'], |
||||
|
1450 => ['MLD'], |
||||
|
1451 => ['MLDI'], |
||||
|
1452 => ['MLDII'], |
||||
|
1453 => ['MLDIII'], |
||||
|
1454 => ['MLDIV'], |
||||
|
1455 => ['MLDV'], |
||||
|
1456 => ['MLDVI'], |
||||
|
1457 => ['MLDVII'], |
||||
|
1458 => ['MLDVIII'], |
||||
|
1459 => ['MLDIX'], |
||||
|
1460 => ['MLDX'], |
||||
|
1461 => ['MLDXI'], |
||||
|
1462 => ['MLDXII'], |
||||
|
1463 => ['MLDXIII'], |
||||
|
1464 => ['MLDXIV'], |
||||
|
1465 => ['MLDXV'], |
||||
|
1466 => ['MLDXVI'], |
||||
|
1467 => ['MLDXVII'], |
||||
|
1468 => ['MLDXVIII'], |
||||
|
1469 => ['MLDXIX'], |
||||
|
1470 => ['MLDXX'], |
||||
|
1471 => ['MLDXXI'], |
||||
|
1472 => ['MLDXXII'], |
||||
|
1473 => ['MLDXXIII'], |
||||
|
1474 => ['MLDXXIV'], |
||||
|
1475 => ['MLDXXV'], |
||||
|
1476 => ['MLDXXVI'], |
||||
|
1477 => ['MLDXXVII'], |
||||
|
1478 => ['MLDXXVIII'], |
||||
|
1479 => ['MLDXXIX'], |
||||
|
1480 => ['MLDXXX'], |
||||
|
1481 => ['MLDXXXI'], |
||||
|
1482 => ['MLDXXXII'], |
||||
|
1483 => ['MLDXXXIII'], |
||||
|
1484 => ['MLDXXXIV'], |
||||
|
1485 => ['MLDXXXV'], |
||||
|
1486 => ['MLDXXXVI'], |
||||
|
1487 => ['MLDXXXVII'], |
||||
|
1488 => ['MLDXXXVIII'], |
||||
|
1489 => ['MLDXXXIX'], |
||||
|
1490 => ['MLDXL', 'MXD'], |
||||
|
1491 => ['MLDXLI', 'MXDI'], |
||||
|
1492 => ['MLDXLII', 'MXDII'], |
||||
|
1493 => ['MLDXLIII', 'MXDIII'], |
||||
|
1494 => ['MLDXLIV', 'MXDIV'], |
||||
|
1495 => ['MLDVL', 'MXDV', 'MVD'], |
||||
|
1496 => ['MLDVLI', 'MXDVI', 'MVDI'], |
||||
|
1497 => ['MLDVLII', 'MXDVII', 'MVDII'], |
||||
|
1498 => ['MLDVLIII', 'MXDVIII', 'MVDIII'], |
||||
|
1499 => ['MLDVLIV', 'MXDIX', 'MVDIV', 'MID'], |
||||
|
1545 => ['MDVL'], |
||||
|
1546 => ['MDVLI'], |
||||
|
1547 => ['MDVLII'], |
||||
|
1548 => ['MDVLIII'], |
||||
|
1549 => ['MDVLIV', 'MDIL'], |
||||
|
1595 => ['MDVC'], |
||||
|
1596 => ['MDVCI'], |
||||
|
1597 => ['MDVCII'], |
||||
|
1598 => ['MDVCIII'], |
||||
|
1599 => ['MDVCIV', 'MDIC'], |
||||
|
1645 => ['MDCVL'], |
||||
|
1646 => ['MDCVLI'], |
||||
|
1647 => ['MDCVLII'], |
||||
|
1648 => ['MDCVLIII'], |
||||
|
1649 => ['MDCVLIV', 'MDCIL'], |
||||
|
1695 => ['MDCVC'], |
||||
|
1696 => ['MDCVCI'], |
||||
|
1697 => ['MDCVCII'], |
||||
|
1698 => ['MDCVCIII'], |
||||
|
1699 => ['MDCVCIV', 'MDCIC'], |
||||
|
1745 => ['MDCCVL'], |
||||
|
1746 => ['MDCCVLI'], |
||||
|
1747 => ['MDCCVLII'], |
||||
|
1748 => ['MDCCVLIII'], |
||||
|
1749 => ['MDCCVLIV', 'MDCCIL'], |
||||
|
1795 => ['MDCCVC'], |
||||
|
1796 => ['MDCCVCI'], |
||||
|
1797 => ['MDCCVCII'], |
||||
|
1798 => ['MDCCVCIII'], |
||||
|
1799 => ['MDCCVCIV', 'MDCCIC'], |
||||
|
1845 => ['MDCCCVL'], |
||||
|
1846 => ['MDCCCVLI'], |
||||
|
1847 => ['MDCCCVLII'], |
||||
|
1848 => ['MDCCCVLIII'], |
||||
|
1849 => ['MDCCCVLIV', 'MDCCCIL'], |
||||
|
1895 => ['MDCCCVC'], |
||||
|
1896 => ['MDCCCVCI'], |
||||
|
1897 => ['MDCCCVCII'], |
||||
|
1898 => ['MDCCCVCIII'], |
||||
|
1899 => ['MDCCCVCIV', 'MDCCCIC'], |
||||
|
1945 => ['MCMVL'], |
||||
|
1946 => ['MCMVLI'], |
||||
|
1947 => ['MCMVLII'], |
||||
|
1948 => ['MCMVLIII'], |
||||
|
1949 => ['MCMVLIV', 'MCMIL'], |
||||
|
1950 => ['MLM'], |
||||
|
1951 => ['MLMI'], |
||||
|
1952 => ['MLMII'], |
||||
|
1953 => ['MLMIII'], |
||||
|
1954 => ['MLMIV'], |
||||
|
1955 => ['MLMV'], |
||||
|
1956 => ['MLMVI'], |
||||
|
1957 => ['MLMVII'], |
||||
|
1958 => ['MLMVIII'], |
||||
|
1959 => ['MLMIX'], |
||||
|
1960 => ['MLMX'], |
||||
|
1961 => ['MLMXI'], |
||||
|
1962 => ['MLMXII'], |
||||
|
1963 => ['MLMXIII'], |
||||
|
1964 => ['MLMXIV'], |
||||
|
1965 => ['MLMXV'], |
||||
|
1966 => ['MLMXVI'], |
||||
|
1967 => ['MLMXVII'], |
||||
|
1968 => ['MLMXVIII'], |
||||
|
1969 => ['MLMXIX'], |
||||
|
1970 => ['MLMXX'], |
||||
|
1971 => ['MLMXXI'], |
||||
|
1972 => ['MLMXXII'], |
||||
|
1973 => ['MLMXXIII'], |
||||
|
1974 => ['MLMXXIV'], |
||||
|
1975 => ['MLMXXV'], |
||||
|
1976 => ['MLMXXVI'], |
||||
|
1977 => ['MLMXXVII'], |
||||
|
1978 => ['MLMXXVIII'], |
||||
|
1979 => ['MLMXXIX'], |
||||
|
1980 => ['MLMXXX'], |
||||
|
1981 => ['MLMXXXI'], |
||||
|
1982 => ['MLMXXXII'], |
||||
|
1983 => ['MLMXXXIII'], |
||||
|
1984 => ['MLMXXXIV'], |
||||
|
1985 => ['MLMXXXV'], |
||||
|
1986 => ['MLMXXXVI'], |
||||
|
1987 => ['MLMXXXVII'], |
||||
|
1988 => ['MLMXXXVIII'], |
||||
|
1989 => ['MLMXXXIX'], |
||||
|
1990 => ['MLMXL', 'MXM'], |
||||
|
1991 => ['MLMXLI', 'MXMI'], |
||||
|
1992 => ['MLMXLII', 'MXMII'], |
||||
|
1993 => ['MLMXLIII', 'MXMIII'], |
||||
|
1994 => ['MLMXLIV', 'MXMIV'], |
||||
|
1995 => ['MLMVL', 'MXMV', 'MVM'], |
||||
|
1996 => ['MLMVLI', 'MXMVI', 'MVMI'], |
||||
|
1997 => ['MLMVLII', 'MXMVII', 'MVMII'], |
||||
|
1998 => ['MLMVLIII', 'MXMVIII', 'MVMIII'], |
||||
|
1999 => ['MLMVLIV', 'MXMIX', 'MVMIV', 'MIM'], |
||||
|
2045 => ['MMVL'], |
||||
|
2046 => ['MMVLI'], |
||||
|
2047 => ['MMVLII'], |
||||
|
2048 => ['MMVLIII'], |
||||
|
2049 => ['MMVLIV', 'MMIL'], |
||||
|
2095 => ['MMVC'], |
||||
|
2096 => ['MMVCI'], |
||||
|
2097 => ['MMVCII'], |
||||
|
2098 => ['MMVCIII'], |
||||
|
2099 => ['MMVCIV', 'MMIC'], |
||||
|
2145 => ['MMCVL'], |
||||
|
2146 => ['MMCVLI'], |
||||
|
2147 => ['MMCVLII'], |
||||
|
2148 => ['MMCVLIII'], |
||||
|
2149 => ['MMCVLIV', 'MMCIL'], |
||||
|
2195 => ['MMCVC'], |
||||
|
2196 => ['MMCVCI'], |
||||
|
2197 => ['MMCVCII'], |
||||
|
2198 => ['MMCVCIII'], |
||||
|
2199 => ['MMCVCIV', 'MMCIC'], |
||||
|
2245 => ['MMCCVL'], |
||||
|
2246 => ['MMCCVLI'], |
||||
|
2247 => ['MMCCVLII'], |
||||
|
2248 => ['MMCCVLIII'], |
||||
|
2249 => ['MMCCVLIV', 'MMCCIL'], |
||||
|
2295 => ['MMCCVC'], |
||||
|
2296 => ['MMCCVCI'], |
||||
|
2297 => ['MMCCVCII'], |
||||
|
2298 => ['MMCCVCIII'], |
||||
|
2299 => ['MMCCVCIV', 'MMCCIC'], |
||||
|
2345 => ['MMCCCVL'], |
||||
|
2346 => ['MMCCCVLI'], |
||||
|
2347 => ['MMCCCVLII'], |
||||
|
2348 => ['MMCCCVLIII'], |
||||
|
2349 => ['MMCCCVLIV', 'MMCCCIL'], |
||||
|
2395 => ['MMCCCVC'], |
||||
|
2396 => ['MMCCCVCI'], |
||||
|
2397 => ['MMCCCVCII'], |
||||
|
2398 => ['MMCCCVCIII'], |
||||
|
2399 => ['MMCCCVCIV', 'MMCCCIC'], |
||||
|
2445 => ['MMCDVL'], |
||||
|
2446 => ['MMCDVLI'], |
||||
|
2447 => ['MMCDVLII'], |
||||
|
2448 => ['MMCDVLIII'], |
||||
|
2449 => ['MMCDVLIV', 'MMCDIL'], |
||||
|
2450 => ['MMLD'], |
||||
|
2451 => ['MMLDI'], |
||||
|
2452 => ['MMLDII'], |
||||
|
2453 => ['MMLDIII'], |
||||
|
2454 => ['MMLDIV'], |
||||
|
2455 => ['MMLDV'], |
||||
|
2456 => ['MMLDVI'], |
||||
|
2457 => ['MMLDVII'], |
||||
|
2458 => ['MMLDVIII'], |
||||
|
2459 => ['MMLDIX'], |
||||
|
2460 => ['MMLDX'], |
||||
|
2461 => ['MMLDXI'], |
||||
|
2462 => ['MMLDXII'], |
||||
|
2463 => ['MMLDXIII'], |
||||
|
2464 => ['MMLDXIV'], |
||||
|
2465 => ['MMLDXV'], |
||||
|
2466 => ['MMLDXVI'], |
||||
|
2467 => ['MMLDXVII'], |
||||
|
2468 => ['MMLDXVIII'], |
||||
|
2469 => ['MMLDXIX'], |
||||
|
2470 => ['MMLDXX'], |
||||
|
2471 => ['MMLDXXI'], |
||||
|
2472 => ['MMLDXXII'], |
||||
|
2473 => ['MMLDXXIII'], |
||||
|
2474 => ['MMLDXXIV'], |
||||
|
2475 => ['MMLDXXV'], |
||||
|
2476 => ['MMLDXXVI'], |
||||
|
2477 => ['MMLDXXVII'], |
||||
|
2478 => ['MMLDXXVIII'], |
||||
|
2479 => ['MMLDXXIX'], |
||||
|
2480 => ['MMLDXXX'], |
||||
|
2481 => ['MMLDXXXI'], |
||||
|
2482 => ['MMLDXXXII'], |
||||
|
2483 => ['MMLDXXXIII'], |
||||
|
2484 => ['MMLDXXXIV'], |
||||
|
2485 => ['MMLDXXXV'], |
||||
|
2486 => ['MMLDXXXVI'], |
||||
|
2487 => ['MMLDXXXVII'], |
||||
|
2488 => ['MMLDXXXVIII'], |
||||
|
2489 => ['MMLDXXXIX'], |
||||
|
2490 => ['MMLDXL', 'MMXD'], |
||||
|
2491 => ['MMLDXLI', 'MMXDI'], |
||||
|
2492 => ['MMLDXLII', 'MMXDII'], |
||||
|
2493 => ['MMLDXLIII', 'MMXDIII'], |
||||
|
2494 => ['MMLDXLIV', 'MMXDIV'], |
||||
|
2495 => ['MMLDVL', 'MMXDV', 'MMVD'], |
||||
|
2496 => ['MMLDVLI', 'MMXDVI', 'MMVDI'], |
||||
|
2497 => ['MMLDVLII', 'MMXDVII', 'MMVDII'], |
||||
|
2498 => ['MMLDVLIII', 'MMXDVIII', 'MMVDIII'], |
||||
|
2499 => ['MMLDVLIV', 'MMXDIX', 'MMVDIV', 'MMID'], |
||||
|
2545 => ['MMDVL'], |
||||
|
2546 => ['MMDVLI'], |
||||
|
2547 => ['MMDVLII'], |
||||
|
2548 => ['MMDVLIII'], |
||||
|
2549 => ['MMDVLIV', 'MMDIL'], |
||||
|
2595 => ['MMDVC'], |
||||
|
2596 => ['MMDVCI'], |
||||
|
2597 => ['MMDVCII'], |
||||
|
2598 => ['MMDVCIII'], |
||||
|
2599 => ['MMDVCIV', 'MMDIC'], |
||||
|
2645 => ['MMDCVL'], |
||||
|
2646 => ['MMDCVLI'], |
||||
|
2647 => ['MMDCVLII'], |
||||
|
2648 => ['MMDCVLIII'], |
||||
|
2649 => ['MMDCVLIV', 'MMDCIL'], |
||||
|
2695 => ['MMDCVC'], |
||||
|
2696 => ['MMDCVCI'], |
||||
|
2697 => ['MMDCVCII'], |
||||
|
2698 => ['MMDCVCIII'], |
||||
|
2699 => ['MMDCVCIV', 'MMDCIC'], |
||||
|
2745 => ['MMDCCVL'], |
||||
|
2746 => ['MMDCCVLI'], |
||||
|
2747 => ['MMDCCVLII'], |
||||
|
2748 => ['MMDCCVLIII'], |
||||
|
2749 => ['MMDCCVLIV', 'MMDCCIL'], |
||||
|
2795 => ['MMDCCVC'], |
||||
|
2796 => ['MMDCCVCI'], |
||||
|
2797 => ['MMDCCVCII'], |
||||
|
2798 => ['MMDCCVCIII'], |
||||
|
2799 => ['MMDCCVCIV', 'MMDCCIC'], |
||||
|
2845 => ['MMDCCCVL'], |
||||
|
2846 => ['MMDCCCVLI'], |
||||
|
2847 => ['MMDCCCVLII'], |
||||
|
2848 => ['MMDCCCVLIII'], |
||||
|
2849 => ['MMDCCCVLIV', 'MMDCCCIL'], |
||||
|
2895 => ['MMDCCCVC'], |
||||
|
2896 => ['MMDCCCVCI'], |
||||
|
2897 => ['MMDCCCVCII'], |
||||
|
2898 => ['MMDCCCVCIII'], |
||||
|
2899 => ['MMDCCCVCIV', 'MMDCCCIC'], |
||||
|
2945 => ['MMCMVL'], |
||||
|
2946 => ['MMCMVLI'], |
||||
|
2947 => ['MMCMVLII'], |
||||
|
2948 => ['MMCMVLIII'], |
||||
|
2949 => ['MMCMVLIV', 'MMCMIL'], |
||||
|
2950 => ['MMLM'], |
||||
|
2951 => ['MMLMI'], |
||||
|
2952 => ['MMLMII'], |
||||
|
2953 => ['MMLMIII'], |
||||
|
2954 => ['MMLMIV'], |
||||
|
2955 => ['MMLMV'], |
||||
|
2956 => ['MMLMVI'], |
||||
|
2957 => ['MMLMVII'], |
||||
|
2958 => ['MMLMVIII'], |
||||
|
2959 => ['MMLMIX'], |
||||
|
2960 => ['MMLMX'], |
||||
|
2961 => ['MMLMXI'], |
||||
|
2962 => ['MMLMXII'], |
||||
|
2963 => ['MMLMXIII'], |
||||
|
2964 => ['MMLMXIV'], |
||||
|
2965 => ['MMLMXV'], |
||||
|
2966 => ['MMLMXVI'], |
||||
|
2967 => ['MMLMXVII'], |
||||
|
2968 => ['MMLMXVIII'], |
||||
|
2969 => ['MMLMXIX'], |
||||
|
2970 => ['MMLMXX'], |
||||
|
2971 => ['MMLMXXI'], |
||||
|
2972 => ['MMLMXXII'], |
||||
|
2973 => ['MMLMXXIII'], |
||||
|
2974 => ['MMLMXXIV'], |
||||
|
2975 => ['MMLMXXV'], |
||||
|
2976 => ['MMLMXXVI'], |
||||
|
2977 => ['MMLMXXVII'], |
||||
|
2978 => ['MMLMXXVIII'], |
||||
|
2979 => ['MMLMXXIX'], |
||||
|
2980 => ['MMLMXXX'], |
||||
|
2981 => ['MMLMXXXI'], |
||||
|
2982 => ['MMLMXXXII'], |
||||
|
2983 => ['MMLMXXXIII'], |
||||
|
2984 => ['MMLMXXXIV'], |
||||
|
2985 => ['MMLMXXXV'], |
||||
|
2986 => ['MMLMXXXVI'], |
||||
|
2987 => ['MMLMXXXVII'], |
||||
|
2988 => ['MMLMXXXVIII'], |
||||
|
2989 => ['MMLMXXXIX'], |
||||
|
2990 => ['MMLMXL', 'MMXM'], |
||||
|
2991 => ['MMLMXLI', 'MMXMI'], |
||||
|
2992 => ['MMLMXLII', 'MMXMII'], |
||||
|
2993 => ['MMLMXLIII', 'MMXMIII'], |
||||
|
2994 => ['MMLMXLIV', 'MMXMIV'], |
||||
|
2995 => ['MMLMVL', 'MMXMV', 'MMVM'], |
||||
|
2996 => ['MMLMVLI', 'MMXMVI', 'MMVMI'], |
||||
|
2997 => ['MMLMVLII', 'MMXMVII', 'MMVMII'], |
||||
|
2998 => ['MMLMVLIII', 'MMXMVIII', 'MMVMIII'], |
||||
|
2999 => ['MMLMVLIV', 'MMXMIX', 'MMVMIV', 'MMIM'], |
||||
|
3045 => ['MMMVL'], |
||||
|
3046 => ['MMMVLI'], |
||||
|
3047 => ['MMMVLII'], |
||||
|
3048 => ['MMMVLIII'], |
||||
|
3049 => ['MMMVLIV', 'MMMIL'], |
||||
|
3095 => ['MMMVC'], |
||||
|
3096 => ['MMMVCI'], |
||||
|
3097 => ['MMMVCII'], |
||||
|
3098 => ['MMMVCIII'], |
||||
|
3099 => ['MMMVCIV', 'MMMIC'], |
||||
|
3145 => ['MMMCVL'], |
||||
|
3146 => ['MMMCVLI'], |
||||
|
3147 => ['MMMCVLII'], |
||||
|
3148 => ['MMMCVLIII'], |
||||
|
3149 => ['MMMCVLIV', 'MMMCIL'], |
||||
|
3195 => ['MMMCVC'], |
||||
|
3196 => ['MMMCVCI'], |
||||
|
3197 => ['MMMCVCII'], |
||||
|
3198 => ['MMMCVCIII'], |
||||
|
3199 => ['MMMCVCIV', 'MMMCIC'], |
||||
|
3245 => ['MMMCCVL'], |
||||
|
3246 => ['MMMCCVLI'], |
||||
|
3247 => ['MMMCCVLII'], |
||||
|
3248 => ['MMMCCVLIII'], |
||||
|
3249 => ['MMMCCVLIV', 'MMMCCIL'], |
||||
|
3295 => ['MMMCCVC'], |
||||
|
3296 => ['MMMCCVCI'], |
||||
|
3297 => ['MMMCCVCII'], |
||||
|
3298 => ['MMMCCVCIII'], |
||||
|
3299 => ['MMMCCVCIV', 'MMMCCIC'], |
||||
|
3345 => ['MMMCCCVL'], |
||||
|
3346 => ['MMMCCCVLI'], |
||||
|
3347 => ['MMMCCCVLII'], |
||||
|
3348 => ['MMMCCCVLIII'], |
||||
|
3349 => ['MMMCCCVLIV', 'MMMCCCIL'], |
||||
|
3395 => ['MMMCCCVC'], |
||||
|
3396 => ['MMMCCCVCI'], |
||||
|
3397 => ['MMMCCCVCII'], |
||||
|
3398 => ['MMMCCCVCIII'], |
||||
|
3399 => ['MMMCCCVCIV', 'MMMCCCIC'], |
||||
|
3445 => ['MMMCDVL'], |
||||
|
3446 => ['MMMCDVLI'], |
||||
|
3447 => ['MMMCDVLII'], |
||||
|
3448 => ['MMMCDVLIII'], |
||||
|
3449 => ['MMMCDVLIV', 'MMMCDIL'], |
||||
|
3450 => ['MMMLD'], |
||||
|
3451 => ['MMMLDI'], |
||||
|
3452 => ['MMMLDII'], |
||||
|
3453 => ['MMMLDIII'], |
||||
|
3454 => ['MMMLDIV'], |
||||
|
3455 => ['MMMLDV'], |
||||
|
3456 => ['MMMLDVI'], |
||||
|
3457 => ['MMMLDVII'], |
||||
|
3458 => ['MMMLDVIII'], |
||||
|
3459 => ['MMMLDIX'], |
||||
|
3460 => ['MMMLDX'], |
||||
|
3461 => ['MMMLDXI'], |
||||
|
3462 => ['MMMLDXII'], |
||||
|
3463 => ['MMMLDXIII'], |
||||
|
3464 => ['MMMLDXIV'], |
||||
|
3465 => ['MMMLDXV'], |
||||
|
3466 => ['MMMLDXVI'], |
||||
|
3467 => ['MMMLDXVII'], |
||||
|
3468 => ['MMMLDXVIII'], |
||||
|
3469 => ['MMMLDXIX'], |
||||
|
3470 => ['MMMLDXX'], |
||||
|
3471 => ['MMMLDXXI'], |
||||
|
3472 => ['MMMLDXXII'], |
||||
|
3473 => ['MMMLDXXIII'], |
||||
|
3474 => ['MMMLDXXIV'], |
||||
|
3475 => ['MMMLDXXV'], |
||||
|
3476 => ['MMMLDXXVI'], |
||||
|
3477 => ['MMMLDXXVII'], |
||||
|
3478 => ['MMMLDXXVIII'], |
||||
|
3479 => ['MMMLDXXIX'], |
||||
|
3480 => ['MMMLDXXX'], |
||||
|
3481 => ['MMMLDXXXI'], |
||||
|
3482 => ['MMMLDXXXII'], |
||||
|
3483 => ['MMMLDXXXIII'], |
||||
|
3484 => ['MMMLDXXXIV'], |
||||
|
3485 => ['MMMLDXXXV'], |
||||
|
3486 => ['MMMLDXXXVI'], |
||||
|
3487 => ['MMMLDXXXVII'], |
||||
|
3488 => ['MMMLDXXXVIII'], |
||||
|
3489 => ['MMMLDXXXIX'], |
||||
|
3490 => ['MMMLDXL', 'MMMXD'], |
||||
|
3491 => ['MMMLDXLI', 'MMMXDI'], |
||||
|
3492 => ['MMMLDXLII', 'MMMXDII'], |
||||
|
3493 => ['MMMLDXLIII', 'MMMXDIII'], |
||||
|
3494 => ['MMMLDXLIV', 'MMMXDIV'], |
||||
|
3495 => ['MMMLDVL', 'MMMXDV', 'MMMVD'], |
||||
|
3496 => ['MMMLDVLI', 'MMMXDVI', 'MMMVDI'], |
||||
|
3497 => ['MMMLDVLII', 'MMMXDVII', 'MMMVDII'], |
||||
|
3498 => ['MMMLDVLIII', 'MMMXDVIII', 'MMMVDIII'], |
||||
|
3499 => ['MMMLDVLIV', 'MMMXDIX', 'MMMVDIV', 'MMMID'], |
||||
|
3545 => ['MMMDVL'], |
||||
|
3546 => ['MMMDVLI'], |
||||
|
3547 => ['MMMDVLII'], |
||||
|
3548 => ['MMMDVLIII'], |
||||
|
3549 => ['MMMDVLIV', 'MMMDIL'], |
||||
|
3595 => ['MMMDVC'], |
||||
|
3596 => ['MMMDVCI'], |
||||
|
3597 => ['MMMDVCII'], |
||||
|
3598 => ['MMMDVCIII'], |
||||
|
3599 => ['MMMDVCIV', 'MMMDIC'], |
||||
|
3645 => ['MMMDCVL'], |
||||
|
3646 => ['MMMDCVLI'], |
||||
|
3647 => ['MMMDCVLII'], |
||||
|
3648 => ['MMMDCVLIII'], |
||||
|
3649 => ['MMMDCVLIV', 'MMMDCIL'], |
||||
|
3695 => ['MMMDCVC'], |
||||
|
3696 => ['MMMDCVCI'], |
||||
|
3697 => ['MMMDCVCII'], |
||||
|
3698 => ['MMMDCVCIII'], |
||||
|
3699 => ['MMMDCVCIV', 'MMMDCIC'], |
||||
|
3745 => ['MMMDCCVL'], |
||||
|
3746 => ['MMMDCCVLI'], |
||||
|
3747 => ['MMMDCCVLII'], |
||||
|
3748 => ['MMMDCCVLIII'], |
||||
|
3749 => ['MMMDCCVLIV', 'MMMDCCIL'], |
||||
|
3795 => ['MMMDCCVC'], |
||||
|
3796 => ['MMMDCCVCI'], |
||||
|
3797 => ['MMMDCCVCII'], |
||||
|
3798 => ['MMMDCCVCIII'], |
||||
|
3799 => ['MMMDCCVCIV', 'MMMDCCIC'], |
||||
|
3845 => ['MMMDCCCVL'], |
||||
|
3846 => ['MMMDCCCVLI'], |
||||
|
3847 => ['MMMDCCCVLII'], |
||||
|
3848 => ['MMMDCCCVLIII'], |
||||
|
3849 => ['MMMDCCCVLIV', 'MMMDCCCIL'], |
||||
|
3895 => ['MMMDCCCVC'], |
||||
|
3896 => ['MMMDCCCVCI'], |
||||
|
3897 => ['MMMDCCCVCII'], |
||||
|
3898 => ['MMMDCCCVCIII'], |
||||
|
3899 => ['MMMDCCCVCIV', 'MMMDCCCIC'], |
||||
|
3945 => ['MMMCMVL'], |
||||
|
3946 => ['MMMCMVLI'], |
||||
|
3947 => ['MMMCMVLII'], |
||||
|
3948 => ['MMMCMVLIII'], |
||||
|
3949 => ['MMMCMVLIV', 'MMMCMIL'], |
||||
|
3950 => ['MMMLM'], |
||||
|
3951 => ['MMMLMI'], |
||||
|
3952 => ['MMMLMII'], |
||||
|
3953 => ['MMMLMIII'], |
||||
|
3954 => ['MMMLMIV'], |
||||
|
3955 => ['MMMLMV'], |
||||
|
3956 => ['MMMLMVI'], |
||||
|
3957 => ['MMMLMVII'], |
||||
|
3958 => ['MMMLMVIII'], |
||||
|
3959 => ['MMMLMIX'], |
||||
|
3960 => ['MMMLMX'], |
||||
|
3961 => ['MMMLMXI'], |
||||
|
3962 => ['MMMLMXII'], |
||||
|
3963 => ['MMMLMXIII'], |
||||
|
3964 => ['MMMLMXIV'], |
||||
|
3965 => ['MMMLMXV'], |
||||
|
3966 => ['MMMLMXVI'], |
||||
|
3967 => ['MMMLMXVII'], |
||||
|
3968 => ['MMMLMXVIII'], |
||||
|
3969 => ['MMMLMXIX'], |
||||
|
3970 => ['MMMLMXX'], |
||||
|
3971 => ['MMMLMXXI'], |
||||
|
3972 => ['MMMLMXXII'], |
||||
|
3973 => ['MMMLMXXIII'], |
||||
|
3974 => ['MMMLMXXIV'], |
||||
|
3975 => ['MMMLMXXV'], |
||||
|
3976 => ['MMMLMXXVI'], |
||||
|
3977 => ['MMMLMXXVII'], |
||||
|
3978 => ['MMMLMXXVIII'], |
||||
|
3979 => ['MMMLMXXIX'], |
||||
|
3980 => ['MMMLMXXX'], |
||||
|
3981 => ['MMMLMXXXI'], |
||||
|
3982 => ['MMMLMXXXII'], |
||||
|
3983 => ['MMMLMXXXIII'], |
||||
|
3984 => ['MMMLMXXXIV'], |
||||
|
3985 => ['MMMLMXXXV'], |
||||
|
3986 => ['MMMLMXXXVI'], |
||||
|
3987 => ['MMMLMXXXVII'], |
||||
|
3988 => ['MMMLMXXXVIII'], |
||||
|
3989 => ['MMMLMXXXIX'], |
||||
|
3990 => ['MMMLMXL', 'MMMXM'], |
||||
|
3991 => ['MMMLMXLI', 'MMMXMI'], |
||||
|
3992 => ['MMMLMXLII', 'MMMXMII'], |
||||
|
3993 => ['MMMLMXLIII', 'MMMXMIII'], |
||||
|
3994 => ['MMMLMXLIV', 'MMMXMIV'], |
||||
|
3995 => ['MMMLMVL', 'MMMXMV', 'MMMVM'], |
||||
|
3996 => ['MMMLMVLI', 'MMMXMVI', 'MMMVMI'], |
||||
|
3997 => ['MMMLMVLII', 'MMMXMVII', 'MMMVMII'], |
||||
|
3998 => ['MMMLMVLIII', 'MMMXMVIII', 'MMMVMIII'], |
||||
|
3999 => ['MMMLMVLIV', 'MMMXMIX', 'MMMVMIV', 'MMMIM'], |
||||
|
]; |
||||
|
|
||||
|
private const THOUSANDS = ['', 'M', 'MM', 'MMM']; |
||||
|
private const HUNDREDS = ['', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM']; |
||||
|
private const TENS = ['', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC']; |
||||
|
private const ONES = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX']; |
||||
|
const MAX_ROMAN_VALUE = 3999; |
||||
|
const MAX_ROMAN_STYLE = 4; |
||||
|
|
||||
|
private static function valueOk(int $aValue, int $style): string |
||||
|
{ |
||||
|
$origValue = $aValue; |
||||
|
$m = \intdiv($aValue, 1000); |
||||
|
$aValue %= 1000; |
||||
|
$c = \intdiv($aValue, 100); |
||||
|
$aValue %= 100; |
||||
|
$t = \intdiv($aValue, 10); |
||||
|
$aValue %= 10; |
||||
|
$result = self::THOUSANDS[$m] . self::HUNDREDS[$c] . self::TENS[$t] . self::ONES[$aValue]; |
||||
|
if ($style > 0) { |
||||
|
if (array_key_exists($origValue, self::VALUES)) { |
||||
|
$arr = self::VALUES[$origValue]; |
||||
|
$idx = min($style, count($arr)) - 1; |
||||
|
$result = $arr[$idx]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
private static function styleOk(int $aValue, int $style): string |
||||
|
{ |
||||
|
return ($aValue < 0 || $aValue > self::MAX_ROMAN_VALUE) ? Functions::VALUE() : self::valueOk($aValue, $style); |
||||
|
} |
||||
|
|
||||
|
public static function calculateRoman(int $aValue, int $style): string |
||||
|
{ |
||||
|
return ($style < 0 || $style > self::MAX_ROMAN_STYLE) ? Functions::VALUE() : self::styleOk($aValue, $style); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ROMAN. |
||||
|
* |
||||
|
* Converts a number to Roman numeral |
||||
|
* |
||||
|
* @param mixed $aValue Number to convert |
||||
|
* Or can be an array of numbers |
||||
|
* @param mixed $style Number indicating one of five possible forms |
||||
|
* Or can be an array of styles |
||||
|
* |
||||
|
* @return array|string Roman numeral, or a string containing an error |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function evaluate($aValue, $style = 0) |
||||
|
{ |
||||
|
if (is_array($aValue) || is_array($style)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $aValue, $style); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$aValue = Helpers::validateNumericNullBool($aValue); |
||||
|
if (is_bool($style)) { |
||||
|
$style = $style ? 0 : 4; |
||||
|
} |
||||
|
$style = Helpers::validateNumericNullSubstitution($style, null); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return self::calculateRoman((int) $aValue, (int) $style); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,218 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Round |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* ROUND. |
||||
|
* |
||||
|
* Returns the result of builtin function round after validating args. |
||||
|
* |
||||
|
* @param mixed $number Should be numeric, or can be an array of numbers |
||||
|
* @param mixed $precision Should be int, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string Rounded number |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function round($number, $precision) |
||||
|
{ |
||||
|
if (is_array($number) || is_array($precision)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $precision); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullBool($number); |
||||
|
$precision = Helpers::validateNumericNullBool($precision); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return round($number, (int) $precision); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ROUNDUP. |
||||
|
* |
||||
|
* Rounds a number up to a specified number of decimal places |
||||
|
* |
||||
|
* @param array|float $number Number to round, or can be an array of numbers |
||||
|
* @param array|int $digits Number of digits to which you want to round $number, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string Rounded Number, or a string containing an error |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function up($number, $digits) |
||||
|
{ |
||||
|
if (is_array($number) || is_array($digits)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $digits); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullBool($number); |
||||
|
$digits = (int) Helpers::validateNumericNullSubstitution($digits, null); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if ($number == 0.0) { |
||||
|
return 0.0; |
||||
|
} |
||||
|
|
||||
|
if ($number < 0.0) { |
||||
|
return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); |
||||
|
} |
||||
|
|
||||
|
return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ROUNDDOWN. |
||||
|
* |
||||
|
* Rounds a number down to a specified number of decimal places |
||||
|
* |
||||
|
* @param array|float $number Number to round, or can be an array of numbers |
||||
|
* @param array|int $digits Number of digits to which you want to round $number, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string Rounded Number, or a string containing an error |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function down($number, $digits) |
||||
|
{ |
||||
|
if (is_array($number) || is_array($digits)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $digits); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullBool($number); |
||||
|
$digits = (int) Helpers::validateNumericNullSubstitution($digits, null); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if ($number == 0.0) { |
||||
|
return 0.0; |
||||
|
} |
||||
|
|
||||
|
if ($number < 0.0) { |
||||
|
return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); |
||||
|
} |
||||
|
|
||||
|
return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* MROUND. |
||||
|
* |
||||
|
* Rounds a number to the nearest multiple of a specified value |
||||
|
* |
||||
|
* @param mixed $number Expect float. Number to round, or can be an array of numbers |
||||
|
* @param mixed $multiple Expect int. Multiple to which you want to round, or can be an array of numbers. |
||||
|
* |
||||
|
* @return array|float|string Rounded Number, or a string containing an error |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function multiple($number, $multiple) |
||||
|
{ |
||||
|
if (is_array($number) || is_array($multiple)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $multiple); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullSubstitution($number, 0); |
||||
|
$multiple = Helpers::validateNumericNullSubstitution($multiple, null); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if ($number == 0 || $multiple == 0) { |
||||
|
return 0; |
||||
|
} |
||||
|
if ((Helpers::returnSign($number)) == (Helpers::returnSign($multiple))) { |
||||
|
$multiplier = 1 / $multiple; |
||||
|
|
||||
|
return round($number * $multiplier) / $multiplier; |
||||
|
} |
||||
|
|
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* EVEN. |
||||
|
* |
||||
|
* Returns number rounded up to the nearest even integer. |
||||
|
* You can use this function for processing items that come in twos. For example, |
||||
|
* a packing crate accepts rows of one or two items. The crate is full when |
||||
|
* the number of items, rounded up to the nearest two, matches the crate's |
||||
|
* capacity. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* EVEN(number) |
||||
|
* |
||||
|
* @param array|float $number Number to round, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string Rounded Number, or a string containing an error |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function even($number) |
||||
|
{ |
||||
|
if (is_array($number)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullBool($number); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return Helpers::getEven($number); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ODD. |
||||
|
* |
||||
|
* Returns number rounded up to the nearest odd integer. |
||||
|
* |
||||
|
* @param array|float $number Number to round, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string Rounded Number, or a string containing an error |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function odd($number) |
||||
|
{ |
||||
|
if (is_array($number)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullBool($number); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$significance = Helpers::returnSign($number); |
||||
|
if ($significance == 0) { |
||||
|
return 1; |
||||
|
} |
||||
|
|
||||
|
$result = ceil($number / $significance) * $significance; |
||||
|
if ($result == Helpers::getEven($result)) { |
||||
|
$result += $significance; |
||||
|
} |
||||
|
|
||||
|
return $result; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class SeriesSum |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* SERIESSUM. |
||||
|
* |
||||
|
* Returns the sum of a power series |
||||
|
* |
||||
|
* @param mixed $x Input value |
||||
|
* @param mixed $n Initial power |
||||
|
* @param mixed $m Step |
||||
|
* @param mixed[] $args An array of coefficients for the Data Series |
||||
|
* |
||||
|
* @return array|float|string The result, or a string containing an error |
||||
|
*/ |
||||
|
public static function evaluate($x, $n, $m, ...$args) |
||||
|
{ |
||||
|
if (is_array($x) || is_array($n) || is_array($m)) { |
||||
|
return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 3, $x, $n, $m, ...$args); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$x = Helpers::validateNumericNullSubstitution($x, 0); |
||||
|
$n = Helpers::validateNumericNullSubstitution($n, 0); |
||||
|
$m = Helpers::validateNumericNullSubstitution($m, 0); |
||||
|
|
||||
|
// Loop through arguments |
||||
|
$aArgs = Functions::flattenArray($args); |
||||
|
$returnValue = 0; |
||||
|
$i = 0; |
||||
|
foreach ($aArgs as $argx) { |
||||
|
if ($argx !== null) { |
||||
|
$arg = Helpers::validateNumericNullSubstitution($argx, 0); |
||||
|
$returnValue += $arg * $x ** ($n + ($m * $i)); |
||||
|
++$i; |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return $returnValue; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
|
||||
|
class Sign |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* SIGN. |
||||
|
* |
||||
|
* Determines the sign of a number. Returns 1 if the number is positive, zero (0) |
||||
|
* if the number is 0, and -1 if the number is negative. |
||||
|
* |
||||
|
* @param array|float $number Number to round, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|int|string sign value, or a string containing an error |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function evaluate($number) |
||||
|
{ |
||||
|
if (is_array($number)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullBool($number); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return Helpers::returnSign($number); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
|
||||
|
class Sqrt |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* SQRT. |
||||
|
* |
||||
|
* Returns the result of builtin function sqrt after validating args. |
||||
|
* |
||||
|
* @param mixed $number Should be numeric, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string square root |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function sqrt($number) |
||||
|
{ |
||||
|
if (is_array($number)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullBool($number); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return Helpers::numberOrNan(sqrt($number)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SQRTPI. |
||||
|
* |
||||
|
* Returns the square root of (number * pi). |
||||
|
* |
||||
|
* @param array|float $number Number, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string Square Root of Number * Pi, or a string containing an error |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function pi($number) |
||||
|
{ |
||||
|
if (is_array($number)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullSubstitution($number, 0); |
||||
|
Helpers::validateNotNegative($number); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return sqrt($number * M_PI); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,114 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Statistical; |
||||
|
|
||||
|
class Subtotal |
||||
|
{ |
||||
|
/** |
||||
|
* @param mixed $cellReference |
||||
|
* @param mixed $args |
||||
|
*/ |
||||
|
protected static function filterHiddenArgs($cellReference, $args): array |
||||
|
{ |
||||
|
return array_filter( |
||||
|
$args, |
||||
|
function ($index) use ($cellReference) { |
||||
|
[, $row, ] = explode('.', $index); |
||||
|
|
||||
|
return $cellReference->getWorksheet()->getRowDimension($row)->getVisible(); |
||||
|
}, |
||||
|
ARRAY_FILTER_USE_KEY |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $cellReference |
||||
|
* @param mixed $args |
||||
|
*/ |
||||
|
protected static function filterFormulaArgs($cellReference, $args): array |
||||
|
{ |
||||
|
return array_filter( |
||||
|
$args, |
||||
|
function ($index) use ($cellReference) { |
||||
|
[, $row, $column] = explode('.', $index); |
||||
|
$retVal = true; |
||||
|
if ($cellReference->getWorksheet()->cellExists($column . $row)) { |
||||
|
//take this cell out if it contains the SUBTOTAL or AGGREGATE functions in a formula |
||||
|
$isFormula = $cellReference->getWorksheet()->getCell($column . $row)->isFormula(); |
||||
|
$cellFormula = !preg_match( |
||||
|
'/^=.*\b(SUBTOTAL|AGGREGATE)\s*\(/i', |
||||
|
$cellReference->getWorksheet()->getCell($column . $row)->getValue() ?? '' |
||||
|
); |
||||
|
|
||||
|
$retVal = !$isFormula || $cellFormula; |
||||
|
} |
||||
|
|
||||
|
return $retVal; |
||||
|
}, |
||||
|
ARRAY_FILTER_USE_KEY |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** @var callable[] */ |
||||
|
private const CALL_FUNCTIONS = [ |
||||
|
1 => [Statistical\Averages::class, 'average'], // 1 and 101 |
||||
|
[Statistical\Counts::class, 'COUNT'], // 2 and 102 |
||||
|
[Statistical\Counts::class, 'COUNTA'], // 3 and 103 |
||||
|
[Statistical\Maximum::class, 'max'], // 4 and 104 |
||||
|
[Statistical\Minimum::class, 'min'], // 5 and 105 |
||||
|
[Operations::class, 'product'], // 6 and 106 |
||||
|
[Statistical\StandardDeviations::class, 'STDEV'], // 7 and 107 |
||||
|
[Statistical\StandardDeviations::class, 'STDEVP'], // 8 and 108 |
||||
|
[Sum::class, 'sumIgnoringStrings'], // 9 and 109 |
||||
|
[Statistical\Variances::class, 'VAR'], // 10 and 110 |
||||
|
[Statistical\Variances::class, 'VARP'], // 111 and 111 |
||||
|
]; |
||||
|
|
||||
|
/** |
||||
|
* SUBTOTAL. |
||||
|
* |
||||
|
* Returns a subtotal in a list or database. |
||||
|
* |
||||
|
* @param mixed $functionType |
||||
|
* A number 1 to 11 that specifies which function to |
||||
|
* use in calculating subtotals within a range |
||||
|
* list |
||||
|
* Numbers 101 to 111 shadow the functions of 1 to 11 |
||||
|
* but ignore any values in the range that are |
||||
|
* in hidden rows |
||||
|
* @param mixed[] $args A mixed data series of values |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function evaluate($functionType, ...$args) |
||||
|
{ |
||||
|
$cellReference = array_pop($args); |
||||
|
$aArgs = Functions::flattenArrayIndexed($args); |
||||
|
|
||||
|
try { |
||||
|
$subtotal = (int) Helpers::validateNumericNullBool($functionType); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
// Calculate |
||||
|
if ($subtotal > 100) { |
||||
|
$aArgs = self::filterHiddenArgs($cellReference, $aArgs); |
||||
|
$subtotal -= 100; |
||||
|
} |
||||
|
|
||||
|
$aArgs = self::filterFormulaArgs($cellReference, $aArgs); |
||||
|
if (array_key_exists($subtotal, self::CALL_FUNCTIONS)) { |
||||
|
/** @var callable */ |
||||
|
$call = self::CALL_FUNCTIONS[$subtotal]; |
||||
|
|
||||
|
return call_user_func_array($call, $aArgs); |
||||
|
} |
||||
|
|
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,115 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Sum |
||||
|
{ |
||||
|
/** |
||||
|
* SUM, ignoring non-numeric non-error strings. This is eventually used by SUMIF. |
||||
|
* |
||||
|
* SUM computes the sum of all the values and cells referenced in the argument list. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* SUM(value1[,value2[, ...]]) |
||||
|
* |
||||
|
* @param mixed ...$args Data values |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function sumIgnoringStrings(...$args) |
||||
|
{ |
||||
|
$returnValue = 0; |
||||
|
|
||||
|
// Loop through the arguments |
||||
|
foreach (Functions::flattenArray($args) as $arg) { |
||||
|
// Is it a numeric value? |
||||
|
if (is_numeric($arg)) { |
||||
|
$returnValue += $arg; |
||||
|
} elseif (Functions::isError($arg)) { |
||||
|
return $arg; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $returnValue; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SUM, returning error for non-numeric strings. This is used by Excel SUM function. |
||||
|
* |
||||
|
* SUM computes the sum of all the values and cells referenced in the argument list. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* SUM(value1[,value2[, ...]]) |
||||
|
* |
||||
|
* @param mixed ...$args Data values |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function sumErroringStrings(...$args) |
||||
|
{ |
||||
|
$returnValue = 0; |
||||
|
// Loop through the arguments |
||||
|
$aArgs = Functions::flattenArrayIndexed($args); |
||||
|
foreach ($aArgs as $k => $arg) { |
||||
|
// Is it a numeric value? |
||||
|
if (is_numeric($arg) || empty($arg)) { |
||||
|
if (is_string($arg)) { |
||||
|
$arg = (int) $arg; |
||||
|
} |
||||
|
$returnValue += $arg; |
||||
|
} elseif (is_bool($arg)) { |
||||
|
$returnValue += (int) $arg; |
||||
|
} elseif (Functions::isError($arg)) { |
||||
|
return $arg; |
||||
|
// ignore non-numerics from cell, but fail as literals (except null) |
||||
|
} elseif ($arg !== null && !Functions::isCellValue($k)) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $returnValue; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SUMPRODUCT. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* SUMPRODUCT(value1[,value2[, ...]]) |
||||
|
* |
||||
|
* @param mixed ...$args Data values |
||||
|
* |
||||
|
* @return float|string The result, or a string containing an error |
||||
|
*/ |
||||
|
public static function product(...$args) |
||||
|
{ |
||||
|
$arrayList = $args; |
||||
|
|
||||
|
$wrkArray = Functions::flattenArray(array_shift($arrayList)); |
||||
|
$wrkCellCount = count($wrkArray); |
||||
|
|
||||
|
for ($i = 0; $i < $wrkCellCount; ++$i) { |
||||
|
if ((!is_numeric($wrkArray[$i])) || (is_string($wrkArray[$i]))) { |
||||
|
$wrkArray[$i] = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
foreach ($arrayList as $matrixData) { |
||||
|
$array2 = Functions::flattenArray($matrixData); |
||||
|
$count = count($array2); |
||||
|
if ($wrkCellCount != $count) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
foreach ($array2 as $i => $val) { |
||||
|
if ((!is_numeric($val)) || (is_string($val))) { |
||||
|
$val = 0; |
||||
|
} |
||||
|
$wrkArray[$i] *= $val; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return array_sum($wrkArray); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,142 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class SumSquares |
||||
|
{ |
||||
|
/** |
||||
|
* SUMSQ. |
||||
|
* |
||||
|
* SUMSQ returns the sum of the squares of the arguments |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* SUMSQ(value1[,value2[, ...]]) |
||||
|
* |
||||
|
* @param mixed ...$args Data values |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function sumSquare(...$args) |
||||
|
{ |
||||
|
try { |
||||
|
$returnValue = 0; |
||||
|
|
||||
|
// Loop through arguments |
||||
|
foreach (Functions::flattenArray($args) as $arg) { |
||||
|
$arg1 = Helpers::validateNumericNullSubstitution($arg, 0); |
||||
|
$returnValue += ($arg1 * $arg1); |
||||
|
} |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return $returnValue; |
||||
|
} |
||||
|
|
||||
|
private static function getCount(array $array1, array $array2): int |
||||
|
{ |
||||
|
$count = count($array1); |
||||
|
if ($count !== count($array2)) { |
||||
|
throw new Exception(Functions::NA()); |
||||
|
} |
||||
|
|
||||
|
return $count; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* These functions accept only numeric arguments, not even strings which are numeric. |
||||
|
* |
||||
|
* @param mixed $item |
||||
|
*/ |
||||
|
private static function numericNotString($item): bool |
||||
|
{ |
||||
|
return is_numeric($item) && !is_string($item); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SUMX2MY2. |
||||
|
* |
||||
|
* @param mixed[] $matrixData1 Matrix #1 |
||||
|
* @param mixed[] $matrixData2 Matrix #2 |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function sumXSquaredMinusYSquared($matrixData1, $matrixData2) |
||||
|
{ |
||||
|
try { |
||||
|
$array1 = Functions::flattenArray($matrixData1); |
||||
|
$array2 = Functions::flattenArray($matrixData2); |
||||
|
$count = self::getCount($array1, $array2); |
||||
|
|
||||
|
$result = 0; |
||||
|
for ($i = 0; $i < $count; ++$i) { |
||||
|
if (self::numericNotString($array1[$i]) && self::numericNotString($array2[$i])) { |
||||
|
$result += ($array1[$i] * $array1[$i]) - ($array2[$i] * $array2[$i]); |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SUMX2PY2. |
||||
|
* |
||||
|
* @param mixed[] $matrixData1 Matrix #1 |
||||
|
* @param mixed[] $matrixData2 Matrix #2 |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function sumXSquaredPlusYSquared($matrixData1, $matrixData2) |
||||
|
{ |
||||
|
try { |
||||
|
$array1 = Functions::flattenArray($matrixData1); |
||||
|
$array2 = Functions::flattenArray($matrixData2); |
||||
|
$count = self::getCount($array1, $array2); |
||||
|
|
||||
|
$result = 0; |
||||
|
for ($i = 0; $i < $count; ++$i) { |
||||
|
if (self::numericNotString($array1[$i]) && self::numericNotString($array2[$i])) { |
||||
|
$result += ($array1[$i] * $array1[$i]) + ($array2[$i] * $array2[$i]); |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SUMXMY2. |
||||
|
* |
||||
|
* @param mixed[] $matrixData1 Matrix #1 |
||||
|
* @param mixed[] $matrixData2 Matrix #2 |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function sumXMinusYSquared($matrixData1, $matrixData2) |
||||
|
{ |
||||
|
try { |
||||
|
$array1 = Functions::flattenArray($matrixData1); |
||||
|
$array2 = Functions::flattenArray($matrixData2); |
||||
|
$count = self::getCount($array1, $array2); |
||||
|
|
||||
|
$result = 0; |
||||
|
for ($i = 0; $i < $count; ++$i) { |
||||
|
if (self::numericNotString($array1[$i]) && self::numericNotString($array2[$i])) { |
||||
|
$result += ($array1[$i] - $array2[$i]) * ($array1[$i] - $array2[$i]); |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return $result; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Helpers; |
||||
|
|
||||
|
class Secant |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* SEC. |
||||
|
* |
||||
|
* Returns the secant of an angle. |
||||
|
* |
||||
|
* @param array|float $angle Number, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string The secant of the angle |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function sec($angle) |
||||
|
{ |
||||
|
if (is_array($angle)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$angle = Helpers::validateNumericNullBool($angle); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return Helpers::verySmallDenominator(1.0, cos($angle)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SECH. |
||||
|
* |
||||
|
* Returns the hyperbolic secant of an angle. |
||||
|
* |
||||
|
* @param array|float $angle Number, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string The hyperbolic secant of the angle |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function sech($angle) |
||||
|
{ |
||||
|
if (is_array($angle)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$angle = Helpers::validateNumericNullBool($angle); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return Helpers::verySmallDenominator(1.0, cosh($angle)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Helpers; |
||||
|
|
||||
|
class Sine |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* SIN. |
||||
|
* |
||||
|
* Returns the result of builtin function sin after validating args. |
||||
|
* |
||||
|
* @param mixed $angle Should be numeric, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string sine |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function sin($angle) |
||||
|
{ |
||||
|
if (is_array($angle)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$angle = Helpers::validateNumericNullBool($angle); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return sin($angle); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SINH. |
||||
|
* |
||||
|
* Returns the result of builtin function sinh after validating args. |
||||
|
* |
||||
|
* @param mixed $angle Should be numeric, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string hyperbolic sine |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function sinh($angle) |
||||
|
{ |
||||
|
if (is_array($angle)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$angle = Helpers::validateNumericNullBool($angle); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return sinh($angle); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ASIN. |
||||
|
* |
||||
|
* Returns the arcsine of a number. |
||||
|
* |
||||
|
* @param array|float $number Number, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string The arcsine of the number |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function asin($number) |
||||
|
{ |
||||
|
if (is_array($number)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullBool($number); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return Helpers::numberOrNan(asin($number)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ASINH. |
||||
|
* |
||||
|
* Returns the inverse hyperbolic sine of a number. |
||||
|
* |
||||
|
* @param array|float $number Number, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string The inverse hyperbolic sine of the number |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function asinh($number) |
||||
|
{ |
||||
|
if (is_array($number)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullBool($number); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return Helpers::numberOrNan(asinh($number)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,161 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Helpers; |
||||
|
|
||||
|
class Tangent |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* TAN. |
||||
|
* |
||||
|
* Returns the result of builtin function tan after validating args. |
||||
|
* |
||||
|
* @param mixed $angle Should be numeric, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string tangent |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function tan($angle) |
||||
|
{ |
||||
|
if (is_array($angle)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$angle = Helpers::validateNumericNullBool($angle); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return Helpers::verySmallDenominator(sin($angle), cos($angle)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TANH. |
||||
|
* |
||||
|
* Returns the result of builtin function sinh after validating args. |
||||
|
* |
||||
|
* @param mixed $angle Should be numeric, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string hyperbolic tangent |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function tanh($angle) |
||||
|
{ |
||||
|
if (is_array($angle)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$angle = Helpers::validateNumericNullBool($angle); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return tanh($angle); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ATAN. |
||||
|
* |
||||
|
* Returns the arctangent of a number. |
||||
|
* |
||||
|
* @param array|float $number Number, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string The arctangent of the number |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function atan($number) |
||||
|
{ |
||||
|
if (is_array($number)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullBool($number); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return Helpers::numberOrNan(atan($number)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ATANH. |
||||
|
* |
||||
|
* Returns the inverse hyperbolic tangent of a number. |
||||
|
* |
||||
|
* @param array|float $number Number, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string The inverse hyperbolic tangent of the number |
||||
|
* If an array of numbers is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function atanh($number) |
||||
|
{ |
||||
|
if (is_array($number)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$number = Helpers::validateNumericNullBool($number); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return Helpers::numberOrNan(atanh($number)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ATAN2. |
||||
|
* |
||||
|
* This function calculates the arc tangent of the two variables x and y. It is similar to |
||||
|
* calculating the arc tangent of y ÷ x, except that the signs of both arguments are used |
||||
|
* to determine the quadrant of the result. |
||||
|
* The arctangent is the angle from the x-axis to a line containing the origin (0, 0) and a |
||||
|
* point with coordinates (xCoordinate, yCoordinate). The angle is given in radians between |
||||
|
* -pi and pi, excluding -pi. |
||||
|
* |
||||
|
* Note that the Excel ATAN2() function accepts its arguments in the reverse order to the standard |
||||
|
* PHP atan2() function, so we need to reverse them here before calling the PHP atan() function. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* ATAN2(xCoordinate,yCoordinate) |
||||
|
* |
||||
|
* @param mixed $xCoordinate should be float, the x-coordinate of the point, or can be an array of numbers |
||||
|
* @param mixed $yCoordinate should be float, the y-coordinate of the point, or can be an array of numbers |
||||
|
* |
||||
|
* @return array|float|string |
||||
|
* The inverse tangent of the specified x- and y-coordinates, or a string containing an error |
||||
|
* If an array of numbers is passed as one of the arguments, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function atan2($xCoordinate, $yCoordinate) |
||||
|
{ |
||||
|
if (is_array($xCoordinate) || is_array($yCoordinate)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $xCoordinate, $yCoordinate); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$xCoordinate = Helpers::validateNumericNullBool($xCoordinate); |
||||
|
$yCoordinate = Helpers::validateNumericNullBool($yCoordinate); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if (($xCoordinate == 0) && ($yCoordinate == 0)) { |
||||
|
return Functions::DIV0(); |
||||
|
} |
||||
|
|
||||
|
return atan2($yCoordinate, $xCoordinate); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
|
||||
|
class Trunc |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* TRUNC. |
||||
|
* |
||||
|
* Truncates value to the number of fractional digits by number_digits. |
||||
|
* |
||||
|
* @param array|float $value |
||||
|
* Or can be an array of values |
||||
|
* @param array|int $digits |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|float|string Truncated value, or a string containing an error |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function evaluate($value = 0, $digits = 0) |
||||
|
{ |
||||
|
if (is_array($value) || is_array($digits)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $digits); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$value = Helpers::validateNumericNullBool($value); |
||||
|
$digits = Helpers::validateNumericNullSubstitution($digits, null); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$digits = floor($digits); |
||||
|
|
||||
|
// Truncate |
||||
|
$adjust = 10 ** $digits; |
||||
|
|
||||
|
if (($digits > 0) && (rtrim((string) (int) ((abs($value) - abs((int) $value)) * $adjust), '0') < $adjust / 10)) { |
||||
|
return $value; |
||||
|
} |
||||
|
|
||||
|
return ((int) ($value * $adjust)) / $adjust; |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,62 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
|
||||
|
class Poisson |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* POISSON. |
||||
|
* |
||||
|
* Returns the Poisson distribution. A common application of the Poisson distribution |
||||
|
* is predicting the number of events over a specific time, such as the number of |
||||
|
* cars arriving at a toll plaza in 1 minute. |
||||
|
* |
||||
|
* @param mixed $value Float value for which we want the probability |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $mean Mean value as a float |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|float|string The result, or a string containing an error |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function distribution($value, $mean, $cumulative) |
||||
|
{ |
||||
|
if (is_array($value) || is_array($mean) || is_array($cumulative)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $cumulative); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$value = DistributionValidations::validateFloat($value); |
||||
|
$mean = DistributionValidations::validateFloat($mean); |
||||
|
$cumulative = DistributionValidations::validateBool($cumulative); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if (($value < 0) || ($mean < 0)) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
if ($cumulative) { |
||||
|
$summer = 0; |
||||
|
$floor = floor($value); |
||||
|
for ($i = 0; $i <= $floor; ++$i) { |
||||
|
$summer += $mean ** $i / MathTrig\Factorial::fact($i); |
||||
|
} |
||||
|
|
||||
|
return exp(0 - $mean) * $summer; |
||||
|
} |
||||
|
|
||||
|
return (exp(0 - $mean) * $mean ** $value) / MathTrig\Factorial::fact($value); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,141 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations; |
||||
|
|
||||
|
class StandardNormal |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* NORMSDIST. |
||||
|
* |
||||
|
* Returns the standard normal cumulative distribution function. The distribution has |
||||
|
* a mean of 0 (zero) and a standard deviation of one. Use this function in place of a |
||||
|
* table of standard normal curve areas. |
||||
|
* |
||||
|
* NOTE: We don't need to check for arrays to array-enable this function, because that is already |
||||
|
* handled by the logic in Normal::distribution() |
||||
|
* All we need to do is pass the value through as scalar or as array. |
||||
|
* |
||||
|
* @param mixed $value Float value for which we want the probability |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|float|string The result, or a string containing an error |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function cumulative($value) |
||||
|
{ |
||||
|
return Normal::distribution($value, 0, 1, true); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* NORM.S.DIST. |
||||
|
* |
||||
|
* Returns the standard normal cumulative distribution function. The distribution has |
||||
|
* a mean of 0 (zero) and a standard deviation of one. Use this function in place of a |
||||
|
* table of standard normal curve areas. |
||||
|
* |
||||
|
* NOTE: We don't need to check for arrays to array-enable this function, because that is already |
||||
|
* handled by the logic in Normal::distribution() |
||||
|
* All we need to do is pass the value and cumulative through as scalar or as array. |
||||
|
* |
||||
|
* @param mixed $value Float value for which we want the probability |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|float|string The result, or a string containing an error |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function distribution($value, $cumulative) |
||||
|
{ |
||||
|
return Normal::distribution($value, 0, 1, $cumulative); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* NORMSINV. |
||||
|
* |
||||
|
* Returns the inverse of the standard normal cumulative distribution |
||||
|
* |
||||
|
* @param mixed $value float probability for which we want the value |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* NOTE: We don't need to check for arrays to array-enable this function, because that is already |
||||
|
* handled by the logic in Normal::inverse() |
||||
|
* All we need to do is pass the value through as scalar or as array |
||||
|
* |
||||
|
* @return array|float|string The result, or a string containing an error |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function inverse($value) |
||||
|
{ |
||||
|
return Normal::inverse($value, 0, 1); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* GAUSS. |
||||
|
* |
||||
|
* Calculates the probability that a member of a standard normal population will fall between |
||||
|
* the mean and z standard deviations from the mean. |
||||
|
* |
||||
|
* @param mixed $value |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|float|string The result, or a string containing an error |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function gauss($value) |
||||
|
{ |
||||
|
if (is_array($value)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); |
||||
|
} |
||||
|
|
||||
|
if (!is_numeric($value)) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
return self::distribution($value, true) - 0.5; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ZTEST. |
||||
|
* |
||||
|
* Returns the one-tailed P-value of a z-test. |
||||
|
* |
||||
|
* For a given hypothesized population mean, x, Z.TEST returns the probability that the sample mean would be |
||||
|
* greater than the average of observations in the data set (array) — that is, the observed sample mean. |
||||
|
* |
||||
|
* @param mixed $dataSet The dataset should be an array of float values for the observations |
||||
|
* @param mixed $m0 Alpha Parameter |
||||
|
* @param mixed $sigma A null or float value for the Beta (Standard Deviation) Parameter; |
||||
|
* if null, we use the standard deviation of the dataset |
||||
|
* |
||||
|
* @return float|string (string if result is an error) |
||||
|
*/ |
||||
|
public static function zTest($dataSet, $m0, $sigma = null) |
||||
|
{ |
||||
|
$dataSet = Functions::flattenArrayIndexed($dataSet); |
||||
|
$m0 = Functions::flattenSingleValue($m0); |
||||
|
$sigma = Functions::flattenSingleValue($sigma); |
||||
|
|
||||
|
if (!is_numeric($m0) || ($sigma !== null && !is_numeric($sigma))) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
if ($sigma === null) { |
||||
|
$sigma = StandardDeviations::STDEV($dataSet); |
||||
|
} |
||||
|
$n = count($dataSet); |
||||
|
|
||||
|
return 1 - self::cumulative((Averages::average($dataSet) - $m0) / ($sigma / sqrt($n))); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,138 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class StudentT |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
private const MAX_ITERATIONS = 256; |
||||
|
|
||||
|
/** |
||||
|
* TDIST. |
||||
|
* |
||||
|
* Returns the probability of Student's T distribution. |
||||
|
* |
||||
|
* @param mixed $value Float value for the distribution |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $degrees Integer value for degrees of freedom |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $tails Integer value for the number of tails (1 or 2) |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|float|string The result, or a string containing an error |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function distribution($value, $degrees, $tails) |
||||
|
{ |
||||
|
if (is_array($value) || is_array($degrees) || is_array($tails)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees, $tails); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$value = DistributionValidations::validateFloat($value); |
||||
|
$degrees = DistributionValidations::validateInt($degrees); |
||||
|
$tails = DistributionValidations::validateInt($tails); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if (($value < 0) || ($degrees < 1) || ($tails < 1) || ($tails > 2)) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
return self::calculateDistribution($value, $degrees, $tails); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TINV. |
||||
|
* |
||||
|
* Returns the one-tailed probability of the chi-squared distribution. |
||||
|
* |
||||
|
* @param mixed $probability Float probability for the function |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $degrees Integer value for degrees of freedom |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|float|string The result, or a string containing an error |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function inverse($probability, $degrees) |
||||
|
{ |
||||
|
if (is_array($probability) || is_array($degrees)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$probability = DistributionValidations::validateProbability($probability); |
||||
|
$degrees = DistributionValidations::validateInt($degrees); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if ($degrees <= 0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
$callback = function ($value) use ($degrees) { |
||||
|
return self::distribution($value, $degrees, 2); |
||||
|
}; |
||||
|
|
||||
|
$newtonRaphson = new NewtonRaphson($callback); |
||||
|
|
||||
|
return $newtonRaphson->execute($probability); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return float |
||||
|
*/ |
||||
|
private static function calculateDistribution(float $value, int $degrees, int $tails) |
||||
|
{ |
||||
|
// tdist, which finds the probability that corresponds to a given value |
||||
|
// of t with k degrees of freedom. This algorithm is translated from a |
||||
|
// pascal function on p81 of "Statistical Computing in Pascal" by D |
||||
|
// Cooke, A H Craven & G M Clark (1985: Edward Arnold (Pubs.) Ltd: |
||||
|
// London). The above Pascal algorithm is itself a translation of the |
||||
|
// fortran algoritm "AS 3" by B E Cooper of the Atlas Computer |
||||
|
// Laboratory as reported in (among other places) "Applied Statistics |
||||
|
// Algorithms", editied by P Griffiths and I D Hill (1985; Ellis |
||||
|
// Horwood Ltd.; W. Sussex, England). |
||||
|
$tterm = $degrees; |
||||
|
$ttheta = atan2($value, sqrt($tterm)); |
||||
|
$tc = cos($ttheta); |
||||
|
$ts = sin($ttheta); |
||||
|
|
||||
|
if (($degrees % 2) === 1) { |
||||
|
$ti = 3; |
||||
|
$tterm = $tc; |
||||
|
} else { |
||||
|
$ti = 2; |
||||
|
$tterm = 1; |
||||
|
} |
||||
|
|
||||
|
$tsum = $tterm; |
||||
|
while ($ti < $degrees) { |
||||
|
$tterm *= $tc * $tc * ($ti - 1) / $ti; |
||||
|
$tsum += $tterm; |
||||
|
$ti += 2; |
||||
|
} |
||||
|
|
||||
|
$tsum *= $ts; |
||||
|
if (($degrees % 2) == 1) { |
||||
|
$tsum = Functions::M_2DIVPI * ($tsum + $ttheta); |
||||
|
} |
||||
|
|
||||
|
$tValue = 0.5 * (1 + $tsum); |
||||
|
if ($tails == 1) { |
||||
|
return 1 - abs($tValue); |
||||
|
} |
||||
|
|
||||
|
return 1 - abs((1 - $tValue) - $tValue); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Weibull |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* WEIBULL. |
||||
|
* |
||||
|
* Returns the Weibull distribution. Use this distribution in reliability |
||||
|
* analysis, such as calculating a device's mean time to failure. |
||||
|
* |
||||
|
* @param mixed $value Float value for the distribution |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $alpha Float alpha Parameter |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $beta Float beta Parameter |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|float|string (string if result is an error) |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function distribution($value, $alpha, $beta, $cumulative) |
||||
|
{ |
||||
|
if (is_array($value) || is_array($alpha) || is_array($beta) || is_array($cumulative)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $alpha, $beta, $cumulative); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$value = DistributionValidations::validateFloat($value); |
||||
|
$alpha = DistributionValidations::validateFloat($alpha); |
||||
|
$beta = DistributionValidations::validateFloat($beta); |
||||
|
$cumulative = DistributionValidations::validateBool($cumulative); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if (($value < 0) || ($alpha <= 0) || ($beta <= 0)) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
if ($cumulative) { |
||||
|
return 1 - exp(0 - ($value / $beta) ** $alpha); |
||||
|
} |
||||
|
|
||||
|
return ($alpha / $beta ** $alpha) * $value ** ($alpha - 1) * exp(0 - ($value / $beta) ** $alpha); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\IntOrFloat; |
||||
|
|
||||
|
class Permutations |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* PERMUT. |
||||
|
* |
||||
|
* Returns the number of permutations for a given number of objects that can be |
||||
|
* selected from number objects. A permutation is any set or subset of objects or |
||||
|
* events where internal order is significant. Permutations are different from |
||||
|
* combinations, for which the internal order is not significant. Use this function |
||||
|
* for lottery-style probability calculations. |
||||
|
* |
||||
|
* @param mixed $numObjs Integer number of different objects |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $numInSet Integer number of objects in each permutation |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|float|int|string Number of permutations, or a string containing an error |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function PERMUT($numObjs, $numInSet) |
||||
|
{ |
||||
|
if (is_array($numObjs) || is_array($numInSet)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $numObjs, $numInSet); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$numObjs = StatisticalValidations::validateInt($numObjs); |
||||
|
$numInSet = StatisticalValidations::validateInt($numInSet); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if ($numObjs < $numInSet) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
$result = round(MathTrig\Factorial::fact($numObjs) / MathTrig\Factorial::fact($numObjs - $numInSet)); |
||||
|
|
||||
|
return IntOrFloat::evaluate($result); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* PERMUTATIONA. |
||||
|
* |
||||
|
* Returns the number of permutations for a given number of objects (with repetitions) |
||||
|
* that can be selected from the total objects. |
||||
|
* |
||||
|
* @param mixed $numObjs Integer number of different objects |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $numInSet Integer number of objects in each permutation |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|float|int|string Number of permutations, or a string containing an error |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function PERMUTATIONA($numObjs, $numInSet) |
||||
|
{ |
||||
|
if (is_array($numObjs) || is_array($numInSet)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $numObjs, $numInSet); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$numObjs = StatisticalValidations::validateInt($numObjs); |
||||
|
$numInSet = StatisticalValidations::validateInt($numInSet); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if ($numObjs < 0 || $numInSet < 0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
$result = $numObjs ** $numInSet; |
||||
|
|
||||
|
return IntOrFloat::evaluate($result); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Size |
||||
|
{ |
||||
|
/** |
||||
|
* LARGE. |
||||
|
* |
||||
|
* Returns the nth largest value in a data set. You can use this function to |
||||
|
* select a value based on its relative standing. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* LARGE(value1[,value2[, ...]],entry) |
||||
|
* |
||||
|
* @param mixed $args Data values |
||||
|
* |
||||
|
* @return float|string The result, or a string containing an error |
||||
|
*/ |
||||
|
public static function large(...$args) |
||||
|
{ |
||||
|
$aArgs = Functions::flattenArray($args); |
||||
|
$entry = array_pop($aArgs); |
||||
|
|
||||
|
if ((is_numeric($entry)) && (!is_string($entry))) { |
||||
|
$entry = (int) floor($entry); |
||||
|
|
||||
|
$mArgs = self::filter($aArgs); |
||||
|
$count = Counts::COUNT($mArgs); |
||||
|
--$entry; |
||||
|
if (($entry < 0) || ($entry >= $count) || ($count == 0)) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
rsort($mArgs); |
||||
|
|
||||
|
return $mArgs[$entry]; |
||||
|
} |
||||
|
|
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SMALL. |
||||
|
* |
||||
|
* Returns the nth smallest value in a data set. You can use this function to |
||||
|
* select a value based on its relative standing. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* SMALL(value1[,value2[, ...]],entry) |
||||
|
* |
||||
|
* @param mixed $args Data values |
||||
|
* |
||||
|
* @return float|string The result, or a string containing an error |
||||
|
*/ |
||||
|
public static function small(...$args) |
||||
|
{ |
||||
|
$aArgs = Functions::flattenArray($args); |
||||
|
|
||||
|
$entry = array_pop($aArgs); |
||||
|
|
||||
|
if ((is_numeric($entry)) && (!is_string($entry))) { |
||||
|
$entry = (int) floor($entry); |
||||
|
|
||||
|
$mArgs = self::filter($aArgs); |
||||
|
$count = Counts::COUNT($mArgs); |
||||
|
--$entry; |
||||
|
if (($entry < 0) || ($entry >= $count) || ($count == 0)) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
sort($mArgs); |
||||
|
|
||||
|
return $mArgs[$entry]; |
||||
|
} |
||||
|
|
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed[] $args Data values |
||||
|
*/ |
||||
|
protected static function filter(array $args): array |
||||
|
{ |
||||
|
$mArgs = []; |
||||
|
|
||||
|
foreach ($args as $arg) { |
||||
|
// Is it a numeric value? |
||||
|
if ((is_numeric($arg)) && (!is_string($arg))) { |
||||
|
$mArgs[] = $arg; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $mArgs; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; |
||||
|
|
||||
|
class StandardDeviations |
||||
|
{ |
||||
|
/** |
||||
|
* STDEV. |
||||
|
* |
||||
|
* Estimates standard deviation based on a sample. The standard deviation is a measure of how |
||||
|
* widely values are dispersed from the average value (the mean). |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* STDEV(value1[,value2[, ...]]) |
||||
|
* |
||||
|
* @param mixed ...$args Data values |
||||
|
* |
||||
|
* @return float|string The result, or a string containing an error |
||||
|
*/ |
||||
|
public static function STDEV(...$args) |
||||
|
{ |
||||
|
$result = Variances::VAR(...$args); |
||||
|
if (!is_numeric($result)) { |
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
return sqrt((float) $result); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* STDEVA. |
||||
|
* |
||||
|
* Estimates standard deviation based on a sample, including numbers, text, and logical values |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* STDEVA(value1[,value2[, ...]]) |
||||
|
* |
||||
|
* @param mixed ...$args Data values |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function STDEVA(...$args) |
||||
|
{ |
||||
|
$result = Variances::VARA(...$args); |
||||
|
if (!is_numeric($result)) { |
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
return sqrt((float) $result); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* STDEVP. |
||||
|
* |
||||
|
* Calculates standard deviation based on the entire population |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* STDEVP(value1[,value2[, ...]]) |
||||
|
* |
||||
|
* @param mixed ...$args Data values |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function STDEVP(...$args) |
||||
|
{ |
||||
|
$result = Variances::VARP(...$args); |
||||
|
if (!is_numeric($result)) { |
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
return sqrt((float) $result); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* STDEVPA. |
||||
|
* |
||||
|
* Calculates standard deviation based on the entire population, including numbers, text, and logical values |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* STDEVPA(value1[,value2[, ...]]) |
||||
|
* |
||||
|
* @param mixed ...$args Data values |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function STDEVPA(...$args) |
||||
|
{ |
||||
|
$result = Variances::VARPA(...$args); |
||||
|
if (!is_numeric($result)) { |
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
return sqrt((float) $result); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Standardize extends StatisticalValidations |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* STANDARDIZE. |
||||
|
* |
||||
|
* Returns a normalized value from a distribution characterized by mean and standard_dev. |
||||
|
* |
||||
|
* @param array|float $value Value to normalize |
||||
|
* Or can be an array of values |
||||
|
* @param array|float $mean Mean Value |
||||
|
* Or can be an array of values |
||||
|
* @param array|float $stdDev Standard Deviation |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|float|string Standardized value, or a string containing an error |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function execute($value, $mean, $stdDev) |
||||
|
{ |
||||
|
if (is_array($value) || is_array($mean) || is_array($stdDev)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$value = self::validateFloat($value); |
||||
|
$mean = self::validateFloat($mean); |
||||
|
$stdDev = self::validateFloat($stdDev); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if ($stdDev <= 0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
|
||||
|
return ($value - $mean) / $stdDev; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class StatisticalValidations |
||||
|
{ |
||||
|
/** |
||||
|
* @param mixed $value |
||||
|
*/ |
||||
|
public static function validateFloat($value): float |
||||
|
{ |
||||
|
if (!is_numeric($value)) { |
||||
|
throw new Exception(Functions::VALUE()); |
||||
|
} |
||||
|
|
||||
|
return (float) $value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $value |
||||
|
*/ |
||||
|
public static function validateInt($value): int |
||||
|
{ |
||||
|
if (!is_numeric($value)) { |
||||
|
throw new Exception(Functions::VALUE()); |
||||
|
} |
||||
|
|
||||
|
return (int) floor((float) $value); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $value |
||||
|
*/ |
||||
|
public static function validateBool($value): bool |
||||
|
{ |
||||
|
if (!is_bool($value) && !is_numeric($value)) { |
||||
|
throw new Exception(Functions::VALUE()); |
||||
|
} |
||||
|
|
||||
|
return (bool) $value; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,429 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\Trend\Trend; |
||||
|
|
||||
|
class Trends |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
private static function filterTrendValues(array &$array1, array &$array2): void |
||||
|
{ |
||||
|
foreach ($array1 as $key => $value) { |
||||
|
if ((is_bool($value)) || (is_string($value)) || ($value === null)) { |
||||
|
unset($array1[$key], $array2[$key]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static function checkTrendArrays(&$array1, &$array2): void |
||||
|
{ |
||||
|
if (!is_array($array1)) { |
||||
|
$array1 = [$array1]; |
||||
|
} |
||||
|
if (!is_array($array2)) { |
||||
|
$array2 = [$array2]; |
||||
|
} |
||||
|
|
||||
|
$array1 = Functions::flattenArray($array1); |
||||
|
$array2 = Functions::flattenArray($array2); |
||||
|
|
||||
|
self::filterTrendValues($array1, $array2); |
||||
|
self::filterTrendValues($array2, $array1); |
||||
|
|
||||
|
// Reset the array indexes |
||||
|
$array1 = array_merge($array1); |
||||
|
$array2 = array_merge($array2); |
||||
|
} |
||||
|
|
||||
|
protected static function validateTrendArrays(array $yValues, array $xValues): void |
||||
|
{ |
||||
|
$yValueCount = count($yValues); |
||||
|
$xValueCount = count($xValues); |
||||
|
|
||||
|
if (($yValueCount === 0) || ($yValueCount !== $xValueCount)) { |
||||
|
throw new Exception(Functions::NA()); |
||||
|
} elseif ($yValueCount === 1) { |
||||
|
throw new Exception(Functions::DIV0()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* CORREL. |
||||
|
* |
||||
|
* Returns covariance, the average of the products of deviations for each data point pair. |
||||
|
* |
||||
|
* @param mixed $yValues array of mixed Data Series Y |
||||
|
* @param null|mixed $xValues array of mixed Data Series X |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function CORREL($yValues, $xValues = null) |
||||
|
{ |
||||
|
if (($xValues === null) || (!is_array($yValues)) || (!is_array($xValues))) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
self::checkTrendArrays($yValues, $xValues); |
||||
|
self::validateTrendArrays($yValues, $xValues); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); |
||||
|
|
||||
|
return $bestFitLinear->getCorrelation(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* COVAR. |
||||
|
* |
||||
|
* Returns covariance, the average of the products of deviations for each data point pair. |
||||
|
* |
||||
|
* @param mixed $yValues array of mixed Data Series Y |
||||
|
* @param mixed $xValues array of mixed Data Series X |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function COVAR($yValues, $xValues) |
||||
|
{ |
||||
|
try { |
||||
|
self::checkTrendArrays($yValues, $xValues); |
||||
|
self::validateTrendArrays($yValues, $xValues); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); |
||||
|
|
||||
|
return $bestFitLinear->getCovariance(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* FORECAST. |
||||
|
* |
||||
|
* Calculates, or predicts, a future value by using existing values. |
||||
|
* The predicted value is a y-value for a given x-value. |
||||
|
* |
||||
|
* @param mixed $xValue Float value of X for which we want to find Y |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $yValues array of mixed Data Series Y |
||||
|
* @param mixed $xValues of mixed Data Series X |
||||
|
* |
||||
|
* @return array|bool|float|string |
||||
|
* If an array of numbers is passed as an argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function FORECAST($xValue, $yValues, $xValues) |
||||
|
{ |
||||
|
if (is_array($xValue)) { |
||||
|
return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $xValue, $yValues, $xValues); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$xValue = StatisticalValidations::validateFloat($xValue); |
||||
|
self::checkTrendArrays($yValues, $xValues); |
||||
|
self::validateTrendArrays($yValues, $xValues); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); |
||||
|
|
||||
|
return $bestFitLinear->getValueOfYForX($xValue); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* GROWTH. |
||||
|
* |
||||
|
* Returns values along a predicted exponential Trend |
||||
|
* |
||||
|
* @param mixed[] $yValues Data Series Y |
||||
|
* @param mixed[] $xValues Data Series X |
||||
|
* @param mixed[] $newValues Values of X for which we want to find Y |
||||
|
* @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not |
||||
|
* |
||||
|
* @return float[] |
||||
|
*/ |
||||
|
public static function GROWTH($yValues, $xValues = [], $newValues = [], $const = true) |
||||
|
{ |
||||
|
$yValues = Functions::flattenArray($yValues); |
||||
|
$xValues = Functions::flattenArray($xValues); |
||||
|
$newValues = Functions::flattenArray($newValues); |
||||
|
$const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); |
||||
|
|
||||
|
$bestFitExponential = Trend::calculate(Trend::TREND_EXPONENTIAL, $yValues, $xValues, $const); |
||||
|
if (empty($newValues)) { |
||||
|
$newValues = $bestFitExponential->getXValues(); |
||||
|
} |
||||
|
|
||||
|
$returnArray = []; |
||||
|
foreach ($newValues as $xValue) { |
||||
|
$returnArray[0][] = [$bestFitExponential->getValueOfYForX($xValue)]; |
||||
|
} |
||||
|
|
||||
|
return $returnArray; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* INTERCEPT. |
||||
|
* |
||||
|
* Calculates the point at which a line will intersect the y-axis by using existing x-values and y-values. |
||||
|
* |
||||
|
* @param mixed[] $yValues Data Series Y |
||||
|
* @param mixed[] $xValues Data Series X |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function INTERCEPT($yValues, $xValues) |
||||
|
{ |
||||
|
try { |
||||
|
self::checkTrendArrays($yValues, $xValues); |
||||
|
self::validateTrendArrays($yValues, $xValues); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); |
||||
|
|
||||
|
return $bestFitLinear->getIntersect(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* LINEST. |
||||
|
* |
||||
|
* Calculates the statistics for a line by using the "least squares" method to calculate a straight line |
||||
|
* that best fits your data, and then returns an array that describes the line. |
||||
|
* |
||||
|
* @param mixed[] $yValues Data Series Y |
||||
|
* @param null|mixed[] $xValues Data Series X |
||||
|
* @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not |
||||
|
* @param mixed $stats A logical (boolean) value specifying whether to return additional regression statistics |
||||
|
* |
||||
|
* @return array|int|string The result, or a string containing an error |
||||
|
*/ |
||||
|
public static function LINEST($yValues, $xValues = null, $const = true, $stats = false) |
||||
|
{ |
||||
|
$const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); |
||||
|
$stats = ($stats === null) ? false : (bool) Functions::flattenSingleValue($stats); |
||||
|
if ($xValues === null) { |
||||
|
$xValues = $yValues; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
self::checkTrendArrays($yValues, $xValues); |
||||
|
self::validateTrendArrays($yValues, $xValues); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues, $const); |
||||
|
|
||||
|
if ($stats === true) { |
||||
|
return [ |
||||
|
[ |
||||
|
$bestFitLinear->getSlope(), |
||||
|
$bestFitLinear->getIntersect(), |
||||
|
], |
||||
|
[ |
||||
|
$bestFitLinear->getSlopeSE(), |
||||
|
($const === false) ? Functions::NA() : $bestFitLinear->getIntersectSE(), |
||||
|
], |
||||
|
[ |
||||
|
$bestFitLinear->getGoodnessOfFit(), |
||||
|
$bestFitLinear->getStdevOfResiduals(), |
||||
|
], |
||||
|
[ |
||||
|
$bestFitLinear->getF(), |
||||
|
$bestFitLinear->getDFResiduals(), |
||||
|
], |
||||
|
[ |
||||
|
$bestFitLinear->getSSRegression(), |
||||
|
$bestFitLinear->getSSResiduals(), |
||||
|
], |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
return [ |
||||
|
$bestFitLinear->getSlope(), |
||||
|
$bestFitLinear->getIntersect(), |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* LOGEST. |
||||
|
* |
||||
|
* Calculates an exponential curve that best fits the X and Y data series, |
||||
|
* and then returns an array that describes the line. |
||||
|
* |
||||
|
* @param mixed[] $yValues Data Series Y |
||||
|
* @param null|mixed[] $xValues Data Series X |
||||
|
* @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not |
||||
|
* @param mixed $stats A logical (boolean) value specifying whether to return additional regression statistics |
||||
|
* |
||||
|
* @return array|int|string The result, or a string containing an error |
||||
|
*/ |
||||
|
public static function LOGEST($yValues, $xValues = null, $const = true, $stats = false) |
||||
|
{ |
||||
|
$const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); |
||||
|
$stats = ($stats === null) ? false : (bool) Functions::flattenSingleValue($stats); |
||||
|
if ($xValues === null) { |
||||
|
$xValues = $yValues; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
self::checkTrendArrays($yValues, $xValues); |
||||
|
self::validateTrendArrays($yValues, $xValues); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
foreach ($yValues as $value) { |
||||
|
if ($value < 0.0) { |
||||
|
return Functions::NAN(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$bestFitExponential = Trend::calculate(Trend::TREND_EXPONENTIAL, $yValues, $xValues, $const); |
||||
|
|
||||
|
if ($stats === true) { |
||||
|
return [ |
||||
|
[ |
||||
|
$bestFitExponential->getSlope(), |
||||
|
$bestFitExponential->getIntersect(), |
||||
|
], |
||||
|
[ |
||||
|
$bestFitExponential->getSlopeSE(), |
||||
|
($const === false) ? Functions::NA() : $bestFitExponential->getIntersectSE(), |
||||
|
], |
||||
|
[ |
||||
|
$bestFitExponential->getGoodnessOfFit(), |
||||
|
$bestFitExponential->getStdevOfResiduals(), |
||||
|
], |
||||
|
[ |
||||
|
$bestFitExponential->getF(), |
||||
|
$bestFitExponential->getDFResiduals(), |
||||
|
], |
||||
|
[ |
||||
|
$bestFitExponential->getSSRegression(), |
||||
|
$bestFitExponential->getSSResiduals(), |
||||
|
], |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
return [ |
||||
|
$bestFitExponential->getSlope(), |
||||
|
$bestFitExponential->getIntersect(), |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* RSQ. |
||||
|
* |
||||
|
* Returns the square of the Pearson product moment correlation coefficient through data points |
||||
|
* in known_y's and known_x's. |
||||
|
* |
||||
|
* @param mixed[] $yValues Data Series Y |
||||
|
* @param mixed[] $xValues Data Series X |
||||
|
* |
||||
|
* @return float|string The result, or a string containing an error |
||||
|
*/ |
||||
|
public static function RSQ($yValues, $xValues) |
||||
|
{ |
||||
|
try { |
||||
|
self::checkTrendArrays($yValues, $xValues); |
||||
|
self::validateTrendArrays($yValues, $xValues); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); |
||||
|
|
||||
|
return $bestFitLinear->getGoodnessOfFit(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SLOPE. |
||||
|
* |
||||
|
* Returns the slope of the linear regression line through data points in known_y's and known_x's. |
||||
|
* |
||||
|
* @param mixed[] $yValues Data Series Y |
||||
|
* @param mixed[] $xValues Data Series X |
||||
|
* |
||||
|
* @return float|string The result, or a string containing an error |
||||
|
*/ |
||||
|
public static function SLOPE($yValues, $xValues) |
||||
|
{ |
||||
|
try { |
||||
|
self::checkTrendArrays($yValues, $xValues); |
||||
|
self::validateTrendArrays($yValues, $xValues); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); |
||||
|
|
||||
|
return $bestFitLinear->getSlope(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* STEYX. |
||||
|
* |
||||
|
* Returns the standard error of the predicted y-value for each x in the regression. |
||||
|
* |
||||
|
* @param mixed[] $yValues Data Series Y |
||||
|
* @param mixed[] $xValues Data Series X |
||||
|
* |
||||
|
* @return float|string |
||||
|
*/ |
||||
|
public static function STEYX($yValues, $xValues) |
||||
|
{ |
||||
|
try { |
||||
|
self::checkTrendArrays($yValues, $xValues); |
||||
|
self::validateTrendArrays($yValues, $xValues); |
||||
|
} catch (Exception $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); |
||||
|
|
||||
|
return $bestFitLinear->getStdevOfResiduals(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TREND. |
||||
|
* |
||||
|
* Returns values along a linear Trend |
||||
|
* |
||||
|
* @param mixed[] $yValues Data Series Y |
||||
|
* @param mixed[] $xValues Data Series X |
||||
|
* @param mixed[] $newValues Values of X for which we want to find Y |
||||
|
* @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not |
||||
|
* |
||||
|
* @return float[] |
||||
|
*/ |
||||
|
public static function TREND($yValues, $xValues = [], $newValues = [], $const = true) |
||||
|
{ |
||||
|
$yValues = Functions::flattenArray($yValues); |
||||
|
$xValues = Functions::flattenArray($xValues); |
||||
|
$newValues = Functions::flattenArray($newValues); |
||||
|
$const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); |
||||
|
|
||||
|
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues, $const); |
||||
|
if (empty($newValues)) { |
||||
|
$newValues = $bestFitLinear->getXValues(); |
||||
|
} |
||||
|
|
||||
|
$returnArray = []; |
||||
|
foreach ($newValues as $xValue) { |
||||
|
$returnArray[0][] = [$bestFitLinear->getValueOfYForX($xValue)]; |
||||
|
} |
||||
|
|
||||
|
return $returnArray; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
abstract class VarianceBase |
||||
|
{ |
||||
|
protected static function datatypeAdjustmentAllowStrings($value) |
||||
|
{ |
||||
|
if (is_bool($value)) { |
||||
|
return (int) $value; |
||||
|
} elseif (is_string($value)) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
return $value; |
||||
|
} |
||||
|
|
||||
|
protected static function datatypeAdjustmentBooleans($value) |
||||
|
{ |
||||
|
if (is_bool($value) && (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE)) { |
||||
|
return (int) $value; |
||||
|
} |
||||
|
|
||||
|
return $value; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,185 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Variances extends VarianceBase |
||||
|
{ |
||||
|
/** |
||||
|
* VAR. |
||||
|
* |
||||
|
* Estimates variance based on a sample. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* VAR(value1[,value2[, ...]]) |
||||
|
* |
||||
|
* @param mixed ...$args Data values |
||||
|
* |
||||
|
* @return float|string (string if result is an error) |
||||
|
*/ |
||||
|
public static function VAR(...$args) |
||||
|
{ |
||||
|
$returnValue = Functions::DIV0(); |
||||
|
|
||||
|
$summerA = $summerB = 0.0; |
||||
|
|
||||
|
// Loop through arguments |
||||
|
$aArgs = Functions::flattenArray($args); |
||||
|
$aCount = 0; |
||||
|
foreach ($aArgs as $arg) { |
||||
|
$arg = self::datatypeAdjustmentBooleans($arg); |
||||
|
|
||||
|
// Is it a numeric value? |
||||
|
if ((is_numeric($arg)) && (!is_string($arg))) { |
||||
|
$summerA += ($arg * $arg); |
||||
|
$summerB += $arg; |
||||
|
++$aCount; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($aCount > 1) { |
||||
|
$summerA *= $aCount; |
||||
|
$summerB *= $summerB; |
||||
|
|
||||
|
return ($summerA - $summerB) / ($aCount * ($aCount - 1)); |
||||
|
} |
||||
|
|
||||
|
return $returnValue; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* VARA. |
||||
|
* |
||||
|
* Estimates variance based on a sample, including numbers, text, and logical values |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* VARA(value1[,value2[, ...]]) |
||||
|
* |
||||
|
* @param mixed ...$args Data values |
||||
|
* |
||||
|
* @return float|string (string if result is an error) |
||||
|
*/ |
||||
|
public static function VARA(...$args) |
||||
|
{ |
||||
|
$returnValue = Functions::DIV0(); |
||||
|
|
||||
|
$summerA = $summerB = 0.0; |
||||
|
|
||||
|
// Loop through arguments |
||||
|
$aArgs = Functions::flattenArrayIndexed($args); |
||||
|
$aCount = 0; |
||||
|
foreach ($aArgs as $k => $arg) { |
||||
|
if ((is_string($arg)) && (Functions::isValue($k))) { |
||||
|
return Functions::VALUE(); |
||||
|
} elseif ((is_string($arg)) && (!Functions::isMatrixValue($k))) { |
||||
|
} else { |
||||
|
// Is it a numeric value? |
||||
|
if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) & ($arg != '')))) { |
||||
|
$arg = self::datatypeAdjustmentAllowStrings($arg); |
||||
|
$summerA += ($arg * $arg); |
||||
|
$summerB += $arg; |
||||
|
++$aCount; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($aCount > 1) { |
||||
|
$summerA *= $aCount; |
||||
|
$summerB *= $summerB; |
||||
|
|
||||
|
return ($summerA - $summerB) / ($aCount * ($aCount - 1)); |
||||
|
} |
||||
|
|
||||
|
return $returnValue; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* VARP. |
||||
|
* |
||||
|
* Calculates variance based on the entire population |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* VARP(value1[,value2[, ...]]) |
||||
|
* |
||||
|
* @param mixed ...$args Data values |
||||
|
* |
||||
|
* @return float|string (string if result is an error) |
||||
|
*/ |
||||
|
public static function VARP(...$args) |
||||
|
{ |
||||
|
// Return value |
||||
|
$returnValue = Functions::DIV0(); |
||||
|
|
||||
|
$summerA = $summerB = 0.0; |
||||
|
|
||||
|
// Loop through arguments |
||||
|
$aArgs = Functions::flattenArray($args); |
||||
|
$aCount = 0; |
||||
|
foreach ($aArgs as $arg) { |
||||
|
$arg = self::datatypeAdjustmentBooleans($arg); |
||||
|
|
||||
|
// Is it a numeric value? |
||||
|
if ((is_numeric($arg)) && (!is_string($arg))) { |
||||
|
$summerA += ($arg * $arg); |
||||
|
$summerB += $arg; |
||||
|
++$aCount; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($aCount > 0) { |
||||
|
$summerA *= $aCount; |
||||
|
$summerB *= $summerB; |
||||
|
|
||||
|
return ($summerA - $summerB) / ($aCount * $aCount); |
||||
|
} |
||||
|
|
||||
|
return $returnValue; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* VARPA. |
||||
|
* |
||||
|
* Calculates variance based on the entire population, including numbers, text, and logical values |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* VARPA(value1[,value2[, ...]]) |
||||
|
* |
||||
|
* @param mixed ...$args Data values |
||||
|
* |
||||
|
* @return float|string (string if result is an error) |
||||
|
*/ |
||||
|
public static function VARPA(...$args) |
||||
|
{ |
||||
|
$returnValue = Functions::DIV0(); |
||||
|
|
||||
|
$summerA = $summerB = 0.0; |
||||
|
|
||||
|
// Loop through arguments |
||||
|
$aArgs = Functions::flattenArrayIndexed($args); |
||||
|
$aCount = 0; |
||||
|
foreach ($aArgs as $k => $arg) { |
||||
|
if ((is_string($arg)) && (Functions::isValue($k))) { |
||||
|
return Functions::VALUE(); |
||||
|
} elseif ((is_string($arg)) && (!Functions::isMatrixValue($k))) { |
||||
|
} else { |
||||
|
// Is it a numeric value? |
||||
|
if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) & ($arg != '')))) { |
||||
|
$arg = self::datatypeAdjustmentAllowStrings($arg); |
||||
|
$summerA += ($arg * $arg); |
||||
|
$summerB += $arg; |
||||
|
++$aCount; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($aCount > 0) { |
||||
|
$summerA *= $aCount; |
||||
|
$summerB *= $summerB; |
||||
|
|
||||
|
return ($summerA - $summerB) / ($aCount * $aCount); |
||||
|
} |
||||
|
|
||||
|
return $returnValue; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,448 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation; |
||||
|
|
||||
|
use DateTimeInterface; |
||||
|
|
||||
|
/** |
||||
|
* @deprecated 1.18.0 |
||||
|
*/ |
||||
|
class TextData |
||||
|
{ |
||||
|
/** |
||||
|
* CHARACTER. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the character() method in the TextData\CharacterConvert class instead |
||||
|
* |
||||
|
* @param string $character Value |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function CHARACTER($character) |
||||
|
{ |
||||
|
return TextData\CharacterConvert::character($character); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TRIMNONPRINTABLE. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the nonPrintable() method in the TextData\Trim class instead |
||||
|
* |
||||
|
* @param mixed $stringValue Value to check |
||||
|
* |
||||
|
* @return null|array|string |
||||
|
*/ |
||||
|
public static function TRIMNONPRINTABLE($stringValue = '') |
||||
|
{ |
||||
|
return TextData\Trim::nonPrintable($stringValue); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TRIMSPACES. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the spaces() method in the TextData\Trim class instead |
||||
|
* |
||||
|
* @param mixed $stringValue Value to check |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function TRIMSPACES($stringValue = '') |
||||
|
{ |
||||
|
return TextData\Trim::spaces($stringValue); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ASCIICODE. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the code() method in the TextData\CharacterConvert class instead |
||||
|
* |
||||
|
* @param array|string $characters Value |
||||
|
* |
||||
|
* @return array|int|string A string if arguments are invalid |
||||
|
*/ |
||||
|
public static function ASCIICODE($characters) |
||||
|
{ |
||||
|
return TextData\CharacterConvert::code($characters); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* CONCATENATE. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the CONCATENATE() method in the TextData\Concatenate class instead |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function CONCATENATE(...$args) |
||||
|
{ |
||||
|
return TextData\Concatenate::CONCATENATE(...$args); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* DOLLAR. |
||||
|
* |
||||
|
* This function converts a number to text using currency format, with the decimals rounded to the specified place. |
||||
|
* The format used is $#,##0.00_);($#,##0.00).. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the DOLLAR() method in the TextData\Format class instead |
||||
|
* |
||||
|
* @param float $value The value to format |
||||
|
* @param int $decimals The number of digits to display to the right of the decimal point. |
||||
|
* If decimals is negative, number is rounded to the left of the decimal point. |
||||
|
* If you omit decimals, it is assumed to be 2 |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function DOLLAR($value = 0, $decimals = 2) |
||||
|
{ |
||||
|
return TextData\Format::DOLLAR($value, $decimals); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* FIND. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the sensitive() method in the TextData\Search class instead |
||||
|
* |
||||
|
* @param array|string $needle The string to look for |
||||
|
* @param array|string $haystack The string in which to look |
||||
|
* @param array|int $offset Offset within $haystack |
||||
|
* |
||||
|
* @return array|int|string |
||||
|
*/ |
||||
|
public static function SEARCHSENSITIVE($needle, $haystack, $offset = 1) |
||||
|
{ |
||||
|
return TextData\Search::sensitive($needle, $haystack, $offset); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SEARCH. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the insensitive() method in the TextData\Search class instead |
||||
|
* |
||||
|
* @param array|string $needle The string to look for |
||||
|
* @param array|string $haystack The string in which to look |
||||
|
* @param array|int $offset Offset within $haystack |
||||
|
* |
||||
|
* @return array|int|string |
||||
|
*/ |
||||
|
public static function SEARCHINSENSITIVE($needle, $haystack, $offset = 1) |
||||
|
{ |
||||
|
return TextData\Search::insensitive($needle, $haystack, $offset); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* FIXEDFORMAT. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the FIXEDFORMAT() method in the TextData\Format class instead |
||||
|
* |
||||
|
* @param mixed $value Value to check |
||||
|
* @param int $decimals |
||||
|
* @param bool $no_commas |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function FIXEDFORMAT($value, $decimals = 2, $no_commas = false) |
||||
|
{ |
||||
|
return TextData\Format::FIXEDFORMAT($value, $decimals, $no_commas); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* LEFT. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the left() method in the TextData\Extract class instead |
||||
|
* |
||||
|
* @param array|string $value Value |
||||
|
* @param array|int $chars Number of characters |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function LEFT($value = '', $chars = 1) |
||||
|
{ |
||||
|
return TextData\Extract::left($value, $chars); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* MID. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the mid() method in the TextData\Extract class instead |
||||
|
* |
||||
|
* @param array|string $value Value |
||||
|
* @param array|int $start Start character |
||||
|
* @param array|int $chars Number of characters |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function MID($value = '', $start = 1, $chars = null) |
||||
|
{ |
||||
|
return TextData\Extract::mid($value, $start, $chars); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* RIGHT. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the right() method in the TextData\Extract class instead |
||||
|
* |
||||
|
* @param array|string $value Value |
||||
|
* @param array|int $chars Number of characters |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function RIGHT($value = '', $chars = 1) |
||||
|
{ |
||||
|
return TextData\Extract::right($value, $chars); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* STRINGLENGTH. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the length() method in the TextData\Text class instead |
||||
|
* |
||||
|
* @param string $value Value |
||||
|
* |
||||
|
* @return array|int |
||||
|
*/ |
||||
|
public static function STRINGLENGTH($value = '') |
||||
|
{ |
||||
|
return TextData\Text::length($value); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* LOWERCASE. |
||||
|
* |
||||
|
* Converts a string value to lower case. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the lower() method in the TextData\CaseConvert class instead |
||||
|
* |
||||
|
* @param array|string $mixedCaseString |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function LOWERCASE($mixedCaseString) |
||||
|
{ |
||||
|
return TextData\CaseConvert::lower($mixedCaseString); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* UPPERCASE. |
||||
|
* |
||||
|
* Converts a string value to upper case. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the upper() method in the TextData\CaseConvert class instead |
||||
|
* |
||||
|
* @param string $mixedCaseString |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function UPPERCASE($mixedCaseString) |
||||
|
{ |
||||
|
return TextData\CaseConvert::upper($mixedCaseString); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* PROPERCASE. |
||||
|
* |
||||
|
* Converts a string value to proper/title case. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the proper() method in the TextData\CaseConvert class instead |
||||
|
* |
||||
|
* @param array|string $mixedCaseString |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function PROPERCASE($mixedCaseString) |
||||
|
{ |
||||
|
return TextData\CaseConvert::proper($mixedCaseString); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* REPLACE. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the replace() method in the TextData\Replace class instead |
||||
|
* |
||||
|
* @param string $oldText String to modify |
||||
|
* @param int $start Start character |
||||
|
* @param int $chars Number of characters |
||||
|
* @param string $newText String to replace in defined position |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function REPLACE($oldText, $start, $chars, $newText) |
||||
|
{ |
||||
|
return TextData\Replace::replace($oldText, $start, $chars, $newText); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SUBSTITUTE. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the substitute() method in the TextData\Replace class instead |
||||
|
* |
||||
|
* @param string $text Value |
||||
|
* @param string $fromText From Value |
||||
|
* @param string $toText To Value |
||||
|
* @param int $instance Instance Number |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function SUBSTITUTE($text = '', $fromText = '', $toText = '', $instance = 0) |
||||
|
{ |
||||
|
return TextData\Replace::substitute($text, $fromText, $toText, $instance); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* RETURNSTRING. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the test() method in the TextData\Text class instead |
||||
|
* |
||||
|
* @param mixed $testValue Value to check |
||||
|
* |
||||
|
* @return null|array|string |
||||
|
*/ |
||||
|
public static function RETURNSTRING($testValue = '') |
||||
|
{ |
||||
|
return TextData\Text::test($testValue); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TEXTFORMAT. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the TEXTFORMAT() method in the TextData\Format class instead |
||||
|
* |
||||
|
* @param mixed $value Value to check |
||||
|
* @param string $format Format mask to use |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function TEXTFORMAT($value, $format) |
||||
|
{ |
||||
|
return TextData\Format::TEXTFORMAT($value, $format); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* VALUE. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the VALUE() method in the TextData\Format class instead |
||||
|
* |
||||
|
* @param mixed $value Value to check |
||||
|
* |
||||
|
* @return array|DateTimeInterface|float|int|string A string if arguments are invalid |
||||
|
*/ |
||||
|
public static function VALUE($value = '') |
||||
|
{ |
||||
|
return TextData\Format::VALUE($value); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* NUMBERVALUE. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the NUMBERVALUE() method in the TextData\Format class instead |
||||
|
* |
||||
|
* @param mixed $value Value to check |
||||
|
* @param string $decimalSeparator decimal separator, defaults to locale defined value |
||||
|
* @param string $groupSeparator group/thosands separator, defaults to locale defined value |
||||
|
* |
||||
|
* @return array|float|string |
||||
|
*/ |
||||
|
public static function NUMBERVALUE($value = '', $decimalSeparator = null, $groupSeparator = null) |
||||
|
{ |
||||
|
return TextData\Format::NUMBERVALUE($value, $decimalSeparator, $groupSeparator); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Compares two text strings and returns TRUE if they are exactly the same, FALSE otherwise. |
||||
|
* EXACT is case-sensitive but ignores formatting differences. |
||||
|
* Use EXACT to test text being entered into a document. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the exact() method in the TextData\Text class instead |
||||
|
* |
||||
|
* @param mixed $value1 |
||||
|
* @param mixed $value2 |
||||
|
* |
||||
|
* @return array|bool |
||||
|
*/ |
||||
|
public static function EXACT($value1, $value2) |
||||
|
{ |
||||
|
return TextData\Text::exact($value1, $value2); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TEXTJOIN. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the TEXTJOIN() method in the TextData\Concatenate class instead |
||||
|
* |
||||
|
* @param mixed $delimiter |
||||
|
* @param mixed $ignoreEmpty |
||||
|
* @param mixed $args |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function TEXTJOIN($delimiter, $ignoreEmpty, ...$args) |
||||
|
{ |
||||
|
return TextData\Concatenate::TEXTJOIN($delimiter, $ignoreEmpty, ...$args); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* REPT. |
||||
|
* |
||||
|
* Returns the result of builtin function repeat after validating args. |
||||
|
* |
||||
|
* @Deprecated 1.18.0 |
||||
|
* |
||||
|
* @see Use the builtinREPT() method in the TextData\Concatenate class instead |
||||
|
* |
||||
|
* @param array|string $str Should be numeric |
||||
|
* @param mixed $number Should be int |
||||
|
* |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public static function builtinREPT($str, $number) |
||||
|
{ |
||||
|
return TextData\Concatenate::builtinREPT($str, $number); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,113 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
|
||||
|
class Replace |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* REPLACE. |
||||
|
* |
||||
|
* @param mixed $oldText The text string value to modify |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $start Integer offset for start character of the replacement |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $chars Integer number of characters to replace from the start offset |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $newText String to replace in the defined position |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|string |
||||
|
* If an array of values is passed for either of the arguments, then the returned result |
||||
|
* will also be an array with matching dimensions |
||||
|
*/ |
||||
|
public static function replace($oldText, $start, $chars, $newText) |
||||
|
{ |
||||
|
if (is_array($oldText) || is_array($start) || is_array($chars) || is_array($newText)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $oldText, $start, $chars, $newText); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$start = Helpers::extractInt($start, 1, 0, true); |
||||
|
$chars = Helpers::extractInt($chars, 0, 0, true); |
||||
|
$oldText = Helpers::extractString($oldText); |
||||
|
$newText = Helpers::extractString($newText); |
||||
|
$left = mb_substr($oldText, 0, $start - 1, 'UTF-8'); |
||||
|
|
||||
|
$right = mb_substr($oldText, $start + $chars - 1, null, 'UTF-8'); |
||||
|
} catch (CalcExp $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return $left . $newText . $right; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SUBSTITUTE. |
||||
|
* |
||||
|
* @param mixed $text The text string value to modify |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $fromText The string value that we want to replace in $text |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $toText The string value that we want to replace with in $text |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $instance Integer instance Number for the occurrence of frmText to change |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|string |
||||
|
* If an array of values is passed for either of the arguments, then the returned result |
||||
|
* will also be an array with matching dimensions |
||||
|
*/ |
||||
|
public static function substitute($text = '', $fromText = '', $toText = '', $instance = null) |
||||
|
{ |
||||
|
if (is_array($text) || is_array($fromText) || is_array($toText) || is_array($instance)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $text, $fromText, $toText, $instance); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$text = Helpers::extractString($text); |
||||
|
$fromText = Helpers::extractString($fromText); |
||||
|
$toText = Helpers::extractString($toText); |
||||
|
if ($instance === null) { |
||||
|
return str_replace($fromText, $toText, $text); |
||||
|
} |
||||
|
if (is_bool($instance)) { |
||||
|
if ($instance === false || Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { |
||||
|
return Functions::Value(); |
||||
|
} |
||||
|
$instance = 1; |
||||
|
} |
||||
|
$instance = Helpers::extractInt($instance, 1, 0, true); |
||||
|
} catch (CalcExp $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
return self::executeSubstitution($text, $fromText, $toText, $instance); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return string |
||||
|
*/ |
||||
|
private static function executeSubstitution(string $text, string $fromText, string $toText, int $instance) |
||||
|
{ |
||||
|
$pos = -1; |
||||
|
while ($instance > 0) { |
||||
|
$pos = mb_strpos($text, $fromText, $pos + 1, 'UTF-8'); |
||||
|
if ($pos === false) { |
||||
|
break; |
||||
|
} |
||||
|
--$instance; |
||||
|
} |
||||
|
|
||||
|
if ($pos !== false) { |
||||
|
return Functions::scalar(self::REPLACE($text, ++$pos, mb_strlen($fromText, 'UTF-8'), $toText)); |
||||
|
} |
||||
|
|
||||
|
return $text; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,97 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; |
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; |
||||
|
|
||||
|
class Search |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* FIND (case sensitive search). |
||||
|
* |
||||
|
* @param mixed $needle The string to look for |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $haystack The string in which to look |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $offset Integer offset within $haystack to start searching from |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|int|string The offset where the first occurrence of needle was found in the haystack |
||||
|
* If an array of values is passed for the $value or $chars arguments, then the returned result |
||||
|
* will also be an array with matching dimensions |
||||
|
*/ |
||||
|
public static function sensitive($needle, $haystack, $offset = 1) |
||||
|
{ |
||||
|
if (is_array($needle) || is_array($haystack) || is_array($offset)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $needle, $haystack, $offset); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$needle = Helpers::extractString($needle); |
||||
|
$haystack = Helpers::extractString($haystack); |
||||
|
$offset = Helpers::extractInt($offset, 1, 0, true); |
||||
|
} catch (CalcExp $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if (StringHelper::countCharacters($haystack) >= $offset) { |
||||
|
if (StringHelper::countCharacters($needle) === 0) { |
||||
|
return $offset; |
||||
|
} |
||||
|
|
||||
|
$pos = mb_strpos($haystack, $needle, --$offset, 'UTF-8'); |
||||
|
if ($pos !== false) { |
||||
|
return ++$pos; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* SEARCH (case insensitive search). |
||||
|
* |
||||
|
* @param mixed $needle The string to look for |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $haystack The string in which to look |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $offset Integer offset within $haystack to start searching from |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|int|string The offset where the first occurrence of needle was found in the haystack |
||||
|
* If an array of values is passed for the $value or $chars arguments, then the returned result |
||||
|
* will also be an array with matching dimensions |
||||
|
*/ |
||||
|
public static function insensitive($needle, $haystack, $offset = 1) |
||||
|
{ |
||||
|
if (is_array($needle) || is_array($haystack) || is_array($offset)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $needle, $haystack, $offset); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$needle = Helpers::extractString($needle); |
||||
|
$haystack = Helpers::extractString($haystack); |
||||
|
$offset = Helpers::extractInt($offset, 1, 0, true); |
||||
|
} catch (CalcExp $e) { |
||||
|
return $e->getMessage(); |
||||
|
} |
||||
|
|
||||
|
if (StringHelper::countCharacters($haystack) >= $offset) { |
||||
|
if (StringHelper::countCharacters($needle) === 0) { |
||||
|
return $offset; |
||||
|
} |
||||
|
|
||||
|
$pos = mb_stripos($haystack, $needle, --$offset, 'UTF-8'); |
||||
|
if ($pos !== false) { |
||||
|
return ++$pos; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
|
||||
|
class Text |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* LEN. |
||||
|
* |
||||
|
* @param mixed $value String Value |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|int |
||||
|
* If an array of values is passed for the argument, then the returned result |
||||
|
* will also be an array with matching dimensions |
||||
|
*/ |
||||
|
public static function length($value = '') |
||||
|
{ |
||||
|
if (is_array($value)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); |
||||
|
} |
||||
|
|
||||
|
$value = Helpers::extractString($value); |
||||
|
|
||||
|
return mb_strlen($value ?? '', 'UTF-8'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Compares two text strings and returns TRUE if they are exactly the same, FALSE otherwise. |
||||
|
* EXACT is case-sensitive but ignores formatting differences. |
||||
|
* Use EXACT to test text being entered into a document. |
||||
|
* |
||||
|
* @param mixed $value1 String Value |
||||
|
* Or can be an array of values |
||||
|
* @param mixed $value2 String Value |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|bool |
||||
|
* If an array of values is passed for either of the arguments, then the returned result |
||||
|
* will also be an array with matching dimensions |
||||
|
*/ |
||||
|
public static function exact($value1, $value2) |
||||
|
{ |
||||
|
if (is_array($value1) || is_array($value2)) { |
||||
|
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value1, $value2); |
||||
|
} |
||||
|
|
||||
|
$value1 = Helpers::extractString($value1); |
||||
|
$value2 = Helpers::extractString($value2); |
||||
|
|
||||
|
return $value2 === $value1; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* RETURNSTRING. |
||||
|
* |
||||
|
* @param mixed $testValue Value to check |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return null|array|string |
||||
|
* If an array of values is passed for the argument, then the returned result |
||||
|
* will also be an array with matching dimensions |
||||
|
*/ |
||||
|
public static function test($testValue = '') |
||||
|
{ |
||||
|
if (is_array($testValue)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $testValue); |
||||
|
} |
||||
|
|
||||
|
if (is_string($testValue)) { |
||||
|
return $testValue; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
||||
|
|
||||
|
class Trim |
||||
|
{ |
||||
|
use ArrayEnabled; |
||||
|
|
||||
|
/** |
||||
|
* CLEAN. |
||||
|
* |
||||
|
* @param mixed $stringValue String Value to check |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return null|array|string |
||||
|
* If an array of values is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function nonPrintable($stringValue = '') |
||||
|
{ |
||||
|
if (is_array($stringValue)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $stringValue); |
||||
|
} |
||||
|
|
||||
|
$stringValue = Helpers::extractString($stringValue); |
||||
|
|
||||
|
return preg_replace('/[\\x00-\\x1f]/', '', "$stringValue"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* TRIM. |
||||
|
* |
||||
|
* @param mixed $stringValue String Value to check |
||||
|
* Or can be an array of values |
||||
|
* |
||||
|
* @return array|string |
||||
|
* If an array of values is passed as the argument, then the returned result will also be an array |
||||
|
* with the same dimensions |
||||
|
*/ |
||||
|
public static function spaces($stringValue = '') |
||||
|
{ |
||||
|
if (is_array($stringValue)) { |
||||
|
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $stringValue); |
||||
|
} |
||||
|
|
||||
|
$stringValue = Helpers::extractString($stringValue); |
||||
|
|
||||
|
return trim(preg_replace('/ +/', ' ', trim("$stringValue", ' ')) ?? '', ' '); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,149 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Token; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Calculation; |
||||
|
|
||||
|
class Stack |
||||
|
{ |
||||
|
/** |
||||
|
* The parser stack for formulae. |
||||
|
* |
||||
|
* @var mixed[] |
||||
|
*/ |
||||
|
private $stack = []; |
||||
|
|
||||
|
/** |
||||
|
* Count of entries in the parser stack. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $count = 0; |
||||
|
|
||||
|
/** |
||||
|
* Return the number of entries on the stack. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function count() |
||||
|
{ |
||||
|
return $this->count; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Push a new entry onto the stack. |
||||
|
* |
||||
|
* @param mixed $type |
||||
|
* @param mixed $value |
||||
|
* @param mixed $reference |
||||
|
* @param null|string $storeKey will store the result under this alias |
||||
|
* @param null|string $onlyIf will only run computation if the matching |
||||
|
* store key is true |
||||
|
* @param null|string $onlyIfNot will only run computation if the matching |
||||
|
* store key is false |
||||
|
*/ |
||||
|
public function push( |
||||
|
$type, |
||||
|
$value, |
||||
|
$reference = null, |
||||
|
$storeKey = null, |
||||
|
$onlyIf = null, |
||||
|
$onlyIfNot = null |
||||
|
): void { |
||||
|
$stackItem = $this->getStackItem($type, $value, $reference, $storeKey, $onlyIf, $onlyIfNot); |
||||
|
|
||||
|
$this->stack[$this->count++] = $stackItem; |
||||
|
|
||||
|
if ($type == 'Function') { |
||||
|
$localeFunction = Calculation::localeFunc($value); |
||||
|
if ($localeFunction != $value) { |
||||
|
$this->stack[($this->count - 1)]['localeValue'] = $localeFunction; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function getStackItem( |
||||
|
$type, |
||||
|
$value, |
||||
|
$reference = null, |
||||
|
$storeKey = null, |
||||
|
$onlyIf = null, |
||||
|
$onlyIfNot = null |
||||
|
) { |
||||
|
$stackItem = [ |
||||
|
'type' => $type, |
||||
|
'value' => $value, |
||||
|
'reference' => $reference, |
||||
|
]; |
||||
|
|
||||
|
if (isset($storeKey)) { |
||||
|
$stackItem['storeKey'] = $storeKey; |
||||
|
} |
||||
|
|
||||
|
if (isset($onlyIf)) { |
||||
|
$stackItem['onlyIf'] = $onlyIf; |
||||
|
} |
||||
|
|
||||
|
if (isset($onlyIfNot)) { |
||||
|
$stackItem['onlyIfNot'] = $onlyIfNot; |
||||
|
} |
||||
|
|
||||
|
return $stackItem; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Pop the last entry from the stack. |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function pop() |
||||
|
{ |
||||
|
if ($this->count > 0) { |
||||
|
return $this->stack[--$this->count]; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return an entry from the stack without removing it. |
||||
|
* |
||||
|
* @param int $n number indicating how far back in the stack we want to look |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function last($n = 1) |
||||
|
{ |
||||
|
if ($this->count - $n < 0) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return $this->stack[$this->count - $n]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Clear the stack. |
||||
|
*/ |
||||
|
public function clear(): void |
||||
|
{ |
||||
|
$this->stack = []; |
||||
|
$this->count = 0; |
||||
|
} |
||||
|
|
||||
|
public function __toString() |
||||
|
{ |
||||
|
$str = 'Stack: '; |
||||
|
foreach ($this->stack as $index => $item) { |
||||
|
if ($index > $this->count - 1) { |
||||
|
break; |
||||
|
} |
||||
|
$value = $item['value'] ?? 'no value'; |
||||
|
while (is_array($value)) { |
||||
|
$value = array_pop($value); |
||||
|
} |
||||
|
$str .= $value . ' |> '; |
||||
|
} |
||||
|
|
||||
|
return $str; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation; |
||||
|
|
||||
|
/** |
||||
|
* @deprecated 1.18.0 |
||||
|
*/ |
||||
|
class Web |
||||
|
{ |
||||
|
/** |
||||
|
* WEBSERVICE. |
||||
|
* |
||||
|
* Returns data from a web service on the Internet or Intranet. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* Webservice(url) |
||||
|
* |
||||
|
* @see Web\Service::webService() |
||||
|
* Use the webService() method in the Web\Service class instead |
||||
|
* |
||||
|
* @return string the output resulting from a call to the webservice |
||||
|
*/ |
||||
|
public static function WEBSERVICE(string $url) |
||||
|
{ |
||||
|
return Web\Service::webService($url); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Calculation\Web; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions; |
||||
|
use PhpOffice\PhpSpreadsheet\Settings; |
||||
|
use Psr\Http\Client\ClientExceptionInterface; |
||||
|
|
||||
|
class Service |
||||
|
{ |
||||
|
/** |
||||
|
* WEBSERVICE. |
||||
|
* |
||||
|
* Returns data from a web service on the Internet or Intranet. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* Webservice(url) |
||||
|
* |
||||
|
* @return string the output resulting from a call to the webservice |
||||
|
*/ |
||||
|
public static function webService(string $url) |
||||
|
{ |
||||
|
$url = trim($url); |
||||
|
if (strlen($url) > 2048) { |
||||
|
return Functions::VALUE(); // Invalid URL length |
||||
|
} |
||||
|
|
||||
|
if (!preg_match('/^http[s]?:\/\//', $url)) { |
||||
|
return Functions::VALUE(); // Invalid protocol |
||||
|
} |
||||
|
|
||||
|
// Get results from the the webservice |
||||
|
$client = Settings::getHttpClient(); |
||||
|
$requestFactory = Settings::getRequestFactory(); |
||||
|
$request = $requestFactory->createRequest('GET', $url); |
||||
|
|
||||
|
try { |
||||
|
$response = $client->sendRequest($request); |
||||
|
} catch (ClientExceptionInterface $e) { |
||||
|
return Functions::VALUE(); // cURL error |
||||
|
} |
||||
|
|
||||
|
if ($response->getStatusCode() != 200) { |
||||
|
return Functions::VALUE(); // cURL error |
||||
|
} |
||||
|
|
||||
|
$output = $response->getBody()->getContents(); |
||||
|
if (strlen($output) > 32767) { |
||||
|
return Functions::VALUE(); // Output not a string or too long |
||||
|
} |
||||
|
|
||||
|
return $output; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* URLENCODE. |
||||
|
* |
||||
|
* Returns data from a web service on the Internet or Intranet. |
||||
|
* |
||||
|
* Excel Function: |
||||
|
* urlEncode(text) |
||||
|
* |
||||
|
* @param mixed $text |
||||
|
* |
||||
|
* @return string the url encoded output |
||||
|
*/ |
||||
|
public static function urlEncode($text) |
||||
|
{ |
||||
|
if (!is_string($text)) { |
||||
|
return Functions::VALUE(); |
||||
|
} |
||||
|
|
||||
|
return str_replace('+', '%20', urlencode($text)); |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -0,0 +1,124 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Cell; |
||||
|
|
||||
|
use DateTimeInterface; |
||||
|
use PhpOffice\PhpSpreadsheet\RichText\RichText; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; |
||||
|
|
||||
|
class StringValueBinder implements IValueBinder |
||||
|
{ |
||||
|
/** |
||||
|
* @var bool |
||||
|
*/ |
||||
|
protected $convertNull = true; |
||||
|
|
||||
|
/** |
||||
|
* @var bool |
||||
|
*/ |
||||
|
protected $convertBoolean = true; |
||||
|
|
||||
|
/** |
||||
|
* @var bool |
||||
|
*/ |
||||
|
protected $convertNumeric = true; |
||||
|
|
||||
|
/** |
||||
|
* @var bool |
||||
|
*/ |
||||
|
protected $convertFormula = true; |
||||
|
|
||||
|
public function setNullConversion(bool $suppressConversion = false): self |
||||
|
{ |
||||
|
$this->convertNull = $suppressConversion; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
public function setBooleanConversion(bool $suppressConversion = false): self |
||||
|
{ |
||||
|
$this->convertBoolean = $suppressConversion; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
public function getBooleanConversion(): bool |
||||
|
{ |
||||
|
return $this->convertBoolean; |
||||
|
} |
||||
|
|
||||
|
public function setNumericConversion(bool $suppressConversion = false): self |
||||
|
{ |
||||
|
$this->convertNumeric = $suppressConversion; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
public function setFormulaConversion(bool $suppressConversion = false): self |
||||
|
{ |
||||
|
$this->convertFormula = $suppressConversion; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
public function setConversionForAllValueTypes(bool $suppressConversion = false): self |
||||
|
{ |
||||
|
$this->convertNull = $suppressConversion; |
||||
|
$this->convertBoolean = $suppressConversion; |
||||
|
$this->convertNumeric = $suppressConversion; |
||||
|
$this->convertFormula = $suppressConversion; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Bind value to a cell. |
||||
|
* |
||||
|
* @param Cell $cell Cell to bind value to |
||||
|
* @param mixed $value Value to bind in cell |
||||
|
*/ |
||||
|
public function bindValue(Cell $cell, $value) |
||||
|
{ |
||||
|
if (is_object($value)) { |
||||
|
return $this->bindObjectValue($cell, $value); |
||||
|
} |
||||
|
|
||||
|
// sanitize UTF-8 strings |
||||
|
if (is_string($value)) { |
||||
|
$value = StringHelper::sanitizeUTF8($value); |
||||
|
} |
||||
|
|
||||
|
if ($value === null && $this->convertNull === false) { |
||||
|
$cell->setValueExplicit($value, DataType::TYPE_NULL); |
||||
|
} elseif (is_bool($value) && $this->convertBoolean === false) { |
||||
|
$cell->setValueExplicit($value, DataType::TYPE_BOOL); |
||||
|
} elseif ((is_int($value) || is_float($value)) && $this->convertNumeric === false) { |
||||
|
$cell->setValueExplicit($value, DataType::TYPE_NUMERIC); |
||||
|
} elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false) { |
||||
|
$cell->setValueExplicit($value, DataType::TYPE_FORMULA); |
||||
|
} else { |
||||
|
if (is_string($value) && strlen($value) > 1 && $value[0] === '=') { |
||||
|
$cell->getStyle()->setQuotePrefix(true); |
||||
|
} |
||||
|
$cell->setValueExplicit((string) $value, DataType::TYPE_STRING); |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
protected function bindObjectValue(Cell $cell, object $value): bool |
||||
|
{ |
||||
|
// Handle any objects that might be injected |
||||
|
if ($value instanceof DateTimeInterface) { |
||||
|
$value = $value->format('Y-m-d H:i:s'); |
||||
|
} elseif ($value instanceof RichText) { |
||||
|
$cell->setValueExplicit($value, DataType::TYPE_INLINE); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
$cell->setValueExplicit((string) $value, DataType::TYPE_STRING); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,109 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Chart; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; |
||||
|
|
||||
|
class PlotArea |
||||
|
{ |
||||
|
/** |
||||
|
* PlotArea Layout. |
||||
|
* |
||||
|
* @var Layout |
||||
|
*/ |
||||
|
private $layout; |
||||
|
|
||||
|
/** |
||||
|
* Plot Series. |
||||
|
* |
||||
|
* @var DataSeries[] |
||||
|
*/ |
||||
|
private $plotSeries = []; |
||||
|
|
||||
|
/** |
||||
|
* Create a new PlotArea. |
||||
|
* |
||||
|
* @param DataSeries[] $plotSeries |
||||
|
*/ |
||||
|
public function __construct(?Layout $layout = null, array $plotSeries = []) |
||||
|
{ |
||||
|
$this->layout = $layout; |
||||
|
$this->plotSeries = $plotSeries; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Layout. |
||||
|
* |
||||
|
* @return Layout |
||||
|
*/ |
||||
|
public function getLayout() |
||||
|
{ |
||||
|
return $this->layout; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Number of Plot Groups. |
||||
|
*/ |
||||
|
public function getPlotGroupCount(): int |
||||
|
{ |
||||
|
return count($this->plotSeries); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Number of Plot Series. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function getPlotSeriesCount() |
||||
|
{ |
||||
|
$seriesCount = 0; |
||||
|
foreach ($this->plotSeries as $plot) { |
||||
|
$seriesCount += $plot->getPlotSeriesCount(); |
||||
|
} |
||||
|
|
||||
|
return $seriesCount; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Plot Series. |
||||
|
* |
||||
|
* @return DataSeries[] |
||||
|
*/ |
||||
|
public function getPlotGroup() |
||||
|
{ |
||||
|
return $this->plotSeries; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Plot Series by Index. |
||||
|
* |
||||
|
* @param mixed $index |
||||
|
* |
||||
|
* @return DataSeries |
||||
|
*/ |
||||
|
public function getPlotGroupByIndex($index) |
||||
|
{ |
||||
|
return $this->plotSeries[$index]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Plot Series. |
||||
|
* |
||||
|
* @param DataSeries[] $plotSeries |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setPlotSeries(array $plotSeries) |
||||
|
{ |
||||
|
$this->plotSeries = $plotSeries; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
public function refresh(Worksheet $worksheet): void |
||||
|
{ |
||||
|
foreach ($this->plotSeries as $plotSeries) { |
||||
|
$plotSeries->refresh($worksheet); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,369 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Chart; |
||||
|
|
||||
|
/** |
||||
|
* Created by PhpStorm. |
||||
|
* User: nhw2h8s |
||||
|
* Date: 7/2/14 |
||||
|
* Time: 5:45 PM. |
||||
|
*/ |
||||
|
abstract class Properties |
||||
|
{ |
||||
|
const |
||||
|
EXCEL_COLOR_TYPE_STANDARD = 'prstClr'; |
||||
|
const EXCEL_COLOR_TYPE_SCHEME = 'schemeClr'; |
||||
|
const EXCEL_COLOR_TYPE_ARGB = 'srgbClr'; |
||||
|
|
||||
|
const |
||||
|
AXIS_LABELS_LOW = 'low'; |
||||
|
const AXIS_LABELS_HIGH = 'high'; |
||||
|
const AXIS_LABELS_NEXT_TO = 'nextTo'; |
||||
|
const AXIS_LABELS_NONE = 'none'; |
||||
|
|
||||
|
const |
||||
|
TICK_MARK_NONE = 'none'; |
||||
|
const TICK_MARK_INSIDE = 'in'; |
||||
|
const TICK_MARK_OUTSIDE = 'out'; |
||||
|
const TICK_MARK_CROSS = 'cross'; |
||||
|
|
||||
|
const |
||||
|
HORIZONTAL_CROSSES_AUTOZERO = 'autoZero'; |
||||
|
const HORIZONTAL_CROSSES_MAXIMUM = 'max'; |
||||
|
|
||||
|
const |
||||
|
FORMAT_CODE_GENERAL = 'General'; |
||||
|
const FORMAT_CODE_NUMBER = '#,##0.00'; |
||||
|
const FORMAT_CODE_CURRENCY = '$#,##0.00'; |
||||
|
const FORMAT_CODE_ACCOUNTING = '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)'; |
||||
|
const FORMAT_CODE_DATE = 'm/d/yyyy'; |
||||
|
const FORMAT_CODE_TIME = '[$-F400]h:mm:ss AM/PM'; |
||||
|
const FORMAT_CODE_PERCENTAGE = '0.00%'; |
||||
|
const FORMAT_CODE_FRACTION = '# ?/?'; |
||||
|
const FORMAT_CODE_SCIENTIFIC = '0.00E+00'; |
||||
|
const FORMAT_CODE_TEXT = '@'; |
||||
|
const FORMAT_CODE_SPECIAL = '00000'; |
||||
|
|
||||
|
const |
||||
|
ORIENTATION_NORMAL = 'minMax'; |
||||
|
const ORIENTATION_REVERSED = 'maxMin'; |
||||
|
|
||||
|
const |
||||
|
LINE_STYLE_COMPOUND_SIMPLE = 'sng'; |
||||
|
const LINE_STYLE_COMPOUND_DOUBLE = 'dbl'; |
||||
|
const LINE_STYLE_COMPOUND_THICKTHIN = 'thickThin'; |
||||
|
const LINE_STYLE_COMPOUND_THINTHICK = 'thinThick'; |
||||
|
const LINE_STYLE_COMPOUND_TRIPLE = 'tri'; |
||||
|
const LINE_STYLE_DASH_SOLID = 'solid'; |
||||
|
const LINE_STYLE_DASH_ROUND_DOT = 'sysDot'; |
||||
|
const LINE_STYLE_DASH_SQUERE_DOT = 'sysDash'; |
||||
|
const LINE_STYPE_DASH_DASH = 'dash'; |
||||
|
const LINE_STYLE_DASH_DASH_DOT = 'dashDot'; |
||||
|
const LINE_STYLE_DASH_LONG_DASH = 'lgDash'; |
||||
|
const LINE_STYLE_DASH_LONG_DASH_DOT = 'lgDashDot'; |
||||
|
const LINE_STYLE_DASH_LONG_DASH_DOT_DOT = 'lgDashDotDot'; |
||||
|
const LINE_STYLE_CAP_SQUARE = 'sq'; |
||||
|
const LINE_STYLE_CAP_ROUND = 'rnd'; |
||||
|
const LINE_STYLE_CAP_FLAT = 'flat'; |
||||
|
const LINE_STYLE_JOIN_ROUND = 'bevel'; |
||||
|
const LINE_STYLE_JOIN_MITER = 'miter'; |
||||
|
const LINE_STYLE_JOIN_BEVEL = 'bevel'; |
||||
|
const LINE_STYLE_ARROW_TYPE_NOARROW = null; |
||||
|
const LINE_STYLE_ARROW_TYPE_ARROW = 'triangle'; |
||||
|
const LINE_STYLE_ARROW_TYPE_OPEN = 'arrow'; |
||||
|
const LINE_STYLE_ARROW_TYPE_STEALTH = 'stealth'; |
||||
|
const LINE_STYLE_ARROW_TYPE_DIAMOND = 'diamond'; |
||||
|
const LINE_STYLE_ARROW_TYPE_OVAL = 'oval'; |
||||
|
const LINE_STYLE_ARROW_SIZE_1 = 1; |
||||
|
const LINE_STYLE_ARROW_SIZE_2 = 2; |
||||
|
const LINE_STYLE_ARROW_SIZE_3 = 3; |
||||
|
const LINE_STYLE_ARROW_SIZE_4 = 4; |
||||
|
const LINE_STYLE_ARROW_SIZE_5 = 5; |
||||
|
const LINE_STYLE_ARROW_SIZE_6 = 6; |
||||
|
const LINE_STYLE_ARROW_SIZE_7 = 7; |
||||
|
const LINE_STYLE_ARROW_SIZE_8 = 8; |
||||
|
const LINE_STYLE_ARROW_SIZE_9 = 9; |
||||
|
|
||||
|
const |
||||
|
SHADOW_PRESETS_NOSHADOW = null; |
||||
|
const SHADOW_PRESETS_OUTER_BOTTTOM_RIGHT = 1; |
||||
|
const SHADOW_PRESETS_OUTER_BOTTOM = 2; |
||||
|
const SHADOW_PRESETS_OUTER_BOTTOM_LEFT = 3; |
||||
|
const SHADOW_PRESETS_OUTER_RIGHT = 4; |
||||
|
const SHADOW_PRESETS_OUTER_CENTER = 5; |
||||
|
const SHADOW_PRESETS_OUTER_LEFT = 6; |
||||
|
const SHADOW_PRESETS_OUTER_TOP_RIGHT = 7; |
||||
|
const SHADOW_PRESETS_OUTER_TOP = 8; |
||||
|
const SHADOW_PRESETS_OUTER_TOP_LEFT = 9; |
||||
|
const SHADOW_PRESETS_INNER_BOTTTOM_RIGHT = 10; |
||||
|
const SHADOW_PRESETS_INNER_BOTTOM = 11; |
||||
|
const SHADOW_PRESETS_INNER_BOTTOM_LEFT = 12; |
||||
|
const SHADOW_PRESETS_INNER_RIGHT = 13; |
||||
|
const SHADOW_PRESETS_INNER_CENTER = 14; |
||||
|
const SHADOW_PRESETS_INNER_LEFT = 15; |
||||
|
const SHADOW_PRESETS_INNER_TOP_RIGHT = 16; |
||||
|
const SHADOW_PRESETS_INNER_TOP = 17; |
||||
|
const SHADOW_PRESETS_INNER_TOP_LEFT = 18; |
||||
|
const SHADOW_PRESETS_PERSPECTIVE_BELOW = 19; |
||||
|
const SHADOW_PRESETS_PERSPECTIVE_UPPER_RIGHT = 20; |
||||
|
const SHADOW_PRESETS_PERSPECTIVE_UPPER_LEFT = 21; |
||||
|
const SHADOW_PRESETS_PERSPECTIVE_LOWER_RIGHT = 22; |
||||
|
const SHADOW_PRESETS_PERSPECTIVE_LOWER_LEFT = 23; |
||||
|
|
||||
|
/** |
||||
|
* @param float $width |
||||
|
* |
||||
|
* @return float |
||||
|
*/ |
||||
|
protected function getExcelPointsWidth($width) |
||||
|
{ |
||||
|
return $width * 12700; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param float $angle |
||||
|
* |
||||
|
* @return float |
||||
|
*/ |
||||
|
protected function getExcelPointsAngle($angle) |
||||
|
{ |
||||
|
return $angle * 60000; |
||||
|
} |
||||
|
|
||||
|
protected function getTrueAlpha($alpha) |
||||
|
{ |
||||
|
return (string) 100 - $alpha . '000'; |
||||
|
} |
||||
|
|
||||
|
protected function setColorProperties($color, $alpha, $colorType) |
||||
|
{ |
||||
|
return [ |
||||
|
'type' => (string) $colorType, |
||||
|
'value' => (string) $color, |
||||
|
'alpha' => (string) $this->getTrueAlpha($alpha), |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
protected function getLineStyleArrowSize($arraySelector, $arrayKaySelector) |
||||
|
{ |
||||
|
$sizes = [ |
||||
|
1 => ['w' => 'sm', 'len' => 'sm'], |
||||
|
2 => ['w' => 'sm', 'len' => 'med'], |
||||
|
3 => ['w' => 'sm', 'len' => 'lg'], |
||||
|
4 => ['w' => 'med', 'len' => 'sm'], |
||||
|
5 => ['w' => 'med', 'len' => 'med'], |
||||
|
6 => ['w' => 'med', 'len' => 'lg'], |
||||
|
7 => ['w' => 'lg', 'len' => 'sm'], |
||||
|
8 => ['w' => 'lg', 'len' => 'med'], |
||||
|
9 => ['w' => 'lg', 'len' => 'lg'], |
||||
|
]; |
||||
|
|
||||
|
return $sizes[$arraySelector][$arrayKaySelector]; |
||||
|
} |
||||
|
|
||||
|
protected function getShadowPresetsMap($presetsOption) |
||||
|
{ |
||||
|
$presets_options = [ |
||||
|
//OUTER |
||||
|
1 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '50800', |
||||
|
'distance' => '38100', |
||||
|
'direction' => '2700000', |
||||
|
'algn' => 'tl', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
2 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '50800', |
||||
|
'distance' => '38100', |
||||
|
'direction' => '5400000', |
||||
|
'algn' => 't', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
3 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '50800', |
||||
|
'distance' => '38100', |
||||
|
'direction' => '8100000', |
||||
|
'algn' => 'tr', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
4 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '50800', |
||||
|
'distance' => '38100', |
||||
|
'algn' => 'l', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
5 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'size' => [ |
||||
|
'sx' => '102000', |
||||
|
'sy' => '102000', |
||||
|
], |
||||
|
'blur' => '63500', |
||||
|
'distance' => '38100', |
||||
|
'algn' => 'ctr', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
6 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '50800', |
||||
|
'distance' => '38100', |
||||
|
'direction' => '10800000', |
||||
|
'algn' => 'r', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
7 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '50800', |
||||
|
'distance' => '38100', |
||||
|
'direction' => '18900000', |
||||
|
'algn' => 'bl', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
8 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '50800', |
||||
|
'distance' => '38100', |
||||
|
'direction' => '16200000', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
9 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '50800', |
||||
|
'distance' => '38100', |
||||
|
'direction' => '13500000', |
||||
|
'algn' => 'br', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
//INNER |
||||
|
10 => [ |
||||
|
'effect' => 'innerShdw', |
||||
|
'blur' => '63500', |
||||
|
'distance' => '50800', |
||||
|
'direction' => '2700000', |
||||
|
], |
||||
|
11 => [ |
||||
|
'effect' => 'innerShdw', |
||||
|
'blur' => '63500', |
||||
|
'distance' => '50800', |
||||
|
'direction' => '5400000', |
||||
|
], |
||||
|
12 => [ |
||||
|
'effect' => 'innerShdw', |
||||
|
'blur' => '63500', |
||||
|
'distance' => '50800', |
||||
|
'direction' => '8100000', |
||||
|
], |
||||
|
13 => [ |
||||
|
'effect' => 'innerShdw', |
||||
|
'blur' => '63500', |
||||
|
'distance' => '50800', |
||||
|
], |
||||
|
14 => [ |
||||
|
'effect' => 'innerShdw', |
||||
|
'blur' => '114300', |
||||
|
], |
||||
|
15 => [ |
||||
|
'effect' => 'innerShdw', |
||||
|
'blur' => '63500', |
||||
|
'distance' => '50800', |
||||
|
'direction' => '10800000', |
||||
|
], |
||||
|
16 => [ |
||||
|
'effect' => 'innerShdw', |
||||
|
'blur' => '63500', |
||||
|
'distance' => '50800', |
||||
|
'direction' => '18900000', |
||||
|
], |
||||
|
17 => [ |
||||
|
'effect' => 'innerShdw', |
||||
|
'blur' => '63500', |
||||
|
'distance' => '50800', |
||||
|
'direction' => '16200000', |
||||
|
], |
||||
|
18 => [ |
||||
|
'effect' => 'innerShdw', |
||||
|
'blur' => '63500', |
||||
|
'distance' => '50800', |
||||
|
'direction' => '13500000', |
||||
|
], |
||||
|
//perspective |
||||
|
19 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '152400', |
||||
|
'distance' => '317500', |
||||
|
'size' => [ |
||||
|
'sx' => '90000', |
||||
|
'sy' => '-19000', |
||||
|
], |
||||
|
'direction' => '5400000', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
20 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '76200', |
||||
|
'direction' => '18900000', |
||||
|
'size' => [ |
||||
|
'sy' => '23000', |
||||
|
'kx' => '-1200000', |
||||
|
], |
||||
|
'algn' => 'bl', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
21 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '76200', |
||||
|
'direction' => '13500000', |
||||
|
'size' => [ |
||||
|
'sy' => '23000', |
||||
|
'kx' => '1200000', |
||||
|
], |
||||
|
'algn' => 'br', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
22 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '76200', |
||||
|
'distance' => '12700', |
||||
|
'direction' => '2700000', |
||||
|
'size' => [ |
||||
|
'sy' => '-23000', |
||||
|
'kx' => '-800400', |
||||
|
], |
||||
|
'algn' => 'bl', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
23 => [ |
||||
|
'effect' => 'outerShdw', |
||||
|
'blur' => '76200', |
||||
|
'distance' => '12700', |
||||
|
'direction' => '8100000', |
||||
|
'size' => [ |
||||
|
'sy' => '-23000', |
||||
|
'kx' => '800400', |
||||
|
], |
||||
|
'algn' => 'br', |
||||
|
'rotWithShape' => '0', |
||||
|
], |
||||
|
]; |
||||
|
|
||||
|
return $presets_options[$presetsOption]; |
||||
|
} |
||||
|
|
||||
|
protected function getArrayElementsValue($properties, $elements) |
||||
|
{ |
||||
|
$reference = &$properties; |
||||
|
if (!is_array($elements)) { |
||||
|
return $reference[$elements]; |
||||
|
} |
||||
|
|
||||
|
foreach ($elements as $keys) { |
||||
|
$reference = &$reference[$keys]; |
||||
|
} |
||||
|
|
||||
|
return $reference; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
ChartDirector |
||||
|
https://www.advsofteng.com/cdphp.html |
||||
|
|
||||
|
GraPHPite |
||||
|
http://graphpite.sourceforge.net/ |
||||
|
|
||||
|
JpGraph |
||||
|
http://www.aditus.nu/jpgraph/ |
||||
|
|
||||
|
LibChart |
||||
|
https://naku.dohcrew.com/libchart/pages/introduction/ |
||||
|
|
||||
|
pChart |
||||
|
http://pchart.sourceforge.net/ |
||||
|
|
||||
|
TeeChart |
||||
|
https://www.steema.com/ |
||||
|
|
||||
|
PHPGraphLib |
||||
|
http://www.ebrueggeman.com/phpgraphlib |
||||
@ -0,0 +1,90 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Chart; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\RichText\RichText; |
||||
|
|
||||
|
class Title |
||||
|
{ |
||||
|
/** |
||||
|
* Title Caption. |
||||
|
* |
||||
|
* @var array|RichText|string |
||||
|
*/ |
||||
|
private $caption = ''; |
||||
|
|
||||
|
/** |
||||
|
* Title Layout. |
||||
|
* |
||||
|
* @var Layout |
||||
|
*/ |
||||
|
private $layout; |
||||
|
|
||||
|
/** |
||||
|
* Create a new Title. |
||||
|
* |
||||
|
* @param array|RichText|string $caption |
||||
|
*/ |
||||
|
public function __construct($caption = '', ?Layout $layout = null) |
||||
|
{ |
||||
|
$this->caption = $caption; |
||||
|
$this->layout = $layout; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get caption. |
||||
|
* |
||||
|
* @return array|RichText|string |
||||
|
*/ |
||||
|
public function getCaption() |
||||
|
{ |
||||
|
return $this->caption; |
||||
|
} |
||||
|
|
||||
|
public function getCaptionText(): string |
||||
|
{ |
||||
|
$caption = $this->caption; |
||||
|
if (is_string($caption)) { |
||||
|
return $caption; |
||||
|
} |
||||
|
if ($caption instanceof RichText) { |
||||
|
return $caption->getPlainText(); |
||||
|
} |
||||
|
$retVal = ''; |
||||
|
foreach ($caption as $textx) { |
||||
|
/** @var RichText|string */ |
||||
|
$text = $textx; |
||||
|
if ($text instanceof RichText) { |
||||
|
$retVal .= $text->getPlainText(); |
||||
|
} else { |
||||
|
$retVal .= $text; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $retVal; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set caption. |
||||
|
* |
||||
|
* @param array|RichText|string $caption |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setCaption($caption) |
||||
|
{ |
||||
|
$this->caption = $caption; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Layout. |
||||
|
* |
||||
|
* @return Layout |
||||
|
*/ |
||||
|
public function getLayout() |
||||
|
{ |
||||
|
return $this->layout; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,537 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Document; |
||||
|
|
||||
|
use DateTime; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\IntOrFloat; |
||||
|
|
||||
|
class Properties |
||||
|
{ |
||||
|
/** constants */ |
||||
|
public const PROPERTY_TYPE_BOOLEAN = 'b'; |
||||
|
public const PROPERTY_TYPE_INTEGER = 'i'; |
||||
|
public const PROPERTY_TYPE_FLOAT = 'f'; |
||||
|
public const PROPERTY_TYPE_DATE = 'd'; |
||||
|
public const PROPERTY_TYPE_STRING = 's'; |
||||
|
public const PROPERTY_TYPE_UNKNOWN = 'u'; |
||||
|
|
||||
|
private const VALID_PROPERTY_TYPE_LIST = [ |
||||
|
self::PROPERTY_TYPE_BOOLEAN, |
||||
|
self::PROPERTY_TYPE_INTEGER, |
||||
|
self::PROPERTY_TYPE_FLOAT, |
||||
|
self::PROPERTY_TYPE_DATE, |
||||
|
self::PROPERTY_TYPE_STRING, |
||||
|
]; |
||||
|
|
||||
|
/** |
||||
|
* Creator. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $creator = 'Unknown Creator'; |
||||
|
|
||||
|
/** |
||||
|
* LastModifiedBy. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $lastModifiedBy; |
||||
|
|
||||
|
/** |
||||
|
* Created. |
||||
|
* |
||||
|
* @var float|int |
||||
|
*/ |
||||
|
private $created; |
||||
|
|
||||
|
/** |
||||
|
* Modified. |
||||
|
* |
||||
|
* @var float|int |
||||
|
*/ |
||||
|
private $modified; |
||||
|
|
||||
|
/** |
||||
|
* Title. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $title = 'Untitled Spreadsheet'; |
||||
|
|
||||
|
/** |
||||
|
* Description. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $description = ''; |
||||
|
|
||||
|
/** |
||||
|
* Subject. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $subject = ''; |
||||
|
|
||||
|
/** |
||||
|
* Keywords. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $keywords = ''; |
||||
|
|
||||
|
/** |
||||
|
* Category. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $category = ''; |
||||
|
|
||||
|
/** |
||||
|
* Manager. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $manager = ''; |
||||
|
|
||||
|
/** |
||||
|
* Company. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $company = ''; |
||||
|
|
||||
|
/** |
||||
|
* Custom Properties. |
||||
|
* |
||||
|
* @var array{value: mixed, type: string}[] |
||||
|
*/ |
||||
|
private $customProperties = []; |
||||
|
|
||||
|
/** |
||||
|
* Create a new Document Properties instance. |
||||
|
*/ |
||||
|
public function __construct() |
||||
|
{ |
||||
|
// Initialise values |
||||
|
$this->lastModifiedBy = $this->creator; |
||||
|
$this->created = self::intOrFloatTimestamp(null); |
||||
|
$this->modified = self::intOrFloatTimestamp(null); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Creator. |
||||
|
*/ |
||||
|
public function getCreator(): string |
||||
|
{ |
||||
|
return $this->creator; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Creator. |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setCreator(string $creator): self |
||||
|
{ |
||||
|
$this->creator = $creator; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Last Modified By. |
||||
|
*/ |
||||
|
public function getLastModifiedBy(): string |
||||
|
{ |
||||
|
return $this->lastModifiedBy; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Last Modified By. |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setLastModifiedBy(string $modifiedBy): self |
||||
|
{ |
||||
|
$this->lastModifiedBy = $modifiedBy; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param null|float|int|string $timestamp |
||||
|
* |
||||
|
* @return float|int |
||||
|
*/ |
||||
|
private static function intOrFloatTimestamp($timestamp) |
||||
|
{ |
||||
|
if ($timestamp === null) { |
||||
|
$timestamp = (float) (new DateTime())->format('U'); |
||||
|
} elseif (is_string($timestamp)) { |
||||
|
if (is_numeric($timestamp)) { |
||||
|
$timestamp = (float) $timestamp; |
||||
|
} else { |
||||
|
$timestamp = preg_replace('/[.][0-9]*$/', '', $timestamp) ?? ''; |
||||
|
$timestamp = preg_replace('/^(\\d{4})- (\\d)/', '$1-0$2', $timestamp) ?? ''; |
||||
|
$timestamp = preg_replace('/^(\\d{4}-\\d{2})- (\\d)/', '$1-0$2', $timestamp) ?? ''; |
||||
|
$timestamp = (float) (new DateTime($timestamp))->format('U'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return IntOrFloat::evaluate($timestamp); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Created. |
||||
|
* |
||||
|
* @return float|int |
||||
|
*/ |
||||
|
public function getCreated() |
||||
|
{ |
||||
|
return $this->created; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Created. |
||||
|
* |
||||
|
* @param null|float|int|string $timestamp |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setCreated($timestamp): self |
||||
|
{ |
||||
|
$this->created = self::intOrFloatTimestamp($timestamp); |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Modified. |
||||
|
* |
||||
|
* @return float|int |
||||
|
*/ |
||||
|
public function getModified() |
||||
|
{ |
||||
|
return $this->modified; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Modified. |
||||
|
* |
||||
|
* @param null|float|int|string $timestamp |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setModified($timestamp): self |
||||
|
{ |
||||
|
$this->modified = self::intOrFloatTimestamp($timestamp); |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Title. |
||||
|
*/ |
||||
|
public function getTitle(): string |
||||
|
{ |
||||
|
return $this->title; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Title. |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setTitle(string $title): self |
||||
|
{ |
||||
|
$this->title = $title; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Description. |
||||
|
*/ |
||||
|
public function getDescription(): string |
||||
|
{ |
||||
|
return $this->description; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Description. |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setDescription(string $description): self |
||||
|
{ |
||||
|
$this->description = $description; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Subject. |
||||
|
*/ |
||||
|
public function getSubject(): string |
||||
|
{ |
||||
|
return $this->subject; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Subject. |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setSubject(string $subject): self |
||||
|
{ |
||||
|
$this->subject = $subject; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Keywords. |
||||
|
*/ |
||||
|
public function getKeywords(): string |
||||
|
{ |
||||
|
return $this->keywords; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Keywords. |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setKeywords(string $keywords): self |
||||
|
{ |
||||
|
$this->keywords = $keywords; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Category. |
||||
|
*/ |
||||
|
public function getCategory(): string |
||||
|
{ |
||||
|
return $this->category; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Category. |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setCategory(string $category): self |
||||
|
{ |
||||
|
$this->category = $category; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Company. |
||||
|
*/ |
||||
|
public function getCompany(): string |
||||
|
{ |
||||
|
return $this->company; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Company. |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setCompany(string $company): self |
||||
|
{ |
||||
|
$this->company = $company; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Manager. |
||||
|
*/ |
||||
|
public function getManager(): string |
||||
|
{ |
||||
|
return $this->manager; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Manager. |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setManager(string $manager): self |
||||
|
{ |
||||
|
$this->manager = $manager; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get a List of Custom Property Names. |
||||
|
* |
||||
|
* @return string[] |
||||
|
*/ |
||||
|
public function getCustomProperties(): array |
||||
|
{ |
||||
|
return array_keys($this->customProperties); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if a Custom Property is defined. |
||||
|
*/ |
||||
|
public function isCustomPropertySet(string $propertyName): bool |
||||
|
{ |
||||
|
return array_key_exists($propertyName, $this->customProperties); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get a Custom Property Value. |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function getCustomPropertyValue(string $propertyName) |
||||
|
{ |
||||
|
if (isset($this->customProperties[$propertyName])) { |
||||
|
return $this->customProperties[$propertyName]['value']; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get a Custom Property Type. |
||||
|
* |
||||
|
* @return null|string |
||||
|
*/ |
||||
|
public function getCustomPropertyType(string $propertyName) |
||||
|
{ |
||||
|
return $this->customProperties[$propertyName]['type'] ?? null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $propertyValue |
||||
|
*/ |
||||
|
private function identifyPropertyType($propertyValue): string |
||||
|
{ |
||||
|
if (is_float($propertyValue)) { |
||||
|
return self::PROPERTY_TYPE_FLOAT; |
||||
|
} |
||||
|
if (is_int($propertyValue)) { |
||||
|
return self::PROPERTY_TYPE_INTEGER; |
||||
|
} |
||||
|
if (is_bool($propertyValue)) { |
||||
|
return self::PROPERTY_TYPE_BOOLEAN; |
||||
|
} |
||||
|
|
||||
|
return self::PROPERTY_TYPE_STRING; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set a Custom Property. |
||||
|
* |
||||
|
* @param mixed $propertyValue |
||||
|
* @param string $propertyType |
||||
|
* 'i' : Integer |
||||
|
* 'f' : Floating Point |
||||
|
* 's' : String |
||||
|
* 'd' : Date/Time |
||||
|
* 'b' : Boolean |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setCustomProperty(string $propertyName, $propertyValue = '', $propertyType = null): self |
||||
|
{ |
||||
|
if (($propertyType === null) || (!in_array($propertyType, self::VALID_PROPERTY_TYPE_LIST))) { |
||||
|
$propertyType = $this->identifyPropertyType($propertyValue); |
||||
|
} |
||||
|
|
||||
|
if (!is_object($propertyValue)) { |
||||
|
$this->customProperties[$propertyName] = [ |
||||
|
'value' => self::convertProperty($propertyValue, $propertyType), |
||||
|
'type' => $propertyType, |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
private const PROPERTY_TYPE_ARRAY = [ |
||||
|
'i' => self::PROPERTY_TYPE_INTEGER, // Integer |
||||
|
'i1' => self::PROPERTY_TYPE_INTEGER, // 1-Byte Signed Integer |
||||
|
'i2' => self::PROPERTY_TYPE_INTEGER, // 2-Byte Signed Integer |
||||
|
'i4' => self::PROPERTY_TYPE_INTEGER, // 4-Byte Signed Integer |
||||
|
'i8' => self::PROPERTY_TYPE_INTEGER, // 8-Byte Signed Integer |
||||
|
'int' => self::PROPERTY_TYPE_INTEGER, // Integer |
||||
|
'ui1' => self::PROPERTY_TYPE_INTEGER, // 1-Byte Unsigned Integer |
||||
|
'ui2' => self::PROPERTY_TYPE_INTEGER, // 2-Byte Unsigned Integer |
||||
|
'ui4' => self::PROPERTY_TYPE_INTEGER, // 4-Byte Unsigned Integer |
||||
|
'ui8' => self::PROPERTY_TYPE_INTEGER, // 8-Byte Unsigned Integer |
||||
|
'uint' => self::PROPERTY_TYPE_INTEGER, // Unsigned Integer |
||||
|
'f' => self::PROPERTY_TYPE_FLOAT, // Real Number |
||||
|
'r4' => self::PROPERTY_TYPE_FLOAT, // 4-Byte Real Number |
||||
|
'r8' => self::PROPERTY_TYPE_FLOAT, // 8-Byte Real Number |
||||
|
'decimal' => self::PROPERTY_TYPE_FLOAT, // Decimal |
||||
|
's' => self::PROPERTY_TYPE_STRING, // String |
||||
|
'empty' => self::PROPERTY_TYPE_STRING, // Empty |
||||
|
'null' => self::PROPERTY_TYPE_STRING, // Null |
||||
|
'lpstr' => self::PROPERTY_TYPE_STRING, // LPSTR |
||||
|
'lpwstr' => self::PROPERTY_TYPE_STRING, // LPWSTR |
||||
|
'bstr' => self::PROPERTY_TYPE_STRING, // Basic String |
||||
|
'd' => self::PROPERTY_TYPE_DATE, // Date and Time |
||||
|
'date' => self::PROPERTY_TYPE_DATE, // Date and Time |
||||
|
'filetime' => self::PROPERTY_TYPE_DATE, // File Time |
||||
|
'b' => self::PROPERTY_TYPE_BOOLEAN, // Boolean |
||||
|
'bool' => self::PROPERTY_TYPE_BOOLEAN, // Boolean |
||||
|
]; |
||||
|
|
||||
|
private const SPECIAL_TYPES = [ |
||||
|
'empty' => '', |
||||
|
'null' => null, |
||||
|
]; |
||||
|
|
||||
|
/** |
||||
|
* Convert property to form desired by Excel. |
||||
|
* |
||||
|
* @param mixed $propertyValue |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public static function convertProperty($propertyValue, string $propertyType) |
||||
|
{ |
||||
|
return self::SPECIAL_TYPES[$propertyType] ?? self::convertProperty2($propertyValue, $propertyType); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert property to form desired by Excel. |
||||
|
* |
||||
|
* @param mixed $propertyValue |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
private static function convertProperty2($propertyValue, string $type) |
||||
|
{ |
||||
|
$propertyType = self::convertPropertyType($type); |
||||
|
switch ($propertyType) { |
||||
|
case self::PROPERTY_TYPE_INTEGER: |
||||
|
$intValue = (int) $propertyValue; |
||||
|
|
||||
|
return ($type[0] === 'u') ? abs($intValue) : $intValue; |
||||
|
case self::PROPERTY_TYPE_FLOAT: |
||||
|
return (float) $propertyValue; |
||||
|
case self::PROPERTY_TYPE_DATE: |
||||
|
return self::intOrFloatTimestamp($propertyValue); |
||||
|
case self::PROPERTY_TYPE_BOOLEAN: |
||||
|
return is_bool($propertyValue) ? $propertyValue : ($propertyValue === 'true'); |
||||
|
default: // includes string |
||||
|
return $propertyValue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static function convertPropertyType(string $propertyType): string |
||||
|
{ |
||||
|
return self::PROPERTY_TYPE_ARRAY[$propertyType] ?? self::PROPERTY_TYPE_UNKNOWN; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,152 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Document; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Shared\PasswordHasher; |
||||
|
|
||||
|
class Security |
||||
|
{ |
||||
|
/** |
||||
|
* LockRevision. |
||||
|
* |
||||
|
* @var bool |
||||
|
*/ |
||||
|
private $lockRevision = false; |
||||
|
|
||||
|
/** |
||||
|
* LockStructure. |
||||
|
* |
||||
|
* @var bool |
||||
|
*/ |
||||
|
private $lockStructure = false; |
||||
|
|
||||
|
/** |
||||
|
* LockWindows. |
||||
|
* |
||||
|
* @var bool |
||||
|
*/ |
||||
|
private $lockWindows = false; |
||||
|
|
||||
|
/** |
||||
|
* RevisionsPassword. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $revisionsPassword = ''; |
||||
|
|
||||
|
/** |
||||
|
* WorkbookPassword. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $workbookPassword = ''; |
||||
|
|
||||
|
/** |
||||
|
* Create a new Document Security instance. |
||||
|
*/ |
||||
|
public function __construct() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Is some sort of document security enabled? |
||||
|
*/ |
||||
|
public function isSecurityEnabled(): bool |
||||
|
{ |
||||
|
return $this->lockRevision || |
||||
|
$this->lockStructure || |
||||
|
$this->lockWindows; |
||||
|
} |
||||
|
|
||||
|
public function getLockRevision(): bool |
||||
|
{ |
||||
|
return $this->lockRevision; |
||||
|
} |
||||
|
|
||||
|
public function setLockRevision(?bool $locked): self |
||||
|
{ |
||||
|
if ($locked !== null) { |
||||
|
$this->lockRevision = $locked; |
||||
|
} |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
public function getLockStructure(): bool |
||||
|
{ |
||||
|
return $this->lockStructure; |
||||
|
} |
||||
|
|
||||
|
public function setLockStructure(?bool $locked): self |
||||
|
{ |
||||
|
if ($locked !== null) { |
||||
|
$this->lockStructure = $locked; |
||||
|
} |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
public function getLockWindows(): bool |
||||
|
{ |
||||
|
return $this->lockWindows; |
||||
|
} |
||||
|
|
||||
|
public function setLockWindows(?bool $locked): self |
||||
|
{ |
||||
|
if ($locked !== null) { |
||||
|
$this->lockWindows = $locked; |
||||
|
} |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
public function getRevisionsPassword(): string |
||||
|
{ |
||||
|
return $this->revisionsPassword; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set RevisionsPassword. |
||||
|
* |
||||
|
* @param string $password |
||||
|
* @param bool $alreadyHashed If the password has already been hashed, set this to true |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setRevisionsPassword(?string $password, bool $alreadyHashed = false) |
||||
|
{ |
||||
|
if ($password !== null) { |
||||
|
if (!$alreadyHashed) { |
||||
|
$password = PasswordHasher::hashPassword($password); |
||||
|
} |
||||
|
$this->revisionsPassword = $password; |
||||
|
} |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
public function getWorkbookPassword(): string |
||||
|
{ |
||||
|
return $this->workbookPassword; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set WorkbookPassword. |
||||
|
* |
||||
|
* @param string $password |
||||
|
* @param bool $alreadyHashed If the password has already been hashed, set this to true |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setWorkbookPassword(?string $password, bool $alreadyHashed = false) |
||||
|
{ |
||||
|
if ($password !== null) { |
||||
|
if (!$alreadyHashed) { |
||||
|
$password = PasswordHasher::hashPassword($password); |
||||
|
} |
||||
|
$this->workbookPassword = $password; |
||||
|
} |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,226 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Helper; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\IOFactory; |
||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet; |
||||
|
use PhpOffice\PhpSpreadsheet\Writer\IWriter; |
||||
|
use RecursiveDirectoryIterator; |
||||
|
use RecursiveIteratorIterator; |
||||
|
use RecursiveRegexIterator; |
||||
|
use ReflectionClass; |
||||
|
use RegexIterator; |
||||
|
use RuntimeException; |
||||
|
|
||||
|
/** |
||||
|
* Helper class to be used in sample code. |
||||
|
*/ |
||||
|
class Sample |
||||
|
{ |
||||
|
/** |
||||
|
* Returns whether we run on CLI or browser. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function isCli() |
||||
|
{ |
||||
|
return PHP_SAPI === 'cli'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the filename currently being executed. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getScriptFilename() |
||||
|
{ |
||||
|
return basename($_SERVER['SCRIPT_FILENAME'], '.php'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Whether we are executing the index page. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function isIndex() |
||||
|
{ |
||||
|
return $this->getScriptFilename() === 'index'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the page title. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getPageTitle() |
||||
|
{ |
||||
|
return $this->isIndex() ? 'PHPSpreadsheet' : $this->getScriptFilename(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the page heading. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getPageHeading() |
||||
|
{ |
||||
|
return $this->isIndex() ? '' : '<h1>' . str_replace('_', ' ', $this->getScriptFilename()) . '</h1>'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns an array of all known samples. |
||||
|
* |
||||
|
* @return string[][] [$name => $path] |
||||
|
*/ |
||||
|
public function getSamples() |
||||
|
{ |
||||
|
// Populate samples |
||||
|
$baseDir = realpath(__DIR__ . '/../../../samples'); |
||||
|
$directory = new RecursiveDirectoryIterator($baseDir); |
||||
|
$iterator = new RecursiveIteratorIterator($directory); |
||||
|
$regex = new RegexIterator($iterator, '/^.+\.php$/', RecursiveRegexIterator::GET_MATCH); |
||||
|
|
||||
|
$files = []; |
||||
|
foreach ($regex as $file) { |
||||
|
$file = str_replace(str_replace('\\', '/', $baseDir) . '/', '', str_replace('\\', '/', $file[0])); |
||||
|
$info = pathinfo($file); |
||||
|
$category = str_replace('_', ' ', $info['dirname']); |
||||
|
$name = str_replace('_', ' ', preg_replace('/(|\.php)/', '', $info['filename'])); |
||||
|
if (!in_array($category, ['.', 'boostrap', 'templates'])) { |
||||
|
if (!isset($files[$category])) { |
||||
|
$files[$category] = []; |
||||
|
} |
||||
|
$files[$category][$name] = $file; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Sort everything |
||||
|
ksort($files); |
||||
|
foreach ($files as &$f) { |
||||
|
asort($f); |
||||
|
} |
||||
|
|
||||
|
return $files; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Write documents. |
||||
|
* |
||||
|
* @param string $filename |
||||
|
* @param string[] $writers |
||||
|
*/ |
||||
|
public function write(Spreadsheet $spreadsheet, $filename, array $writers = ['Xlsx', 'Xls']): void |
||||
|
{ |
||||
|
// Set active sheet index to the first sheet, so Excel opens this as the first sheet |
||||
|
$spreadsheet->setActiveSheetIndex(0); |
||||
|
|
||||
|
// Write documents |
||||
|
foreach ($writers as $writerType) { |
||||
|
$path = $this->getFilename($filename, mb_strtolower($writerType)); |
||||
|
$writer = IOFactory::createWriter($spreadsheet, $writerType); |
||||
|
$callStartTime = microtime(true); |
||||
|
$writer->save($path); |
||||
|
$this->logWrite($writer, $path, $callStartTime); |
||||
|
} |
||||
|
|
||||
|
$this->logEndingNotes(); |
||||
|
} |
||||
|
|
||||
|
protected function isDirOrMkdir(string $folder): bool |
||||
|
{ |
||||
|
return \is_dir($folder) || \mkdir($folder); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the temporary directory and make sure it exists. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
private function getTemporaryFolder() |
||||
|
{ |
||||
|
$tempFolder = sys_get_temp_dir() . '/phpspreadsheet'; |
||||
|
if (!$this->isDirOrMkdir($tempFolder)) { |
||||
|
throw new RuntimeException(sprintf('Directory "%s" was not created', $tempFolder)); |
||||
|
} |
||||
|
|
||||
|
return $tempFolder; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the filename that should be used for sample output. |
||||
|
* |
||||
|
* @param string $filename |
||||
|
* @param string $extension |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getFilename($filename, $extension = 'xlsx') |
||||
|
{ |
||||
|
$originalExtension = pathinfo($filename, PATHINFO_EXTENSION); |
||||
|
|
||||
|
return $this->getTemporaryFolder() . '/' . str_replace('.' . $originalExtension, '.' . $extension, basename($filename)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return a random temporary file name. |
||||
|
* |
||||
|
* @param string $extension |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getTemporaryFilename($extension = 'xlsx') |
||||
|
{ |
||||
|
$temporaryFilename = tempnam($this->getTemporaryFolder(), 'phpspreadsheet-'); |
||||
|
unlink($temporaryFilename); |
||||
|
|
||||
|
return $temporaryFilename . '.' . $extension; |
||||
|
} |
||||
|
|
||||
|
public function log($message): void |
||||
|
{ |
||||
|
$eol = $this->isCli() ? PHP_EOL : '<br />'; |
||||
|
echo date('H:i:s ') . $message . $eol; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Log ending notes. |
||||
|
*/ |
||||
|
public function logEndingNotes(): void |
||||
|
{ |
||||
|
// Do not show execution time for index |
||||
|
$this->log('Peak memory usage: ' . (memory_get_peak_usage(true) / 1024 / 1024) . 'MB'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Log a line about the write operation. |
||||
|
* |
||||
|
* @param string $path |
||||
|
* @param float $callStartTime |
||||
|
*/ |
||||
|
public function logWrite(IWriter $writer, $path, $callStartTime): void |
||||
|
{ |
||||
|
$callEndTime = microtime(true); |
||||
|
$callTime = $callEndTime - $callStartTime; |
||||
|
$reflection = new ReflectionClass($writer); |
||||
|
$format = $reflection->getShortName(); |
||||
|
$message = "Write {$format} format to <code>{$path}</code> in " . sprintf('%.4f', $callTime) . ' seconds'; |
||||
|
|
||||
|
$this->log($message); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Log a line about the read operation. |
||||
|
* |
||||
|
* @param string $format |
||||
|
* @param string $path |
||||
|
* @param float $callStartTime |
||||
|
*/ |
||||
|
public function logRead($format, $path, $callStartTime): void |
||||
|
{ |
||||
|
$callEndTime = microtime(true); |
||||
|
$callTime = $callEndTime - $callStartTime; |
||||
|
$message = "Read {$format} format from <code>{$path}</code> in " . sprintf('%.4f', $callTime) . ' seconds'; |
||||
|
|
||||
|
$this->log($message); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Helper; |
||||
|
|
||||
|
class Size |
||||
|
{ |
||||
|
const REGEXP_SIZE_VALIDATION = '/^(?P<size>\d*\.?\d+)(?P<unit>pt|px|em)?$/i'; |
||||
|
|
||||
|
/** |
||||
|
* @var bool |
||||
|
*/ |
||||
|
protected $valid; |
||||
|
|
||||
|
/** |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $size = ''; |
||||
|
|
||||
|
/** |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $unit = ''; |
||||
|
|
||||
|
public function __construct(string $size) |
||||
|
{ |
||||
|
$this->valid = (bool) preg_match(self::REGEXP_SIZE_VALIDATION, $size, $matches); |
||||
|
if ($this->valid) { |
||||
|
$this->size = $matches['size']; |
||||
|
$this->unit = $matches['unit'] ?? 'pt'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function valid(): bool |
||||
|
{ |
||||
|
return $this->valid; |
||||
|
} |
||||
|
|
||||
|
public function size(): string |
||||
|
{ |
||||
|
return $this->size; |
||||
|
} |
||||
|
|
||||
|
public function unit(): string |
||||
|
{ |
||||
|
return $this->unit; |
||||
|
} |
||||
|
|
||||
|
public function __toString() |
||||
|
{ |
||||
|
return $this->size . $this->unit; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,164 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Gnumeric; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Reader\Gnumeric; |
||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet; |
||||
|
use SimpleXMLElement; |
||||
|
|
||||
|
class Properties |
||||
|
{ |
||||
|
/** |
||||
|
* @var Spreadsheet |
||||
|
*/ |
||||
|
protected $spreadsheet; |
||||
|
|
||||
|
public function __construct(Spreadsheet $spreadsheet) |
||||
|
{ |
||||
|
$this->spreadsheet = $spreadsheet; |
||||
|
} |
||||
|
|
||||
|
private function docPropertiesOld(SimpleXMLElement $gnmXML): void |
||||
|
{ |
||||
|
$docProps = $this->spreadsheet->getProperties(); |
||||
|
foreach ($gnmXML->Summary->Item as $summaryItem) { |
||||
|
$propertyName = $summaryItem->name; |
||||
|
$propertyValue = $summaryItem->{'val-string'}; |
||||
|
switch ($propertyName) { |
||||
|
case 'title': |
||||
|
$docProps->setTitle(trim($propertyValue)); |
||||
|
|
||||
|
break; |
||||
|
case 'comments': |
||||
|
$docProps->setDescription(trim($propertyValue)); |
||||
|
|
||||
|
break; |
||||
|
case 'keywords': |
||||
|
$docProps->setKeywords(trim($propertyValue)); |
||||
|
|
||||
|
break; |
||||
|
case 'category': |
||||
|
$docProps->setCategory(trim($propertyValue)); |
||||
|
|
||||
|
break; |
||||
|
case 'manager': |
||||
|
$docProps->setManager(trim($propertyValue)); |
||||
|
|
||||
|
break; |
||||
|
case 'author': |
||||
|
$docProps->setCreator(trim($propertyValue)); |
||||
|
$docProps->setLastModifiedBy(trim($propertyValue)); |
||||
|
|
||||
|
break; |
||||
|
case 'company': |
||||
|
$docProps->setCompany(trim($propertyValue)); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function docPropertiesDC(SimpleXMLElement $officePropertyDC): void |
||||
|
{ |
||||
|
$docProps = $this->spreadsheet->getProperties(); |
||||
|
foreach ($officePropertyDC as $propertyName => $propertyValue) { |
||||
|
$propertyValue = trim((string) $propertyValue); |
||||
|
switch ($propertyName) { |
||||
|
case 'title': |
||||
|
$docProps->setTitle($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'subject': |
||||
|
$docProps->setSubject($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'creator': |
||||
|
$docProps->setCreator($propertyValue); |
||||
|
$docProps->setLastModifiedBy($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'date': |
||||
|
$creationDate = $propertyValue; |
||||
|
$docProps->setModified($creationDate); |
||||
|
|
||||
|
break; |
||||
|
case 'description': |
||||
|
$docProps->setDescription($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function docPropertiesMeta(SimpleXMLElement $officePropertyMeta): void |
||||
|
{ |
||||
|
$docProps = $this->spreadsheet->getProperties(); |
||||
|
foreach ($officePropertyMeta as $propertyName => $propertyValue) { |
||||
|
if ($propertyValue !== null) { |
||||
|
$attributes = $propertyValue->attributes(Gnumeric::NAMESPACE_META); |
||||
|
$propertyValue = trim((string) $propertyValue); |
||||
|
switch ($propertyName) { |
||||
|
case 'keyword': |
||||
|
$docProps->setKeywords($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'initial-creator': |
||||
|
$docProps->setCreator($propertyValue); |
||||
|
$docProps->setLastModifiedBy($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'creation-date': |
||||
|
$creationDate = $propertyValue; |
||||
|
$docProps->setCreated($creationDate); |
||||
|
|
||||
|
break; |
||||
|
case 'user-defined': |
||||
|
if ($attributes) { |
||||
|
[, $attrName] = explode(':', (string) $attributes['name']); |
||||
|
$this->userDefinedProperties($attrName, $propertyValue); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function userDefinedProperties(string $attrName, string $propertyValue): void |
||||
|
{ |
||||
|
$docProps = $this->spreadsheet->getProperties(); |
||||
|
switch ($attrName) { |
||||
|
case 'publisher': |
||||
|
$docProps->setCompany($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'category': |
||||
|
$docProps->setCategory($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'manager': |
||||
|
$docProps->setManager($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function readProperties(SimpleXMLElement $xml, SimpleXMLElement $gnmXML): void |
||||
|
{ |
||||
|
$officeXML = $xml->children(Gnumeric::NAMESPACE_OFFICE); |
||||
|
if (!empty($officeXML)) { |
||||
|
$officeDocXML = $officeXML->{'document-meta'}; |
||||
|
$officeDocMetaXML = $officeDocXML->meta; |
||||
|
|
||||
|
foreach ($officeDocMetaXML as $officePropertyData) { |
||||
|
$officePropertyDC = $officePropertyData->children(Gnumeric::NAMESPACE_DC); |
||||
|
$this->docPropertiesDC($officePropertyDC); |
||||
|
|
||||
|
$officePropertyMeta = $officePropertyData->children(Gnumeric::NAMESPACE_META); |
||||
|
$this->docPropertiesMeta($officePropertyMeta); |
||||
|
} |
||||
|
} elseif (isset($gnmXML->Summary)) { |
||||
|
$this->docPropertiesOld($gnmXML); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,278 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Gnumeric; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date; |
||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Alignment; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Border; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Borders; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Font; |
||||
|
use SimpleXMLElement; |
||||
|
|
||||
|
class Styles |
||||
|
{ |
||||
|
/** |
||||
|
* @var Spreadsheet |
||||
|
*/ |
||||
|
private $spreadsheet; |
||||
|
|
||||
|
/** |
||||
|
* @var bool |
||||
|
*/ |
||||
|
protected $readDataOnly = false; |
||||
|
|
||||
|
/** @var array */ |
||||
|
public static $mappings = [ |
||||
|
'borderStyle' => [ |
||||
|
'0' => Border::BORDER_NONE, |
||||
|
'1' => Border::BORDER_THIN, |
||||
|
'2' => Border::BORDER_MEDIUM, |
||||
|
'3' => Border::BORDER_SLANTDASHDOT, |
||||
|
'4' => Border::BORDER_DASHED, |
||||
|
'5' => Border::BORDER_THICK, |
||||
|
'6' => Border::BORDER_DOUBLE, |
||||
|
'7' => Border::BORDER_DOTTED, |
||||
|
'8' => Border::BORDER_MEDIUMDASHED, |
||||
|
'9' => Border::BORDER_DASHDOT, |
||||
|
'10' => Border::BORDER_MEDIUMDASHDOT, |
||||
|
'11' => Border::BORDER_DASHDOTDOT, |
||||
|
'12' => Border::BORDER_MEDIUMDASHDOTDOT, |
||||
|
'13' => Border::BORDER_MEDIUMDASHDOTDOT, |
||||
|
], |
||||
|
'fillType' => [ |
||||
|
'1' => Fill::FILL_SOLID, |
||||
|
'2' => Fill::FILL_PATTERN_DARKGRAY, |
||||
|
'3' => Fill::FILL_PATTERN_MEDIUMGRAY, |
||||
|
'4' => Fill::FILL_PATTERN_LIGHTGRAY, |
||||
|
'5' => Fill::FILL_PATTERN_GRAY125, |
||||
|
'6' => Fill::FILL_PATTERN_GRAY0625, |
||||
|
'7' => Fill::FILL_PATTERN_DARKHORIZONTAL, // horizontal stripe |
||||
|
'8' => Fill::FILL_PATTERN_DARKVERTICAL, // vertical stripe |
||||
|
'9' => Fill::FILL_PATTERN_DARKDOWN, // diagonal stripe |
||||
|
'10' => Fill::FILL_PATTERN_DARKUP, // reverse diagonal stripe |
||||
|
'11' => Fill::FILL_PATTERN_DARKGRID, // diagoanl crosshatch |
||||
|
'12' => Fill::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch |
||||
|
'13' => Fill::FILL_PATTERN_LIGHTHORIZONTAL, |
||||
|
'14' => Fill::FILL_PATTERN_LIGHTVERTICAL, |
||||
|
'15' => Fill::FILL_PATTERN_LIGHTUP, |
||||
|
'16' => Fill::FILL_PATTERN_LIGHTDOWN, |
||||
|
'17' => Fill::FILL_PATTERN_LIGHTGRID, // thin horizontal crosshatch |
||||
|
'18' => Fill::FILL_PATTERN_LIGHTTRELLIS, // thin diagonal crosshatch |
||||
|
], |
||||
|
'horizontal' => [ |
||||
|
'1' => Alignment::HORIZONTAL_GENERAL, |
||||
|
'2' => Alignment::HORIZONTAL_LEFT, |
||||
|
'4' => Alignment::HORIZONTAL_RIGHT, |
||||
|
'8' => Alignment::HORIZONTAL_CENTER, |
||||
|
'16' => Alignment::HORIZONTAL_CENTER_CONTINUOUS, |
||||
|
'32' => Alignment::HORIZONTAL_JUSTIFY, |
||||
|
'64' => Alignment::HORIZONTAL_CENTER_CONTINUOUS, |
||||
|
], |
||||
|
'underline' => [ |
||||
|
'1' => Font::UNDERLINE_SINGLE, |
||||
|
'2' => Font::UNDERLINE_DOUBLE, |
||||
|
'3' => Font::UNDERLINE_SINGLEACCOUNTING, |
||||
|
'4' => Font::UNDERLINE_DOUBLEACCOUNTING, |
||||
|
], |
||||
|
'vertical' => [ |
||||
|
'1' => Alignment::VERTICAL_TOP, |
||||
|
'2' => Alignment::VERTICAL_BOTTOM, |
||||
|
'4' => Alignment::VERTICAL_CENTER, |
||||
|
'8' => Alignment::VERTICAL_JUSTIFY, |
||||
|
], |
||||
|
]; |
||||
|
|
||||
|
public function __construct(Spreadsheet $spreadsheet, bool $readDataOnly) |
||||
|
{ |
||||
|
$this->spreadsheet = $spreadsheet; |
||||
|
$this->readDataOnly = $readDataOnly; |
||||
|
} |
||||
|
|
||||
|
public function read(SimpleXMLElement $sheet, int $maxRow, int $maxCol): void |
||||
|
{ |
||||
|
if ($sheet->Styles->StyleRegion !== null) { |
||||
|
$this->readStyles($sheet->Styles->StyleRegion, $maxRow, $maxCol); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function readStyles(SimpleXMLElement $styleRegion, int $maxRow, int $maxCol): void |
||||
|
{ |
||||
|
foreach ($styleRegion as $style) { |
||||
|
$styleAttributes = $style->attributes(); |
||||
|
if ($styleAttributes !== null && ($styleAttributes['startRow'] <= $maxRow) && ($styleAttributes['startCol'] <= $maxCol)) { |
||||
|
$cellRange = $this->readStyleRange($styleAttributes, $maxCol, $maxRow); |
||||
|
|
||||
|
$styleAttributes = $style->Style->attributes(); |
||||
|
|
||||
|
$styleArray = []; |
||||
|
// We still set the number format mask for date/time values, even if readDataOnly is true |
||||
|
// so that we can identify whether a float is a float or a date value |
||||
|
$formatCode = $styleAttributes ? (string) $styleAttributes['Format'] : null; |
||||
|
if ($formatCode && Date::isDateTimeFormatCode($formatCode)) { |
||||
|
$styleArray['numberFormat']['formatCode'] = $formatCode; |
||||
|
} |
||||
|
if ($this->readDataOnly === false && $styleAttributes !== null) { |
||||
|
// If readDataOnly is false, we set all formatting information |
||||
|
$styleArray['numberFormat']['formatCode'] = $formatCode; |
||||
|
$styleArray = $this->readStyle($styleArray, $styleAttributes, $style); |
||||
|
} |
||||
|
$this->spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($styleArray); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function addBorderDiagonal(SimpleXMLElement $srssb, array &$styleArray): void |
||||
|
{ |
||||
|
if (isset($srssb->Diagonal, $srssb->{'Rev-Diagonal'})) { |
||||
|
$styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); |
||||
|
$styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_BOTH; |
||||
|
} elseif (isset($srssb->Diagonal)) { |
||||
|
$styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); |
||||
|
$styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_UP; |
||||
|
} elseif (isset($srssb->{'Rev-Diagonal'})) { |
||||
|
$styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->{'Rev-Diagonal'}->attributes()); |
||||
|
$styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_DOWN; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function addBorderStyle(SimpleXMLElement $srssb, array &$styleArray, string $direction): void |
||||
|
{ |
||||
|
$ucDirection = ucfirst($direction); |
||||
|
if (isset($srssb->$ucDirection)) { |
||||
|
$styleArray['borders'][$direction] = self::parseBorderAttributes($srssb->$ucDirection->attributes()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function calcRotation(SimpleXMLElement $styleAttributes): int |
||||
|
{ |
||||
|
$rotation = (int) $styleAttributes->Rotation; |
||||
|
if ($rotation >= 270 && $rotation <= 360) { |
||||
|
$rotation -= 360; |
||||
|
} |
||||
|
$rotation = (abs($rotation) > 90) ? 0 : $rotation; |
||||
|
|
||||
|
return $rotation; |
||||
|
} |
||||
|
|
||||
|
private static function addStyle(array &$styleArray, string $key, string $value): void |
||||
|
{ |
||||
|
if (array_key_exists($value, self::$mappings[$key])) { |
||||
|
$styleArray[$key] = self::$mappings[$key][$value]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static function addStyle2(array &$styleArray, string $key1, string $key, string $value): void |
||||
|
{ |
||||
|
if (array_key_exists($value, self::$mappings[$key])) { |
||||
|
$styleArray[$key1][$key] = self::$mappings[$key][$value]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static function parseBorderAttributes(?SimpleXMLElement $borderAttributes): array |
||||
|
{ |
||||
|
$styleArray = []; |
||||
|
if ($borderAttributes !== null) { |
||||
|
if (isset($borderAttributes['Color'])) { |
||||
|
$styleArray['color']['rgb'] = self::parseGnumericColour($borderAttributes['Color']); |
||||
|
} |
||||
|
|
||||
|
self::addStyle($styleArray, 'borderStyle', (string) $borderAttributes['Style']); |
||||
|
} |
||||
|
|
||||
|
return $styleArray; |
||||
|
} |
||||
|
|
||||
|
private static function parseGnumericColour(string $gnmColour): string |
||||
|
{ |
||||
|
[$gnmR, $gnmG, $gnmB] = explode(':', $gnmColour); |
||||
|
$gnmR = substr(str_pad($gnmR, 4, '0', STR_PAD_RIGHT), 0, 2); |
||||
|
$gnmG = substr(str_pad($gnmG, 4, '0', STR_PAD_RIGHT), 0, 2); |
||||
|
$gnmB = substr(str_pad($gnmB, 4, '0', STR_PAD_RIGHT), 0, 2); |
||||
|
|
||||
|
return $gnmR . $gnmG . $gnmB; |
||||
|
} |
||||
|
|
||||
|
private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes): void |
||||
|
{ |
||||
|
$RGB = self::parseGnumericColour((string) $styleAttributes['Fore']); |
||||
|
$styleArray['font']['color']['rgb'] = $RGB; |
||||
|
$RGB = self::parseGnumericColour((string) $styleAttributes['Back']); |
||||
|
$shade = (string) $styleAttributes['Shade']; |
||||
|
if (($RGB !== '000000') || ($shade !== '0')) { |
||||
|
$RGB2 = self::parseGnumericColour((string) $styleAttributes['PatternColor']); |
||||
|
if ($shade === '1') { |
||||
|
$styleArray['fill']['startColor']['rgb'] = $RGB; |
||||
|
$styleArray['fill']['endColor']['rgb'] = $RGB2; |
||||
|
} else { |
||||
|
$styleArray['fill']['endColor']['rgb'] = $RGB; |
||||
|
$styleArray['fill']['startColor']['rgb'] = $RGB2; |
||||
|
} |
||||
|
self::addStyle2($styleArray, 'fill', 'fillType', $shade); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function readStyleRange(SimpleXMLElement $styleAttributes, int $maxCol, int $maxRow): string |
||||
|
{ |
||||
|
$startColumn = Coordinate::stringFromColumnIndex((int) $styleAttributes['startCol'] + 1); |
||||
|
$startRow = $styleAttributes['startRow'] + 1; |
||||
|
|
||||
|
$endColumn = ($styleAttributes['endCol'] > $maxCol) ? $maxCol : (int) $styleAttributes['endCol']; |
||||
|
$endColumn = Coordinate::stringFromColumnIndex($endColumn + 1); |
||||
|
|
||||
|
$endRow = 1 + (($styleAttributes['endRow'] > $maxRow) ? $maxRow : (int) $styleAttributes['endRow']); |
||||
|
$cellRange = $startColumn . $startRow . ':' . $endColumn . $endRow; |
||||
|
|
||||
|
return $cellRange; |
||||
|
} |
||||
|
|
||||
|
private function readStyle(array $styleArray, SimpleXMLElement $styleAttributes, SimpleXMLElement $style): array |
||||
|
{ |
||||
|
self::addStyle2($styleArray, 'alignment', 'horizontal', (string) $styleAttributes['HAlign']); |
||||
|
self::addStyle2($styleArray, 'alignment', 'vertical', (string) $styleAttributes['VAlign']); |
||||
|
$styleArray['alignment']['wrapText'] = $styleAttributes['WrapText'] == '1'; |
||||
|
$styleArray['alignment']['textRotation'] = $this->calcRotation($styleAttributes); |
||||
|
$styleArray['alignment']['shrinkToFit'] = $styleAttributes['ShrinkToFit'] == '1'; |
||||
|
$styleArray['alignment']['indent'] = ((int) ($styleAttributes['Indent']) > 0) ? $styleAttributes['indent'] : 0; |
||||
|
|
||||
|
$this->addColors($styleArray, $styleAttributes); |
||||
|
|
||||
|
$fontAttributes = $style->Style->Font->attributes(); |
||||
|
if ($fontAttributes !== null) { |
||||
|
$styleArray['font']['name'] = (string) $style->Style->Font; |
||||
|
$styleArray['font']['size'] = (int) ($fontAttributes['Unit']); |
||||
|
$styleArray['font']['bold'] = $fontAttributes['Bold'] == '1'; |
||||
|
$styleArray['font']['italic'] = $fontAttributes['Italic'] == '1'; |
||||
|
$styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1'; |
||||
|
self::addStyle2($styleArray, 'font', 'underline', (string) $fontAttributes['Underline']); |
||||
|
|
||||
|
switch ($fontAttributes['Script']) { |
||||
|
case '1': |
||||
|
$styleArray['font']['superscript'] = true; |
||||
|
|
||||
|
break; |
||||
|
case '-1': |
||||
|
$styleArray['font']['subscript'] = true; |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isset($style->Style->StyleBorder)) { |
||||
|
$srssb = $style->Style->StyleBorder; |
||||
|
$this->addBorderStyle($srssb, $styleArray, 'top'); |
||||
|
$this->addBorderStyle($srssb, $styleArray, 'bottom'); |
||||
|
$this->addBorderStyle($srssb, $styleArray, 'left'); |
||||
|
$this->addBorderStyle($srssb, $styleArray, 'right'); |
||||
|
$this->addBorderDiagonal($srssb, $styleArray); |
||||
|
} |
||||
|
if (isset($style->Style->HyperLink)) { |
||||
|
// TO DO |
||||
|
$hyperlink = $style->Style->HyperLink->attributes(); |
||||
|
} |
||||
|
|
||||
|
return $styleArray; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,129 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Ods; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Document\Properties as DocumentProperties; |
||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet; |
||||
|
use SimpleXMLElement; |
||||
|
|
||||
|
class Properties |
||||
|
{ |
||||
|
private $spreadsheet; |
||||
|
|
||||
|
public function __construct(Spreadsheet $spreadsheet) |
||||
|
{ |
||||
|
$this->spreadsheet = $spreadsheet; |
||||
|
} |
||||
|
|
||||
|
public function load(SimpleXMLElement $xml, $namespacesMeta): void |
||||
|
{ |
||||
|
$docProps = $this->spreadsheet->getProperties(); |
||||
|
$officeProperty = $xml->children($namespacesMeta['office']); |
||||
|
foreach ($officeProperty as $officePropertyData) { |
||||
|
// @var \SimpleXMLElement $officePropertyData |
||||
|
if (isset($namespacesMeta['dc'])) { |
||||
|
$officePropertiesDC = $officePropertyData->children($namespacesMeta['dc']); |
||||
|
$this->setCoreProperties($docProps, $officePropertiesDC); |
||||
|
} |
||||
|
|
||||
|
$officePropertyMeta = []; |
||||
|
if (isset($namespacesMeta['dc'])) { |
||||
|
$officePropertyMeta = $officePropertyData->children($namespacesMeta['meta']); |
||||
|
} |
||||
|
foreach ($officePropertyMeta as $propertyName => $propertyValue) { |
||||
|
$this->setMetaProperties($namespacesMeta, $propertyValue, $propertyName, $docProps); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function setCoreProperties(DocumentProperties $docProps, SimpleXMLElement $officePropertyDC): void |
||||
|
{ |
||||
|
foreach ($officePropertyDC as $propertyName => $propertyValue) { |
||||
|
$propertyValue = (string) $propertyValue; |
||||
|
switch ($propertyName) { |
||||
|
case 'title': |
||||
|
$docProps->setTitle($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'subject': |
||||
|
$docProps->setSubject($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'creator': |
||||
|
$docProps->setCreator($propertyValue); |
||||
|
$docProps->setLastModifiedBy($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'date': |
||||
|
$docProps->setModified($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'description': |
||||
|
$docProps->setDescription($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function setMetaProperties( |
||||
|
$namespacesMeta, |
||||
|
SimpleXMLElement $propertyValue, |
||||
|
$propertyName, |
||||
|
DocumentProperties $docProps |
||||
|
): void { |
||||
|
$propertyValueAttributes = $propertyValue->attributes($namespacesMeta['meta']); |
||||
|
$propertyValue = (string) $propertyValue; |
||||
|
switch ($propertyName) { |
||||
|
case 'initial-creator': |
||||
|
$docProps->setCreator($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'keyword': |
||||
|
$docProps->setKeywords($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'creation-date': |
||||
|
$docProps->setCreated($propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'user-defined': |
||||
|
$this->setUserDefinedProperty($propertyValueAttributes, $propertyValue, $docProps); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function setUserDefinedProperty($propertyValueAttributes, $propertyValue, DocumentProperties $docProps): void |
||||
|
{ |
||||
|
$propertyValueName = ''; |
||||
|
$propertyValueType = DocumentProperties::PROPERTY_TYPE_STRING; |
||||
|
foreach ($propertyValueAttributes as $key => $value) { |
||||
|
if ($key == 'name') { |
||||
|
$propertyValueName = (string) $value; |
||||
|
} elseif ($key == 'value-type') { |
||||
|
switch ($value) { |
||||
|
case 'date': |
||||
|
$propertyValue = DocumentProperties::convertProperty($propertyValue, 'date'); |
||||
|
$propertyValueType = DocumentProperties::PROPERTY_TYPE_DATE; |
||||
|
|
||||
|
break; |
||||
|
case 'boolean': |
||||
|
$propertyValue = DocumentProperties::convertProperty($propertyValue, 'bool'); |
||||
|
$propertyValueType = DocumentProperties::PROPERTY_TYPE_BOOLEAN; |
||||
|
|
||||
|
break; |
||||
|
case 'float': |
||||
|
$propertyValue = DocumentProperties::convertProperty($propertyValue, 'r4'); |
||||
|
$propertyValueType = DocumentProperties::PROPERTY_TYPE_FLOAT; |
||||
|
|
||||
|
break; |
||||
|
default: |
||||
|
$propertyValueType = DocumentProperties::PROPERTY_TYPE_STRING; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$docProps->setCustomProperty($propertyValueName, $propertyValue, $propertyValueType); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,157 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Security; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Reader; |
||||
|
|
||||
|
class XmlScanner |
||||
|
{ |
||||
|
/** |
||||
|
* String used to identify risky xml elements. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $pattern; |
||||
|
|
||||
|
private $callback; |
||||
|
|
||||
|
private static $libxmlDisableEntityLoaderValue; |
||||
|
|
||||
|
/** |
||||
|
* @var bool |
||||
|
*/ |
||||
|
private static $shutdownRegistered = false; |
||||
|
|
||||
|
public function __construct($pattern = '<!DOCTYPE') |
||||
|
{ |
||||
|
$this->pattern = $pattern; |
||||
|
|
||||
|
$this->disableEntityLoaderCheck(); |
||||
|
|
||||
|
// A fatal error will bypass the destructor, so we register a shutdown here |
||||
|
if (!self::$shutdownRegistered) { |
||||
|
self::$shutdownRegistered = true; |
||||
|
register_shutdown_function([__CLASS__, 'shutdown']); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static function getInstance(Reader\IReader $reader) |
||||
|
{ |
||||
|
switch (true) { |
||||
|
case $reader instanceof Reader\Html: |
||||
|
return new self('<!ENTITY'); |
||||
|
case $reader instanceof Reader\Xlsx: |
||||
|
case $reader instanceof Reader\Xml: |
||||
|
case $reader instanceof Reader\Ods: |
||||
|
case $reader instanceof Reader\Gnumeric: |
||||
|
return new self('<!DOCTYPE'); |
||||
|
default: |
||||
|
return new self('<!DOCTYPE'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static function threadSafeLibxmlDisableEntityLoaderAvailability() |
||||
|
{ |
||||
|
if (PHP_MAJOR_VERSION == 7) { |
||||
|
switch (PHP_MINOR_VERSION) { |
||||
|
case 2: |
||||
|
return PHP_RELEASE_VERSION >= 1; |
||||
|
case 1: |
||||
|
return PHP_RELEASE_VERSION >= 13; |
||||
|
case 0: |
||||
|
return PHP_RELEASE_VERSION >= 27; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private function disableEntityLoaderCheck(): void |
||||
|
{ |
||||
|
if (\PHP_VERSION_ID < 80000) { |
||||
|
$libxmlDisableEntityLoaderValue = libxml_disable_entity_loader(true); |
||||
|
|
||||
|
if (self::$libxmlDisableEntityLoaderValue === null) { |
||||
|
self::$libxmlDisableEntityLoaderValue = $libxmlDisableEntityLoaderValue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static function shutdown(): void |
||||
|
{ |
||||
|
if (self::$libxmlDisableEntityLoaderValue !== null && \PHP_VERSION_ID < 80000) { |
||||
|
libxml_disable_entity_loader(self::$libxmlDisableEntityLoaderValue); |
||||
|
self::$libxmlDisableEntityLoaderValue = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function __destruct() |
||||
|
{ |
||||
|
self::shutdown(); |
||||
|
} |
||||
|
|
||||
|
public function setAdditionalCallback(callable $callback): void |
||||
|
{ |
||||
|
$this->callback = $callback; |
||||
|
} |
||||
|
|
||||
|
private function toUtf8($xml) |
||||
|
{ |
||||
|
$pattern = '/encoding="(.*?)"/'; |
||||
|
$result = preg_match($pattern, $xml, $matches); |
||||
|
$charset = strtoupper($result ? $matches[1] : 'UTF-8'); |
||||
|
|
||||
|
if ($charset !== 'UTF-8') { |
||||
|
$xml = mb_convert_encoding($xml, 'UTF-8', $charset); |
||||
|
|
||||
|
$result = preg_match($pattern, $xml, $matches); |
||||
|
$charset = strtoupper($result ? $matches[1] : 'UTF-8'); |
||||
|
if ($charset !== 'UTF-8') { |
||||
|
throw new Reader\Exception('Suspicious Double-encoded XML, spreadsheet file load() aborted to prevent XXE/XEE attacks'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $xml; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Scan the XML for use of <!ENTITY to prevent XXE/XEE attacks. |
||||
|
* |
||||
|
* @param mixed $xml |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function scan($xml) |
||||
|
{ |
||||
|
$this->disableEntityLoaderCheck(); |
||||
|
|
||||
|
$xml = $this->toUtf8($xml); |
||||
|
|
||||
|
// Don't rely purely on libxml_disable_entity_loader() |
||||
|
$pattern = '/\\0?' . implode('\\0?', str_split($this->pattern)) . '\\0?/'; |
||||
|
|
||||
|
if (preg_match($pattern, $xml)) { |
||||
|
throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks'); |
||||
|
} |
||||
|
|
||||
|
if ($this->callback !== null && is_callable($this->callback)) { |
||||
|
$xml = call_user_func($this->callback, $xml); |
||||
|
} |
||||
|
|
||||
|
return $xml; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Scan theXML for use of <!ENTITY to prevent XXE/XEE attacks. |
||||
|
* |
||||
|
* @param string $filestream |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function scanFile($filestream) |
||||
|
{ |
||||
|
return $this->scan(file_get_contents($filestream)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,596 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Calculation; |
||||
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; |
||||
|
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; |
||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Border; |
||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; |
||||
|
|
||||
|
class Slk extends BaseReader |
||||
|
{ |
||||
|
/** |
||||
|
* Input encoding. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $inputEncoding = 'ANSI'; |
||||
|
|
||||
|
/** |
||||
|
* Sheet index to read. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $sheetIndex = 0; |
||||
|
|
||||
|
/** |
||||
|
* Formats. |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
private $formats = []; |
||||
|
|
||||
|
/** |
||||
|
* Format Count. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $format = 0; |
||||
|
|
||||
|
/** |
||||
|
* Fonts. |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
private $fonts = []; |
||||
|
|
||||
|
/** |
||||
|
* Font Count. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $fontcount = 0; |
||||
|
|
||||
|
/** |
||||
|
* Create a new SYLK Reader instance. |
||||
|
*/ |
||||
|
public function __construct() |
||||
|
{ |
||||
|
parent::__construct(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Validate that the current file is a SYLK file. |
||||
|
*/ |
||||
|
public function canRead(string $filename): bool |
||||
|
{ |
||||
|
try { |
||||
|
$this->openFile($filename); |
||||
|
} catch (ReaderException $e) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Read sample data (first 2 KB will do) |
||||
|
$data = (string) fread($this->fileHandle, 2048); |
||||
|
|
||||
|
// Count delimiters in file |
||||
|
$delimiterCount = substr_count($data, ';'); |
||||
|
$hasDelimiter = $delimiterCount > 0; |
||||
|
|
||||
|
// Analyze first line looking for ID; signature |
||||
|
$lines = explode("\n", $data); |
||||
|
$hasId = substr($lines[0], 0, 4) === 'ID;P'; |
||||
|
|
||||
|
fclose($this->fileHandle); |
||||
|
|
||||
|
return $hasDelimiter && $hasId; |
||||
|
} |
||||
|
|
||||
|
private function canReadOrBust(string $filename): void |
||||
|
{ |
||||
|
if (!$this->canRead($filename)) { |
||||
|
throw new ReaderException($filename . ' is an Invalid SYLK file.'); |
||||
|
} |
||||
|
$this->openFile($filename); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set input encoding. |
||||
|
* |
||||
|
* @deprecated no use is made of this property |
||||
|
* |
||||
|
* @param string $inputEncoding Input encoding, eg: 'ANSI' |
||||
|
* |
||||
|
* @return $this |
||||
|
* |
||||
|
* @codeCoverageIgnore |
||||
|
*/ |
||||
|
public function setInputEncoding($inputEncoding) |
||||
|
{ |
||||
|
$this->inputEncoding = $inputEncoding; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get input encoding. |
||||
|
* |
||||
|
* @deprecated no use is made of this property |
||||
|
* |
||||
|
* @return string |
||||
|
* |
||||
|
* @codeCoverageIgnore |
||||
|
*/ |
||||
|
public function getInputEncoding() |
||||
|
{ |
||||
|
return $this->inputEncoding; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). |
||||
|
* |
||||
|
* @param string $filename |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function listWorksheetInfo($filename) |
||||
|
{ |
||||
|
// Open file |
||||
|
$this->canReadOrBust($filename); |
||||
|
$fileHandle = $this->fileHandle; |
||||
|
rewind($fileHandle); |
||||
|
|
||||
|
$worksheetInfo = []; |
||||
|
$worksheetInfo[0]['worksheetName'] = basename($filename, '.slk'); |
||||
|
|
||||
|
// loop through one row (line) at a time in the file |
||||
|
$rowIndex = 0; |
||||
|
$columnIndex = 0; |
||||
|
while (($rowData = fgets($fileHandle)) !== false) { |
||||
|
$columnIndex = 0; |
||||
|
|
||||
|
// convert SYLK encoded $rowData to UTF-8 |
||||
|
$rowData = StringHelper::SYLKtoUTF8($rowData); |
||||
|
|
||||
|
// explode each row at semicolons while taking into account that literal semicolon (;) |
||||
|
// is escaped like this (;;) |
||||
|
$rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowData))))); |
||||
|
|
||||
|
$dataType = array_shift($rowData); |
||||
|
if ($dataType == 'B') { |
||||
|
foreach ($rowData as $rowDatum) { |
||||
|
switch ($rowDatum[0]) { |
||||
|
case 'X': |
||||
|
$columnIndex = (int) substr($rowDatum, 1) - 1; |
||||
|
|
||||
|
break; |
||||
|
case 'Y': |
||||
|
$rowIndex = substr($rowDatum, 1); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$worksheetInfo[0]['lastColumnIndex'] = $columnIndex; |
||||
|
$worksheetInfo[0]['totalRows'] = $rowIndex; |
||||
|
$worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1); |
||||
|
$worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1; |
||||
|
|
||||
|
// Close file |
||||
|
fclose($fileHandle); |
||||
|
|
||||
|
return $worksheetInfo; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Loads PhpSpreadsheet from file. |
||||
|
* |
||||
|
* @return Spreadsheet |
||||
|
*/ |
||||
|
public function load(string $filename, int $flags = 0) |
||||
|
{ |
||||
|
$this->processFlags($flags); |
||||
|
|
||||
|
// Create new Spreadsheet |
||||
|
$spreadsheet = new Spreadsheet(); |
||||
|
|
||||
|
// Load into this instance |
||||
|
return $this->loadIntoExisting($filename, $spreadsheet); |
||||
|
} |
||||
|
|
||||
|
private const COLOR_ARRAY = [ |
||||
|
'FF00FFFF', // 0 - cyan |
||||
|
'FF000000', // 1 - black |
||||
|
'FFFFFFFF', // 2 - white |
||||
|
'FFFF0000', // 3 - red |
||||
|
'FF00FF00', // 4 - green |
||||
|
'FF0000FF', // 5 - blue |
||||
|
'FFFFFF00', // 6 - yellow |
||||
|
'FFFF00FF', // 7 - magenta |
||||
|
]; |
||||
|
|
||||
|
private const FONT_STYLE_MAPPINGS = [ |
||||
|
'B' => 'bold', |
||||
|
'I' => 'italic', |
||||
|
'U' => 'underline', |
||||
|
]; |
||||
|
|
||||
|
private function processFormula(string $rowDatum, bool &$hasCalculatedValue, string &$cellDataFormula, string $row, string $column): void |
||||
|
{ |
||||
|
$cellDataFormula = '=' . substr($rowDatum, 1); |
||||
|
// Convert R1C1 style references to A1 style references (but only when not quoted) |
||||
|
$temp = explode('"', $cellDataFormula); |
||||
|
$key = false; |
||||
|
foreach ($temp as &$value) { |
||||
|
// Only count/replace in alternate array entries |
||||
|
$key = !$key; |
||||
|
if ($key) { |
||||
|
preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE); |
||||
|
// Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way |
||||
|
// through the formula from left to right. Reversing means that we work right to left.through |
||||
|
// the formula |
||||
|
$cellReferences = array_reverse($cellReferences); |
||||
|
// Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent, |
||||
|
// then modify the formula to use that new reference |
||||
|
foreach ($cellReferences as $cellReference) { |
||||
|
$rowReference = $cellReference[2][0]; |
||||
|
// Empty R reference is the current row |
||||
|
if ($rowReference == '') { |
||||
|
$rowReference = $row; |
||||
|
} |
||||
|
// Bracketed R references are relative to the current row |
||||
|
if ($rowReference[0] == '[') { |
||||
|
$rowReference = (int) $row + (int) trim($rowReference, '[]'); |
||||
|
} |
||||
|
$columnReference = $cellReference[4][0]; |
||||
|
// Empty C reference is the current column |
||||
|
if ($columnReference == '') { |
||||
|
$columnReference = $column; |
||||
|
} |
||||
|
// Bracketed C references are relative to the current column |
||||
|
if ($columnReference[0] == '[') { |
||||
|
$columnReference = (int) $column + (int) trim($columnReference, '[]'); |
||||
|
} |
||||
|
$A1CellReference = Coordinate::stringFromColumnIndex($columnReference) . $rowReference; |
||||
|
|
||||
|
$value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0])); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
unset($value); |
||||
|
// Then rebuild the formula string |
||||
|
$cellDataFormula = implode('"', $temp); |
||||
|
$hasCalculatedValue = true; |
||||
|
} |
||||
|
|
||||
|
private function processCRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void |
||||
|
{ |
||||
|
// Read cell value data |
||||
|
$hasCalculatedValue = false; |
||||
|
$cellDataFormula = $cellData = ''; |
||||
|
foreach ($rowData as $rowDatum) { |
||||
|
switch ($rowDatum[0]) { |
||||
|
case 'C': |
||||
|
case 'X': |
||||
|
$column = substr($rowDatum, 1); |
||||
|
|
||||
|
break; |
||||
|
case 'R': |
||||
|
case 'Y': |
||||
|
$row = substr($rowDatum, 1); |
||||
|
|
||||
|
break; |
||||
|
case 'K': |
||||
|
$cellData = substr($rowDatum, 1); |
||||
|
|
||||
|
break; |
||||
|
case 'E': |
||||
|
$this->processFormula($rowDatum, $hasCalculatedValue, $cellDataFormula, $row, $column); |
||||
|
|
||||
|
break; |
||||
|
case 'A': |
||||
|
$comment = substr($rowDatum, 1); |
||||
|
$columnLetter = Coordinate::stringFromColumnIndex((int) $column); |
||||
|
$spreadsheet->getActiveSheet() |
||||
|
->getComment("$columnLetter$row") |
||||
|
->getText() |
||||
|
->createText($comment); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
$columnLetter = Coordinate::stringFromColumnIndex((int) $column); |
||||
|
$cellData = Calculation::unwrapResult($cellData); |
||||
|
|
||||
|
// Set cell value |
||||
|
$this->processCFinal($spreadsheet, $hasCalculatedValue, $cellDataFormula, $cellData, "$columnLetter$row"); |
||||
|
} |
||||
|
|
||||
|
private function processCFinal(Spreadsheet &$spreadsheet, bool $hasCalculatedValue, string $cellDataFormula, string $cellData, string $coordinate): void |
||||
|
{ |
||||
|
// Set cell value |
||||
|
$spreadsheet->getActiveSheet()->getCell($coordinate)->setValue(($hasCalculatedValue) ? $cellDataFormula : $cellData); |
||||
|
if ($hasCalculatedValue) { |
||||
|
$cellData = Calculation::unwrapResult($cellData); |
||||
|
$spreadsheet->getActiveSheet()->getCell($coordinate)->setCalculatedValue($cellData); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function processFRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void |
||||
|
{ |
||||
|
// Read cell formatting |
||||
|
$formatStyle = $columnWidth = ''; |
||||
|
$startCol = $endCol = ''; |
||||
|
$fontStyle = ''; |
||||
|
$styleData = []; |
||||
|
foreach ($rowData as $rowDatum) { |
||||
|
switch ($rowDatum[0]) { |
||||
|
case 'C': |
||||
|
case 'X': |
||||
|
$column = substr($rowDatum, 1); |
||||
|
|
||||
|
break; |
||||
|
case 'R': |
||||
|
case 'Y': |
||||
|
$row = substr($rowDatum, 1); |
||||
|
|
||||
|
break; |
||||
|
case 'P': |
||||
|
$formatStyle = $rowDatum; |
||||
|
|
||||
|
break; |
||||
|
case 'W': |
||||
|
[$startCol, $endCol, $columnWidth] = explode(' ', substr($rowDatum, 1)); |
||||
|
|
||||
|
break; |
||||
|
case 'S': |
||||
|
$this->styleSettings($rowDatum, $styleData, $fontStyle); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
$this->addFormats($spreadsheet, $formatStyle, $row, $column); |
||||
|
$this->addFonts($spreadsheet, $fontStyle, $row, $column); |
||||
|
$this->addStyle($spreadsheet, $styleData, $row, $column); |
||||
|
$this->addWidth($spreadsheet, $columnWidth, $startCol, $endCol); |
||||
|
} |
||||
|
|
||||
|
private const STYLE_SETTINGS_FONT = ['D' => 'bold', 'I' => 'italic']; |
||||
|
|
||||
|
private const STYLE_SETTINGS_BORDER = [ |
||||
|
'B' => 'bottom', |
||||
|
'L' => 'left', |
||||
|
'R' => 'right', |
||||
|
'T' => 'top', |
||||
|
]; |
||||
|
|
||||
|
private function styleSettings(string $rowDatum, array &$styleData, string &$fontStyle): void |
||||
|
{ |
||||
|
$styleSettings = substr($rowDatum, 1); |
||||
|
$iMax = strlen($styleSettings); |
||||
|
for ($i = 0; $i < $iMax; ++$i) { |
||||
|
$char = $styleSettings[$i]; |
||||
|
if (array_key_exists($char, self::STYLE_SETTINGS_FONT)) { |
||||
|
$styleData['font'][self::STYLE_SETTINGS_FONT[$char]] = true; |
||||
|
} elseif (array_key_exists($char, self::STYLE_SETTINGS_BORDER)) { |
||||
|
$styleData['borders'][self::STYLE_SETTINGS_BORDER[$char]]['borderStyle'] = Border::BORDER_THIN; |
||||
|
} elseif ($char == 'S') { |
||||
|
$styleData['fill']['fillType'] = \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_PATTERN_GRAY125; |
||||
|
} elseif ($char == 'M') { |
||||
|
if (preg_match('/M([1-9]\\d*)/', $styleSettings, $matches)) { |
||||
|
$fontStyle = $matches[1]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function addFormats(Spreadsheet &$spreadsheet, string $formatStyle, string $row, string $column): void |
||||
|
{ |
||||
|
if ($formatStyle && $column > '' && $row > '') { |
||||
|
$columnLetter = Coordinate::stringFromColumnIndex((int) $column); |
||||
|
if (isset($this->formats[$formatStyle])) { |
||||
|
$spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->formats[$formatStyle]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function addFonts(Spreadsheet &$spreadsheet, string $fontStyle, string $row, string $column): void |
||||
|
{ |
||||
|
if ($fontStyle && $column > '' && $row > '') { |
||||
|
$columnLetter = Coordinate::stringFromColumnIndex((int) $column); |
||||
|
if (isset($this->fonts[$fontStyle])) { |
||||
|
$spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->fonts[$fontStyle]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function addStyle(Spreadsheet &$spreadsheet, array $styleData, string $row, string $column): void |
||||
|
{ |
||||
|
if ((!empty($styleData)) && $column > '' && $row > '') { |
||||
|
$columnLetter = Coordinate::stringFromColumnIndex((int) $column); |
||||
|
$spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($styleData); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function addWidth(Spreadsheet $spreadsheet, string $columnWidth, string $startCol, string $endCol): void |
||||
|
{ |
||||
|
if ($columnWidth > '') { |
||||
|
if ($startCol == $endCol) { |
||||
|
$startCol = Coordinate::stringFromColumnIndex((int) $startCol); |
||||
|
$spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth((float) $columnWidth); |
||||
|
} else { |
||||
|
$startCol = Coordinate::stringFromColumnIndex((int) $startCol); |
||||
|
$endCol = Coordinate::stringFromColumnIndex((int) $endCol); |
||||
|
$spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth((float) $columnWidth); |
||||
|
do { |
||||
|
$spreadsheet->getActiveSheet()->getColumnDimension(++$startCol)->setWidth((float) $columnWidth); |
||||
|
} while ($startCol !== $endCol); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function processPRecord(array $rowData, Spreadsheet &$spreadsheet): void |
||||
|
{ |
||||
|
// Read shared styles |
||||
|
$formatArray = []; |
||||
|
$fromFormats = ['\-', '\ ']; |
||||
|
$toFormats = ['-', ' ']; |
||||
|
foreach ($rowData as $rowDatum) { |
||||
|
switch ($rowDatum[0]) { |
||||
|
case 'P': |
||||
|
$formatArray['numberFormat']['formatCode'] = str_replace($fromFormats, $toFormats, substr($rowDatum, 1)); |
||||
|
|
||||
|
break; |
||||
|
case 'E': |
||||
|
case 'F': |
||||
|
$formatArray['font']['name'] = substr($rowDatum, 1); |
||||
|
|
||||
|
break; |
||||
|
case 'M': |
||||
|
$formatArray['font']['size'] = substr($rowDatum, 1) / 20; |
||||
|
|
||||
|
break; |
||||
|
case 'L': |
||||
|
$this->processPColors($rowDatum, $formatArray); |
||||
|
|
||||
|
break; |
||||
|
case 'S': |
||||
|
$this->processPFontStyles($rowDatum, $formatArray); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
$this->processPFinal($spreadsheet, $formatArray); |
||||
|
} |
||||
|
|
||||
|
private function processPColors(string $rowDatum, array &$formatArray): void |
||||
|
{ |
||||
|
if (preg_match('/L([1-9]\\d*)/', $rowDatum, $matches)) { |
||||
|
$fontColor = $matches[1] % 8; |
||||
|
$formatArray['font']['color']['argb'] = self::COLOR_ARRAY[$fontColor]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function processPFontStyles(string $rowDatum, array &$formatArray): void |
||||
|
{ |
||||
|
$styleSettings = substr($rowDatum, 1); |
||||
|
$iMax = strlen($styleSettings); |
||||
|
for ($i = 0; $i < $iMax; ++$i) { |
||||
|
if (array_key_exists($styleSettings[$i], self::FONT_STYLE_MAPPINGS)) { |
||||
|
$formatArray['font'][self::FONT_STYLE_MAPPINGS[$styleSettings[$i]]] = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function processPFinal(Spreadsheet &$spreadsheet, array $formatArray): void |
||||
|
{ |
||||
|
if (array_key_exists('numberFormat', $formatArray)) { |
||||
|
$this->formats['P' . $this->format] = $formatArray; |
||||
|
++$this->format; |
||||
|
} elseif (array_key_exists('font', $formatArray)) { |
||||
|
++$this->fontcount; |
||||
|
$this->fonts[$this->fontcount] = $formatArray; |
||||
|
if ($this->fontcount === 1) { |
||||
|
$spreadsheet->getDefaultStyle()->applyFromArray($formatArray); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Loads PhpSpreadsheet from file into PhpSpreadsheet instance. |
||||
|
* |
||||
|
* @param string $filename |
||||
|
* |
||||
|
* @return Spreadsheet |
||||
|
*/ |
||||
|
public function loadIntoExisting($filename, Spreadsheet $spreadsheet) |
||||
|
{ |
||||
|
// Open file |
||||
|
$this->canReadOrBust($filename); |
||||
|
$fileHandle = $this->fileHandle; |
||||
|
rewind($fileHandle); |
||||
|
|
||||
|
// Create new Worksheets |
||||
|
while ($spreadsheet->getSheetCount() <= $this->sheetIndex) { |
||||
|
$spreadsheet->createSheet(); |
||||
|
} |
||||
|
$spreadsheet->setActiveSheetIndex($this->sheetIndex); |
||||
|
$spreadsheet->getActiveSheet()->setTitle(substr(basename($filename, '.slk'), 0, Worksheet::SHEET_TITLE_MAXIMUM_LENGTH)); |
||||
|
|
||||
|
// Loop through file |
||||
|
$column = $row = ''; |
||||
|
|
||||
|
// loop through one row (line) at a time in the file |
||||
|
while (($rowDataTxt = fgets($fileHandle)) !== false) { |
||||
|
// convert SYLK encoded $rowData to UTF-8 |
||||
|
$rowDataTxt = StringHelper::SYLKtoUTF8($rowDataTxt); |
||||
|
|
||||
|
// explode each row at semicolons while taking into account that literal semicolon (;) |
||||
|
// is escaped like this (;;) |
||||
|
$rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowDataTxt))))); |
||||
|
|
||||
|
$dataType = array_shift($rowData); |
||||
|
if ($dataType == 'P') { |
||||
|
// Read shared styles |
||||
|
$this->processPRecord($rowData, $spreadsheet); |
||||
|
} elseif ($dataType == 'C') { |
||||
|
// Read cell value data |
||||
|
$this->processCRecord($rowData, $spreadsheet, $row, $column); |
||||
|
} elseif ($dataType == 'F') { |
||||
|
// Read cell formatting |
||||
|
$this->processFRecord($rowData, $spreadsheet, $row, $column); |
||||
|
} else { |
||||
|
$this->columnRowFromRowData($rowData, $column, $row); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Close file |
||||
|
fclose($fileHandle); |
||||
|
|
||||
|
// Return |
||||
|
return $spreadsheet; |
||||
|
} |
||||
|
|
||||
|
private function columnRowFromRowData(array $rowData, string &$column, string &$row): void |
||||
|
{ |
||||
|
foreach ($rowData as $rowDatum) { |
||||
|
$char0 = $rowDatum[0]; |
||||
|
if ($char0 === 'X' || $char0 == 'C') { |
||||
|
$column = substr($rowDatum, 1); |
||||
|
} elseif ($char0 === 'Y' || $char0 == 'R') { |
||||
|
$row = substr($rowDatum, 1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get sheet index. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function getSheetIndex() |
||||
|
{ |
||||
|
return $this->sheetIndex; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set sheet index. |
||||
|
* |
||||
|
* @param int $sheetIndex Sheet index |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setSheetIndex($sheetIndex) |
||||
|
{ |
||||
|
$this->sheetIndex = $sheetIndex; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,61 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Xls; |
||||
|
|
||||
|
class RC4 |
||||
|
{ |
||||
|
// Context |
||||
|
protected $s = []; |
||||
|
|
||||
|
protected $i = 0; |
||||
|
|
||||
|
protected $j = 0; |
||||
|
|
||||
|
/** |
||||
|
* RC4 stream decryption/encryption constrcutor. |
||||
|
* |
||||
|
* @param string $key Encryption key/passphrase |
||||
|
*/ |
||||
|
public function __construct($key) |
||||
|
{ |
||||
|
$len = strlen($key); |
||||
|
|
||||
|
for ($this->i = 0; $this->i < 256; ++$this->i) { |
||||
|
$this->s[$this->i] = $this->i; |
||||
|
} |
||||
|
|
||||
|
$this->j = 0; |
||||
|
for ($this->i = 0; $this->i < 256; ++$this->i) { |
||||
|
$this->j = ($this->j + $this->s[$this->i] + ord($key[$this->i % $len])) % 256; |
||||
|
$t = $this->s[$this->i]; |
||||
|
$this->s[$this->i] = $this->s[$this->j]; |
||||
|
$this->s[$this->j] = $t; |
||||
|
} |
||||
|
$this->i = $this->j = 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Symmetric decryption/encryption function. |
||||
|
* |
||||
|
* @param string $data Data to encrypt/decrypt |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function RC4($data) |
||||
|
{ |
||||
|
$len = strlen($data); |
||||
|
for ($c = 0; $c < $len; ++$c) { |
||||
|
$this->i = ($this->i + 1) % 256; |
||||
|
$this->j = ($this->j + $this->s[$this->i]) % 256; |
||||
|
$t = $this->s[$this->i]; |
||||
|
$this->s[$this->i] = $this->s[$this->j]; |
||||
|
$this->s[$this->j] = $t; |
||||
|
|
||||
|
$t = ($this->s[$this->i] + $this->s[$this->j]) % 256; |
||||
|
|
||||
|
$data[$c] = chr(ord($data[$c]) ^ $this->s[$t]); |
||||
|
} |
||||
|
|
||||
|
return $data; |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,109 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Document\Properties as DocumentProperties; |
||||
|
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; |
||||
|
use PhpOffice\PhpSpreadsheet\Settings; |
||||
|
use SimpleXMLElement; |
||||
|
|
||||
|
class Properties |
||||
|
{ |
||||
|
/** @var XmlScanner */ |
||||
|
private $securityScanner; |
||||
|
|
||||
|
/** @var DocumentProperties */ |
||||
|
private $docProps; |
||||
|
|
||||
|
public function __construct(XmlScanner $securityScanner, DocumentProperties $docProps) |
||||
|
{ |
||||
|
$this->securityScanner = $securityScanner; |
||||
|
$this->docProps = $docProps; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $obj |
||||
|
*/ |
||||
|
private static function nullOrSimple($obj): ?SimpleXMLElement |
||||
|
{ |
||||
|
return ($obj instanceof SimpleXMLElement) ? $obj : null; |
||||
|
} |
||||
|
|
||||
|
private function extractPropertyData(string $propertyData): ?SimpleXMLElement |
||||
|
{ |
||||
|
// okay to omit namespace because everything will be processed by xpath |
||||
|
$obj = simplexml_load_string( |
||||
|
$this->securityScanner->scan($propertyData), |
||||
|
'SimpleXMLElement', |
||||
|
Settings::getLibXmlLoaderOptions() |
||||
|
); |
||||
|
|
||||
|
return self::nullOrSimple($obj); |
||||
|
} |
||||
|
|
||||
|
public function readCoreProperties(string $propertyData): void |
||||
|
{ |
||||
|
$xmlCore = $this->extractPropertyData($propertyData); |
||||
|
|
||||
|
if (is_object($xmlCore)) { |
||||
|
$xmlCore->registerXPathNamespace('dc', Namespaces::DC_ELEMENTS); |
||||
|
$xmlCore->registerXPathNamespace('dcterms', Namespaces::DC_TERMS); |
||||
|
$xmlCore->registerXPathNamespace('cp', Namespaces::CORE_PROPERTIES2); |
||||
|
|
||||
|
$this->docProps->setCreator((string) self::getArrayItem($xmlCore->xpath('dc:creator'))); |
||||
|
$this->docProps->setLastModifiedBy((string) self::getArrayItem($xmlCore->xpath('cp:lastModifiedBy'))); |
||||
|
$this->docProps->setCreated((string) self::getArrayItem($xmlCore->xpath('dcterms:created'))); //! respect xsi:type |
||||
|
$this->docProps->setModified((string) self::getArrayItem($xmlCore->xpath('dcterms:modified'))); //! respect xsi:type |
||||
|
$this->docProps->setTitle((string) self::getArrayItem($xmlCore->xpath('dc:title'))); |
||||
|
$this->docProps->setDescription((string) self::getArrayItem($xmlCore->xpath('dc:description'))); |
||||
|
$this->docProps->setSubject((string) self::getArrayItem($xmlCore->xpath('dc:subject'))); |
||||
|
$this->docProps->setKeywords((string) self::getArrayItem($xmlCore->xpath('cp:keywords'))); |
||||
|
$this->docProps->setCategory((string) self::getArrayItem($xmlCore->xpath('cp:category'))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function readExtendedProperties(string $propertyData): void |
||||
|
{ |
||||
|
$xmlCore = $this->extractPropertyData($propertyData); |
||||
|
|
||||
|
if (is_object($xmlCore)) { |
||||
|
if (isset($xmlCore->Company)) { |
||||
|
$this->docProps->setCompany((string) $xmlCore->Company); |
||||
|
} |
||||
|
if (isset($xmlCore->Manager)) { |
||||
|
$this->docProps->setManager((string) $xmlCore->Manager); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function readCustomProperties(string $propertyData): void |
||||
|
{ |
||||
|
$xmlCore = $this->extractPropertyData($propertyData); |
||||
|
|
||||
|
if (is_object($xmlCore)) { |
||||
|
foreach ($xmlCore as $xmlProperty) { |
||||
|
/** @var SimpleXMLElement $xmlProperty */ |
||||
|
$cellDataOfficeAttributes = $xmlProperty->attributes(); |
||||
|
if (isset($cellDataOfficeAttributes['name'])) { |
||||
|
$propertyName = (string) $cellDataOfficeAttributes['name']; |
||||
|
$cellDataOfficeChildren = $xmlProperty->children('http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'); |
||||
|
|
||||
|
$attributeType = $cellDataOfficeChildren->getName(); |
||||
|
$attributeValue = (string) $cellDataOfficeChildren->{$attributeType}; |
||||
|
$attributeValue = DocumentProperties::convertProperty($attributeValue, $attributeType); |
||||
|
$attributeType = DocumentProperties::convertPropertyType($attributeType); |
||||
|
$this->docProps->setCustomProperty($propertyName, $attributeValue, $attributeType); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param array|false $array |
||||
|
* @param mixed $key |
||||
|
*/ |
||||
|
private static function getArrayItem($array, $key = 0): ?SimpleXMLElement |
||||
|
{ |
||||
|
return is_array($array) ? ($array[$key] ?? null) : null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; |
||||
|
use SimpleXMLElement; |
||||
|
|
||||
|
class SheetViewOptions extends BaseParserClass |
||||
|
{ |
||||
|
private $worksheet; |
||||
|
|
||||
|
private $worksheetXml; |
||||
|
|
||||
|
public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) |
||||
|
{ |
||||
|
$this->worksheet = $workSheet; |
||||
|
$this->worksheetXml = $worksheetXml; |
||||
|
} |
||||
|
|
||||
|
public function load(bool $readDataOnly, Styles $styleReader): void |
||||
|
{ |
||||
|
if ($this->worksheetXml === null) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (isset($this->worksheetXml->sheetPr)) { |
||||
|
$this->tabColor($this->worksheetXml->sheetPr, $styleReader); |
||||
|
$this->codeName($this->worksheetXml->sheetPr); |
||||
|
$this->outlines($this->worksheetXml->sheetPr); |
||||
|
$this->pageSetup($this->worksheetXml->sheetPr); |
||||
|
} |
||||
|
|
||||
|
if (isset($this->worksheetXml->sheetFormatPr)) { |
||||
|
$this->sheetFormat($this->worksheetXml->sheetFormatPr); |
||||
|
} |
||||
|
|
||||
|
if (!$readDataOnly && isset($this->worksheetXml->printOptions)) { |
||||
|
$this->printOptions($this->worksheetXml->printOptions); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function tabColor(SimpleXMLElement $sheetPr, Styles $styleReader): void |
||||
|
{ |
||||
|
if (isset($sheetPr->tabColor)) { |
||||
|
$this->worksheet->getTabColor()->setARGB($styleReader->readColor($sheetPr->tabColor)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function codeName(SimpleXMLElement $sheetPr): void |
||||
|
{ |
||||
|
if (isset($sheetPr['codeName'])) { |
||||
|
$this->worksheet->setCodeName((string) $sheetPr['codeName'], false); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function outlines(SimpleXMLElement $sheetPr): void |
||||
|
{ |
||||
|
if (isset($sheetPr->outlinePr)) { |
||||
|
if ( |
||||
|
isset($sheetPr->outlinePr['summaryRight']) && |
||||
|
!self::boolean((string) $sheetPr->outlinePr['summaryRight']) |
||||
|
) { |
||||
|
$this->worksheet->setShowSummaryRight(false); |
||||
|
} else { |
||||
|
$this->worksheet->setShowSummaryRight(true); |
||||
|
} |
||||
|
|
||||
|
if ( |
||||
|
isset($sheetPr->outlinePr['summaryBelow']) && |
||||
|
!self::boolean((string) $sheetPr->outlinePr['summaryBelow']) |
||||
|
) { |
||||
|
$this->worksheet->setShowSummaryBelow(false); |
||||
|
} else { |
||||
|
$this->worksheet->setShowSummaryBelow(true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function pageSetup(SimpleXMLElement $sheetPr): void |
||||
|
{ |
||||
|
if (isset($sheetPr->pageSetUpPr)) { |
||||
|
if ( |
||||
|
isset($sheetPr->pageSetUpPr['fitToPage']) && |
||||
|
!self::boolean((string) $sheetPr->pageSetUpPr['fitToPage']) |
||||
|
) { |
||||
|
$this->worksheet->getPageSetup()->setFitToPage(false); |
||||
|
} else { |
||||
|
$this->worksheet->getPageSetup()->setFitToPage(true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function sheetFormat(SimpleXMLElement $sheetFormatPr): void |
||||
|
{ |
||||
|
if ( |
||||
|
isset($sheetFormatPr['customHeight']) && |
||||
|
self::boolean((string) $sheetFormatPr['customHeight']) && |
||||
|
isset($sheetFormatPr['defaultRowHeight']) |
||||
|
) { |
||||
|
$this->worksheet->getDefaultRowDimension() |
||||
|
->setRowHeight((float) $sheetFormatPr['defaultRowHeight']); |
||||
|
} |
||||
|
|
||||
|
if (isset($sheetFormatPr['defaultColWidth'])) { |
||||
|
$this->worksheet->getDefaultColumnDimension() |
||||
|
->setWidth((float) $sheetFormatPr['defaultColWidth']); |
||||
|
} |
||||
|
|
||||
|
if ( |
||||
|
isset($sheetFormatPr['zeroHeight']) && |
||||
|
((string) $sheetFormatPr['zeroHeight'] === '1') |
||||
|
) { |
||||
|
$this->worksheet->getDefaultRowDimension()->setZeroHeight(true); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function printOptions(SimpleXMLElement $printOptions): void |
||||
|
{ |
||||
|
if (self::boolean((string) $printOptions['gridLinesSet'])) { |
||||
|
$this->worksheet->setShowGridlines(true); |
||||
|
} |
||||
|
if (self::boolean((string) $printOptions['gridLines'])) { |
||||
|
$this->worksheet->setPrintGridlines(true); |
||||
|
} |
||||
|
if (self::boolean((string) $printOptions['horizontalCentered'])) { |
||||
|
$this->worksheet->getPageSetup()->setHorizontalCentered(true); |
||||
|
} |
||||
|
if (self::boolean((string) $printOptions['verticalCentered'])) { |
||||
|
$this->worksheet->getPageSetup()->setVerticalCentered(true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,156 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; |
||||
|
use PhpOffice\PhpSpreadsheet\Reader\Xlsx; |
||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; |
||||
|
use SimpleXMLElement; |
||||
|
|
||||
|
class SheetViews extends BaseParserClass |
||||
|
{ |
||||
|
/** @var SimpleXMLElement */ |
||||
|
private $sheetViewXml; |
||||
|
|
||||
|
/** @var SimpleXMLElement */ |
||||
|
private $sheetViewAttributes; |
||||
|
|
||||
|
/** @var Worksheet */ |
||||
|
private $worksheet; |
||||
|
|
||||
|
public function __construct(SimpleXMLElement $sheetViewXml, Worksheet $workSheet) |
||||
|
{ |
||||
|
$this->sheetViewXml = $sheetViewXml; |
||||
|
$this->sheetViewAttributes = Xlsx::testSimpleXml($sheetViewXml->attributes()); |
||||
|
$this->worksheet = $workSheet; |
||||
|
} |
||||
|
|
||||
|
public function load(): void |
||||
|
{ |
||||
|
$this->topLeft(); |
||||
|
$this->zoomScale(); |
||||
|
$this->view(); |
||||
|
$this->gridLines(); |
||||
|
$this->headers(); |
||||
|
$this->direction(); |
||||
|
$this->showZeros(); |
||||
|
|
||||
|
if (isset($this->sheetViewXml->pane)) { |
||||
|
$this->pane(); |
||||
|
} |
||||
|
if (isset($this->sheetViewXml->selection, $this->sheetViewXml->selection->attributes()->sqref)) { |
||||
|
$this->selection(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function zoomScale(): void |
||||
|
{ |
||||
|
if (isset($this->sheetViewAttributes->zoomScale)) { |
||||
|
$zoomScale = (int) ($this->sheetViewAttributes->zoomScale); |
||||
|
if ($zoomScale <= 0) { |
||||
|
// setZoomScale will throw an Exception if the scale is less than or equals 0 |
||||
|
// that is OK when manually creating documents, but we should be able to read all documents |
||||
|
$zoomScale = 100; |
||||
|
} |
||||
|
|
||||
|
$this->worksheet->getSheetView()->setZoomScale($zoomScale); |
||||
|
} |
||||
|
|
||||
|
if (isset($this->sheetViewAttributes->zoomScaleNormal)) { |
||||
|
$zoomScaleNormal = (int) ($this->sheetViewAttributes->zoomScaleNormal); |
||||
|
if ($zoomScaleNormal <= 0) { |
||||
|
// setZoomScaleNormal will throw an Exception if the scale is less than or equals 0 |
||||
|
// that is OK when manually creating documents, but we should be able to read all documents |
||||
|
$zoomScaleNormal = 100; |
||||
|
} |
||||
|
|
||||
|
$this->worksheet->getSheetView()->setZoomScaleNormal($zoomScaleNormal); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function view(): void |
||||
|
{ |
||||
|
if (isset($this->sheetViewAttributes->view)) { |
||||
|
$this->worksheet->getSheetView()->setView((string) $this->sheetViewAttributes->view); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function topLeft(): void |
||||
|
{ |
||||
|
if (isset($this->sheetViewAttributes->topLeftCell)) { |
||||
|
$this->worksheet->setTopLeftCell($this->sheetViewAttributes->topLeftCell); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function gridLines(): void |
||||
|
{ |
||||
|
if (isset($this->sheetViewAttributes->showGridLines)) { |
||||
|
$this->worksheet->setShowGridLines( |
||||
|
self::boolean((string) $this->sheetViewAttributes->showGridLines) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function headers(): void |
||||
|
{ |
||||
|
if (isset($this->sheetViewAttributes->showRowColHeaders)) { |
||||
|
$this->worksheet->setShowRowColHeaders( |
||||
|
self::boolean((string) $this->sheetViewAttributes->showRowColHeaders) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function direction(): void |
||||
|
{ |
||||
|
if (isset($this->sheetViewAttributes->rightToLeft)) { |
||||
|
$this->worksheet->setRightToLeft( |
||||
|
self::boolean((string) $this->sheetViewAttributes->rightToLeft) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function showZeros(): void |
||||
|
{ |
||||
|
if (isset($this->sheetViewAttributes->showZeros)) { |
||||
|
$this->worksheet->getSheetView()->setShowZeros( |
||||
|
self::boolean((string) $this->sheetViewAttributes->showZeros) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function pane(): void |
||||
|
{ |
||||
|
$xSplit = 0; |
||||
|
$ySplit = 0; |
||||
|
$topLeftCell = null; |
||||
|
$paneAttributes = $this->sheetViewXml->pane->attributes(); |
||||
|
|
||||
|
if (isset($paneAttributes->xSplit)) { |
||||
|
$xSplit = (int) ($paneAttributes->xSplit); |
||||
|
} |
||||
|
|
||||
|
if (isset($paneAttributes->ySplit)) { |
||||
|
$ySplit = (int) ($paneAttributes->ySplit); |
||||
|
} |
||||
|
|
||||
|
if (isset($paneAttributes->topLeftCell)) { |
||||
|
$topLeftCell = (string) $paneAttributes->topLeftCell; |
||||
|
} |
||||
|
|
||||
|
$this->worksheet->freezePane( |
||||
|
Coordinate::stringFromColumnIndex($xSplit + 1) . ($ySplit + 1), |
||||
|
$topLeftCell |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private function selection(): void |
||||
|
{ |
||||
|
$attributes = $this->sheetViewXml->selection->attributes(); |
||||
|
if ($attributes !== null) { |
||||
|
$sqref = (string) $attributes->sqref; |
||||
|
$sqref = explode(' ', $sqref); |
||||
|
$sqref = $sqref[0]; |
||||
|
$this->worksheet->setSelectedCells($sqref); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,423 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Reader\Xlsx; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Alignment; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Border; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Borders; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Color; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Font; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Protection; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\Style; |
||||
|
use SimpleXMLElement; |
||||
|
use stdClass; |
||||
|
|
||||
|
class Styles extends BaseParserClass |
||||
|
{ |
||||
|
/** |
||||
|
* Theme instance. |
||||
|
* |
||||
|
* @var ?Theme |
||||
|
*/ |
||||
|
private $theme; |
||||
|
|
||||
|
/** @var array */ |
||||
|
private $styles = []; |
||||
|
|
||||
|
/** @var array */ |
||||
|
private $cellStyles = []; |
||||
|
|
||||
|
/** @var SimpleXMLElement */ |
||||
|
private $styleXml; |
||||
|
|
||||
|
/** @var string */ |
||||
|
private $namespace = ''; |
||||
|
|
||||
|
public function setNamespace(string $namespace): void |
||||
|
{ |
||||
|
$this->namespace = $namespace; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Cast SimpleXMLElement to bool to overcome Scrutinizer problem. |
||||
|
* |
||||
|
* @param mixed $value |
||||
|
*/ |
||||
|
private static function castBool($value): bool |
||||
|
{ |
||||
|
return (bool) $value; |
||||
|
} |
||||
|
|
||||
|
private function getStyleAttributes(SimpleXMLElement $value): SimpleXMLElement |
||||
|
{ |
||||
|
$attr = null; |
||||
|
if (self::castBool($value)) { |
||||
|
$attr = $value->attributes(''); |
||||
|
if ($attr === null || count($attr) === 0) { |
||||
|
$attr = $value->attributes($this->namespace); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Xlsx::testSimpleXml($attr); |
||||
|
} |
||||
|
|
||||
|
public function setStyleXml(SimpleXmlElement $styleXml): void |
||||
|
{ |
||||
|
$this->styleXml = $styleXml; |
||||
|
} |
||||
|
|
||||
|
public function setTheme(Theme $theme): void |
||||
|
{ |
||||
|
$this->theme = $theme; |
||||
|
} |
||||
|
|
||||
|
public function setStyleBaseData(?Theme $theme = null, array $styles = [], array $cellStyles = []): void |
||||
|
{ |
||||
|
$this->theme = $theme; |
||||
|
$this->styles = $styles; |
||||
|
$this->cellStyles = $cellStyles; |
||||
|
} |
||||
|
|
||||
|
public function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml): void |
||||
|
{ |
||||
|
if (isset($fontStyleXml->name)) { |
||||
|
$attr = $this->getStyleAttributes($fontStyleXml->name); |
||||
|
if (isset($attr['val'])) { |
||||
|
$fontStyle->setName((string) $attr['val']); |
||||
|
} |
||||
|
} |
||||
|
if (isset($fontStyleXml->sz)) { |
||||
|
$attr = $this->getStyleAttributes($fontStyleXml->sz); |
||||
|
if (isset($attr['val'])) { |
||||
|
$fontStyle->setSize((float) $attr['val']); |
||||
|
} |
||||
|
} |
||||
|
if (isset($fontStyleXml->b)) { |
||||
|
$attr = $this->getStyleAttributes($fontStyleXml->b); |
||||
|
$fontStyle->setBold(!isset($attr['val']) || self::boolean((string) $attr['val'])); |
||||
|
} |
||||
|
if (isset($fontStyleXml->i)) { |
||||
|
$attr = $this->getStyleAttributes($fontStyleXml->i); |
||||
|
$fontStyle->setItalic(!isset($attr['val']) || self::boolean((string) $attr['val'])); |
||||
|
} |
||||
|
if (isset($fontStyleXml->strike)) { |
||||
|
$attr = $this->getStyleAttributes($fontStyleXml->strike); |
||||
|
$fontStyle->setStrikethrough(!isset($attr['val']) || self::boolean((string) $attr['val'])); |
||||
|
} |
||||
|
$fontStyle->getColor()->setARGB($this->readColor($fontStyleXml->color)); |
||||
|
|
||||
|
if (isset($fontStyleXml->u)) { |
||||
|
$attr = $this->getStyleAttributes($fontStyleXml->u); |
||||
|
if (!isset($attr['val'])) { |
||||
|
$fontStyle->setUnderline(Font::UNDERLINE_SINGLE); |
||||
|
} else { |
||||
|
$fontStyle->setUnderline((string) $attr['val']); |
||||
|
} |
||||
|
} |
||||
|
if (isset($fontStyleXml->vertAlign)) { |
||||
|
$attr = $this->getStyleAttributes($fontStyleXml->vertAlign); |
||||
|
if (!isset($attr['val'])) { |
||||
|
$verticalAlign = strtolower((string) $attr['val']); |
||||
|
if ($verticalAlign === 'superscript') { |
||||
|
$fontStyle->setSuperscript(true); |
||||
|
} elseif ($verticalAlign === 'subscript') { |
||||
|
$fontStyle->setSubscript(true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLElement $numfmtStyleXml): void |
||||
|
{ |
||||
|
if ((string) $numfmtStyleXml['formatCode'] !== '') { |
||||
|
$numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmtStyleXml['formatCode'])); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
$numfmt = $this->getStyleAttributes($numfmtStyleXml); |
||||
|
if (isset($numfmt['formatCode'])) { |
||||
|
$numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmt['formatCode'])); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function readFillStyle(Fill $fillStyle, SimpleXMLElement $fillStyleXml): void |
||||
|
{ |
||||
|
if ($fillStyleXml->gradientFill) { |
||||
|
/** @var SimpleXMLElement $gradientFill */ |
||||
|
$gradientFill = $fillStyleXml->gradientFill[0]; |
||||
|
$attr = $this->getStyleAttributes($gradientFill); |
||||
|
if (!empty($attr['type'])) { |
||||
|
$fillStyle->setFillType((string) $attr['type']); |
||||
|
} |
||||
|
$fillStyle->setRotation((float) ($attr['degree'])); |
||||
|
$gradientFill->registerXPathNamespace('sml', Namespaces::MAIN); |
||||
|
$fillStyle->getStartColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=0]'))->color)); |
||||
|
$fillStyle->getEndColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color)); |
||||
|
} elseif ($fillStyleXml->patternFill) { |
||||
|
$defaultFillStyle = Fill::FILL_NONE; |
||||
|
if ($fillStyleXml->patternFill->fgColor) { |
||||
|
$fillStyle->getStartColor()->setARGB($this->readColor($fillStyleXml->patternFill->fgColor, true)); |
||||
|
$defaultFillStyle = Fill::FILL_SOLID; |
||||
|
} |
||||
|
if ($fillStyleXml->patternFill->bgColor) { |
||||
|
$fillStyle->getEndColor()->setARGB($this->readColor($fillStyleXml->patternFill->bgColor, true)); |
||||
|
$defaultFillStyle = Fill::FILL_SOLID; |
||||
|
} |
||||
|
|
||||
|
$type = ''; |
||||
|
if ((string) $fillStyleXml->patternFill['patternType'] !== '') { |
||||
|
$type = (string) $fillStyleXml->patternFill['patternType']; |
||||
|
} else { |
||||
|
$attr = $this->getStyleAttributes($fillStyleXml->patternFill); |
||||
|
$type = (string) $attr['patternType']; |
||||
|
} |
||||
|
$patternType = ($type === '') ? $defaultFillStyle : $type; |
||||
|
|
||||
|
$fillStyle->setFillType($patternType); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function readBorderStyle(Borders $borderStyle, SimpleXMLElement $borderStyleXml): void |
||||
|
{ |
||||
|
$diagonalUp = $this->getAttribute($borderStyleXml, 'diagonalUp'); |
||||
|
$diagonalUp = self::boolean($diagonalUp); |
||||
|
$diagonalDown = $this->getAttribute($borderStyleXml, 'diagonalDown'); |
||||
|
$diagonalDown = self::boolean($diagonalDown); |
||||
|
if (!$diagonalUp && !$diagonalDown) { |
||||
|
$borderStyle->setDiagonalDirection(Borders::DIAGONAL_NONE); |
||||
|
} elseif ($diagonalUp && !$diagonalDown) { |
||||
|
$borderStyle->setDiagonalDirection(Borders::DIAGONAL_UP); |
||||
|
} elseif (!$diagonalUp && $diagonalDown) { |
||||
|
$borderStyle->setDiagonalDirection(Borders::DIAGONAL_DOWN); |
||||
|
} else { |
||||
|
$borderStyle->setDiagonalDirection(Borders::DIAGONAL_BOTH); |
||||
|
} |
||||
|
|
||||
|
$this->readBorder($borderStyle->getLeft(), $borderStyleXml->left); |
||||
|
$this->readBorder($borderStyle->getRight(), $borderStyleXml->right); |
||||
|
$this->readBorder($borderStyle->getTop(), $borderStyleXml->top); |
||||
|
$this->readBorder($borderStyle->getBottom(), $borderStyleXml->bottom); |
||||
|
$this->readBorder($borderStyle->getDiagonal(), $borderStyleXml->diagonal); |
||||
|
} |
||||
|
|
||||
|
private function getAttribute(SimpleXMLElement $xml, string $attribute): string |
||||
|
{ |
||||
|
$style = ''; |
||||
|
if ((string) $xml[$attribute] !== '') { |
||||
|
$style = (string) $xml[$attribute]; |
||||
|
} else { |
||||
|
$attr = $this->getStyleAttributes($xml); |
||||
|
if (isset($attr[$attribute])) { |
||||
|
$style = (string) $attr[$attribute]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $style; |
||||
|
} |
||||
|
|
||||
|
private function readBorder(Border $border, SimpleXMLElement $borderXml): void |
||||
|
{ |
||||
|
$style = $this->getAttribute($borderXml, 'style'); |
||||
|
if ($style !== '') { |
||||
|
$border->setBorderStyle((string) $style); |
||||
|
} |
||||
|
if (isset($borderXml->color)) { |
||||
|
$border->getColor()->setARGB($this->readColor($borderXml->color)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function readAlignmentStyle(Alignment $alignment, SimpleXMLElement $alignmentXml): void |
||||
|
{ |
||||
|
$horizontal = $this->getAttribute($alignmentXml, 'horizontal'); |
||||
|
$alignment->setHorizontal($horizontal); |
||||
|
$vertical = $this->getAttribute($alignmentXml, 'vertical'); |
||||
|
$alignment->setVertical((string) $vertical); |
||||
|
|
||||
|
$textRotation = (int) $this->getAttribute($alignmentXml, 'textRotation'); |
||||
|
if ($textRotation > 90) { |
||||
|
$textRotation = 90 - $textRotation; |
||||
|
} |
||||
|
$alignment->setTextRotation($textRotation); |
||||
|
|
||||
|
$wrapText = $this->getAttribute($alignmentXml, 'wrapText'); |
||||
|
$alignment->setWrapText(self::boolean((string) $wrapText)); |
||||
|
$shrinkToFit = $this->getAttribute($alignmentXml, 'shrinkToFit'); |
||||
|
$alignment->setShrinkToFit(self::boolean((string) $shrinkToFit)); |
||||
|
$indent = (int) $this->getAttribute($alignmentXml, 'indent'); |
||||
|
$alignment->setIndent(max($indent, 0)); |
||||
|
$readingOrder = (int) $this->getAttribute($alignmentXml, 'readingOrder'); |
||||
|
$alignment->setReadOrder(max($readingOrder, 0)); |
||||
|
} |
||||
|
|
||||
|
private static function formatGeneral(string $formatString): string |
||||
|
{ |
||||
|
if ($formatString === 'GENERAL') { |
||||
|
$formatString = NumberFormat::FORMAT_GENERAL; |
||||
|
} |
||||
|
|
||||
|
return $formatString; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Read style. |
||||
|
* |
||||
|
* @param SimpleXMLElement|stdClass $style |
||||
|
*/ |
||||
|
public function readStyle(Style $docStyle, $style): void |
||||
|
{ |
||||
|
if ($style->numFmt instanceof SimpleXMLElement) { |
||||
|
$this->readNumberFormat($docStyle->getNumberFormat(), $style->numFmt); |
||||
|
} else { |
||||
|
$docStyle->getNumberFormat()->setFormatCode(self::formatGeneral((string) $style->numFmt)); |
||||
|
} |
||||
|
|
||||
|
if (isset($style->font)) { |
||||
|
$this->readFontStyle($docStyle->getFont(), $style->font); |
||||
|
} |
||||
|
|
||||
|
if (isset($style->fill)) { |
||||
|
$this->readFillStyle($docStyle->getFill(), $style->fill); |
||||
|
} |
||||
|
|
||||
|
if (isset($style->border)) { |
||||
|
$this->readBorderStyle($docStyle->getBorders(), $style->border); |
||||
|
} |
||||
|
|
||||
|
if (isset($style->alignment)) { |
||||
|
$this->readAlignmentStyle($docStyle->getAlignment(), $style->alignment); |
||||
|
} |
||||
|
|
||||
|
// protection |
||||
|
if (isset($style->protection)) { |
||||
|
$this->readProtectionLocked($docStyle, $style->protection); |
||||
|
$this->readProtectionHidden($docStyle, $style->protection); |
||||
|
} |
||||
|
|
||||
|
// top-level style settings |
||||
|
if (isset($style->quotePrefix)) { |
||||
|
$docStyle->setQuotePrefix((bool) $style->quotePrefix); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Read protection locked attribute. |
||||
|
*/ |
||||
|
public function readProtectionLocked(Style $docStyle, SimpleXMLElement $style): void |
||||
|
{ |
||||
|
$locked = ''; |
||||
|
if ((string) $style['locked'] !== '') { |
||||
|
$locked = (string) $style['locked']; |
||||
|
} else { |
||||
|
$attr = $this->getStyleAttributes($style); |
||||
|
if (isset($attr['locked'])) { |
||||
|
$locked = (string) $attr['locked']; |
||||
|
} |
||||
|
} |
||||
|
if ($locked !== '') { |
||||
|
if (self::boolean($locked)) { |
||||
|
$docStyle->getProtection()->setLocked(Protection::PROTECTION_PROTECTED); |
||||
|
} else { |
||||
|
$docStyle->getProtection()->setLocked(Protection::PROTECTION_UNPROTECTED); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Read protection hidden attribute. |
||||
|
*/ |
||||
|
public function readProtectionHidden(Style $docStyle, SimpleXMLElement $style): void |
||||
|
{ |
||||
|
$hidden = ''; |
||||
|
if ((string) $style['hidden'] !== '') { |
||||
|
$hidden = (string) $style['hidden']; |
||||
|
} else { |
||||
|
$attr = $this->getStyleAttributes($style); |
||||
|
if (isset($attr['hidden'])) { |
||||
|
$hidden = (string) $attr['hidden']; |
||||
|
} |
||||
|
} |
||||
|
if ($hidden !== '') { |
||||
|
if (self::boolean((string) $hidden)) { |
||||
|
$docStyle->getProtection()->setHidden(Protection::PROTECTION_PROTECTED); |
||||
|
} else { |
||||
|
$docStyle->getProtection()->setHidden(Protection::PROTECTION_UNPROTECTED); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function readColor(SimpleXMLElement $color, bool $background = false): string |
||||
|
{ |
||||
|
$attr = $this->getStyleAttributes($color); |
||||
|
if (isset($attr['rgb'])) { |
||||
|
return (string) $attr['rgb']; |
||||
|
} |
||||
|
if (isset($attr['indexed'])) { |
||||
|
return Color::indexedColor((int) ($attr['indexed'] - 7), $background)->getARGB() ?? ''; |
||||
|
} |
||||
|
if (isset($attr['theme'])) { |
||||
|
if ($this->theme !== null) { |
||||
|
$returnColour = $this->theme->getColourByIndex((int) $attr['theme']); |
||||
|
if (isset($attr['tint'])) { |
||||
|
$tintAdjust = (float) $attr['tint']; |
||||
|
$returnColour = Color::changeBrightness($returnColour ?? '', $tintAdjust); |
||||
|
} |
||||
|
|
||||
|
return 'FF' . $returnColour; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ($background) ? 'FFFFFFFF' : 'FF000000'; |
||||
|
} |
||||
|
|
||||
|
public function dxfs(bool $readDataOnly = false): array |
||||
|
{ |
||||
|
$dxfs = []; |
||||
|
if (!$readDataOnly && $this->styleXml) { |
||||
|
// Conditional Styles |
||||
|
if ($this->styleXml->dxfs) { |
||||
|
foreach ($this->styleXml->dxfs->dxf as $dxf) { |
||||
|
$style = new Style(false, true); |
||||
|
$this->readStyle($style, $dxf); |
||||
|
$dxfs[] = $style; |
||||
|
} |
||||
|
} |
||||
|
// Cell Styles |
||||
|
if ($this->styleXml->cellStyles) { |
||||
|
foreach ($this->styleXml->cellStyles->cellStyle as $cellStylex) { |
||||
|
$cellStyle = Xlsx::getAttributes($cellStylex); |
||||
|
if ((int) ($cellStyle['builtinId']) == 0) { |
||||
|
if (isset($this->cellStyles[(int) ($cellStyle['xfId'])])) { |
||||
|
// Set default style |
||||
|
$style = new Style(); |
||||
|
$this->readStyle($style, $this->cellStyles[(int) ($cellStyle['xfId'])]); |
||||
|
|
||||
|
// normal style, currently not using it for anything |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $dxfs; |
||||
|
} |
||||
|
|
||||
|
public function styles(): array |
||||
|
{ |
||||
|
return $this->styles; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get array item. |
||||
|
* |
||||
|
* @param mixed $array (usually array, in theory can be false) |
||||
|
* |
||||
|
* @return stdClass |
||||
|
*/ |
||||
|
private static function getArrayItem($array, int $key = 0) |
||||
|
{ |
||||
|
return is_array($array) ? ($array[$key] ?? null) : null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; |
||||
|
|
||||
|
class Theme |
||||
|
{ |
||||
|
/** |
||||
|
* Theme Name. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $themeName; |
||||
|
|
||||
|
/** |
||||
|
* Colour Scheme Name. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $colourSchemeName; |
||||
|
|
||||
|
/** |
||||
|
* Colour Map. |
||||
|
* |
||||
|
* @var string[] |
||||
|
*/ |
||||
|
private $colourMap; |
||||
|
|
||||
|
/** |
||||
|
* Create a new Theme. |
||||
|
* |
||||
|
* @param string $themeName |
||||
|
* @param string $colourSchemeName |
||||
|
* @param string[] $colourMap |
||||
|
*/ |
||||
|
public function __construct($themeName, $colourSchemeName, $colourMap) |
||||
|
{ |
||||
|
// Initialise values |
||||
|
$this->themeName = $themeName; |
||||
|
$this->colourSchemeName = $colourSchemeName; |
||||
|
$this->colourMap = $colourMap; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Theme Name. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getThemeName() |
||||
|
{ |
||||
|
return $this->themeName; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get colour Scheme Name. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getColourSchemeName() |
||||
|
{ |
||||
|
return $this->colourSchemeName; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get colour Map Value by Position. |
||||
|
* |
||||
|
* @param int $index |
||||
|
* |
||||
|
* @return null|string |
||||
|
*/ |
||||
|
public function getColourByIndex($index) |
||||
|
{ |
||||
|
if (isset($this->colourMap[$index])) { |
||||
|
return $this->colourMap[$index]; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Implement PHP __clone to create a deep clone, not just a shallow copy. |
||||
|
*/ |
||||
|
public function __clone() |
||||
|
{ |
||||
|
$vars = get_object_vars($this); |
||||
|
foreach ($vars as $key => $value) { |
||||
|
if ((is_object($value)) && ($key != '_parent')) { |
||||
|
$this->$key = clone $value; |
||||
|
} else { |
||||
|
$this->$key = $value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,536 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader; |
||||
|
|
||||
|
use DateTime; |
||||
|
use DateTimeZone; |
||||
|
use PhpOffice\PhpSpreadsheet\Cell\AddressHelper; |
||||
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; |
||||
|
use PhpOffice\PhpSpreadsheet\Cell\DataType; |
||||
|
use PhpOffice\PhpSpreadsheet\DefinedName; |
||||
|
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; |
||||
|
use PhpOffice\PhpSpreadsheet\Reader\Xml\PageSettings; |
||||
|
use PhpOffice\PhpSpreadsheet\Reader\Xml\Properties; |
||||
|
use PhpOffice\PhpSpreadsheet\Reader\Xml\Style; |
||||
|
use PhpOffice\PhpSpreadsheet\RichText\RichText; |
||||
|
use PhpOffice\PhpSpreadsheet\Settings; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\File; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; |
||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet; |
||||
|
use SimpleXMLElement; |
||||
|
|
||||
|
/** |
||||
|
* Reader for SpreadsheetML, the XML schema for Microsoft Office Excel 2003. |
||||
|
*/ |
||||
|
class Xml extends BaseReader |
||||
|
{ |
||||
|
/** |
||||
|
* Formats. |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
protected $styles = []; |
||||
|
|
||||
|
/** |
||||
|
* Create a new Excel2003XML Reader instance. |
||||
|
*/ |
||||
|
public function __construct() |
||||
|
{ |
||||
|
parent::__construct(); |
||||
|
$this->securityScanner = XmlScanner::getInstance($this); |
||||
|
} |
||||
|
|
||||
|
private $fileContents = ''; |
||||
|
|
||||
|
public static function xmlMappings(): array |
||||
|
{ |
||||
|
return array_merge( |
||||
|
Style\Fill::FILL_MAPPINGS, |
||||
|
Style\Border::BORDER_MAPPINGS |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Can the current IReader read the file? |
||||
|
*/ |
||||
|
public function canRead(string $filename): bool |
||||
|
{ |
||||
|
// Office xmlns:o="urn:schemas-microsoft-com:office:office" |
||||
|
// Excel xmlns:x="urn:schemas-microsoft-com:office:excel" |
||||
|
// XML Spreadsheet xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" |
||||
|
// Spreadsheet component xmlns:c="urn:schemas-microsoft-com:office:component:spreadsheet" |
||||
|
// XML schema xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" |
||||
|
// XML data type xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" |
||||
|
// MS-persist recordset xmlns:rs="urn:schemas-microsoft-com:rowset" |
||||
|
// Rowset xmlns:z="#RowsetSchema" |
||||
|
// |
||||
|
|
||||
|
$signature = [ |
||||
|
'<?xml version="1.0"', |
||||
|
'xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet', |
||||
|
]; |
||||
|
|
||||
|
// Open file |
||||
|
$data = file_get_contents($filename); |
||||
|
|
||||
|
// Why? |
||||
|
//$data = str_replace("'", '"', $data); // fix headers with single quote |
||||
|
|
||||
|
$valid = true; |
||||
|
foreach ($signature as $match) { |
||||
|
// every part of the signature must be present |
||||
|
if (strpos($data, $match) === false) { |
||||
|
$valid = false; |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Retrieve charset encoding |
||||
|
if (preg_match('/<?xml.*encoding=[\'"](.*?)[\'"].*?>/m', $data, $matches)) { |
||||
|
$charSet = strtoupper($matches[1]); |
||||
|
if (1 == preg_match('/^ISO-8859-\d[\dL]?$/i', $charSet)) { |
||||
|
$data = StringHelper::convertEncoding($data, 'UTF-8', $charSet); |
||||
|
$data = preg_replace('/(<?xml.*encoding=[\'"]).*?([\'"].*?>)/um', '$1' . 'UTF-8' . '$2', $data, 1); |
||||
|
} |
||||
|
} |
||||
|
$this->fileContents = $data; |
||||
|
|
||||
|
return $valid; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if the file is a valid SimpleXML. |
||||
|
* |
||||
|
* @param string $filename |
||||
|
* |
||||
|
* @return false|SimpleXMLElement |
||||
|
*/ |
||||
|
public function trySimpleXMLLoadString($filename) |
||||
|
{ |
||||
|
try { |
||||
|
$xml = simplexml_load_string( |
||||
|
$this->securityScanner->scan($this->fileContents ?: file_get_contents($filename)), |
||||
|
'SimpleXMLElement', |
||||
|
Settings::getLibXmlLoaderOptions() |
||||
|
); |
||||
|
} catch (\Exception $e) { |
||||
|
throw new Exception('Cannot load invalid XML file: ' . $filename, 0, $e); |
||||
|
} |
||||
|
$this->fileContents = ''; |
||||
|
|
||||
|
return $xml; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. |
||||
|
* |
||||
|
* @param string $filename |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function listWorksheetNames($filename) |
||||
|
{ |
||||
|
File::assertFile($filename); |
||||
|
if (!$this->canRead($filename)) { |
||||
|
throw new Exception($filename . ' is an Invalid Spreadsheet file.'); |
||||
|
} |
||||
|
|
||||
|
$worksheetNames = []; |
||||
|
|
||||
|
$xml = $this->trySimpleXMLLoadString($filename); |
||||
|
if ($xml === false) { |
||||
|
throw new Exception("Problem reading {$filename}"); |
||||
|
} |
||||
|
|
||||
|
$namespaces = $xml->getNamespaces(true); |
||||
|
|
||||
|
$xml_ss = $xml->children($namespaces['ss']); |
||||
|
foreach ($xml_ss->Worksheet as $worksheet) { |
||||
|
$worksheet_ss = self::getAttributes($worksheet, $namespaces['ss']); |
||||
|
$worksheetNames[] = (string) $worksheet_ss['Name']; |
||||
|
} |
||||
|
|
||||
|
return $worksheetNames; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). |
||||
|
* |
||||
|
* @param string $filename |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function listWorksheetInfo($filename) |
||||
|
{ |
||||
|
File::assertFile($filename); |
||||
|
if (!$this->canRead($filename)) { |
||||
|
throw new Exception($filename . ' is an Invalid Spreadsheet file.'); |
||||
|
} |
||||
|
|
||||
|
$worksheetInfo = []; |
||||
|
|
||||
|
$xml = $this->trySimpleXMLLoadString($filename); |
||||
|
if ($xml === false) { |
||||
|
throw new Exception("Problem reading {$filename}"); |
||||
|
} |
||||
|
|
||||
|
$namespaces = $xml->getNamespaces(true); |
||||
|
|
||||
|
$worksheetID = 1; |
||||
|
$xml_ss = $xml->children($namespaces['ss']); |
||||
|
foreach ($xml_ss->Worksheet as $worksheet) { |
||||
|
$worksheet_ss = self::getAttributes($worksheet, $namespaces['ss']); |
||||
|
|
||||
|
$tmpInfo = []; |
||||
|
$tmpInfo['worksheetName'] = ''; |
||||
|
$tmpInfo['lastColumnLetter'] = 'A'; |
||||
|
$tmpInfo['lastColumnIndex'] = 0; |
||||
|
$tmpInfo['totalRows'] = 0; |
||||
|
$tmpInfo['totalColumns'] = 0; |
||||
|
|
||||
|
$tmpInfo['worksheetName'] = "Worksheet_{$worksheetID}"; |
||||
|
if (isset($worksheet_ss['Name'])) { |
||||
|
$tmpInfo['worksheetName'] = (string) $worksheet_ss['Name']; |
||||
|
} |
||||
|
|
||||
|
if (isset($worksheet->Table->Row)) { |
||||
|
$rowIndex = 0; |
||||
|
|
||||
|
foreach ($worksheet->Table->Row as $rowData) { |
||||
|
$columnIndex = 0; |
||||
|
$rowHasData = false; |
||||
|
|
||||
|
foreach ($rowData->Cell as $cell) { |
||||
|
if (isset($cell->Data)) { |
||||
|
$tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex); |
||||
|
$rowHasData = true; |
||||
|
} |
||||
|
|
||||
|
++$columnIndex; |
||||
|
} |
||||
|
|
||||
|
++$rowIndex; |
||||
|
|
||||
|
if ($rowHasData) { |
||||
|
$tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); |
||||
|
$tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1; |
||||
|
|
||||
|
$worksheetInfo[] = $tmpInfo; |
||||
|
++$worksheetID; |
||||
|
} |
||||
|
|
||||
|
return $worksheetInfo; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Loads Spreadsheet from file. |
||||
|
* |
||||
|
* @return Spreadsheet |
||||
|
*/ |
||||
|
public function load(string $filename, int $flags = 0) |
||||
|
{ |
||||
|
$this->processFlags($flags); |
||||
|
|
||||
|
// Create new Spreadsheet |
||||
|
$spreadsheet = new Spreadsheet(); |
||||
|
$spreadsheet->removeSheetByIndex(0); |
||||
|
|
||||
|
// Load into this instance |
||||
|
return $this->loadIntoExisting($filename, $spreadsheet); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Loads from file into Spreadsheet instance. |
||||
|
* |
||||
|
* @param string $filename |
||||
|
* |
||||
|
* @return Spreadsheet |
||||
|
*/ |
||||
|
public function loadIntoExisting($filename, Spreadsheet $spreadsheet) |
||||
|
{ |
||||
|
File::assertFile($filename); |
||||
|
if (!$this->canRead($filename)) { |
||||
|
throw new Exception($filename . ' is an Invalid Spreadsheet file.'); |
||||
|
} |
||||
|
|
||||
|
$xml = $this->trySimpleXMLLoadString($filename); |
||||
|
if ($xml === false) { |
||||
|
throw new Exception("Problem reading {$filename}"); |
||||
|
} |
||||
|
|
||||
|
$namespaces = $xml->getNamespaces(true); |
||||
|
|
||||
|
(new Properties($spreadsheet))->readProperties($xml, $namespaces); |
||||
|
|
||||
|
$this->styles = (new Style())->parseStyles($xml, $namespaces); |
||||
|
|
||||
|
$worksheetID = 0; |
||||
|
$xml_ss = $xml->children($namespaces['ss']); |
||||
|
|
||||
|
/** @var null|SimpleXMLElement $worksheetx */ |
||||
|
foreach ($xml_ss->Worksheet as $worksheetx) { |
||||
|
$worksheet = $worksheetx ?? new SimpleXMLElement('<xml></xml>'); |
||||
|
$worksheet_ss = self::getAttributes($worksheet, $namespaces['ss']); |
||||
|
|
||||
|
if ( |
||||
|
isset($this->loadSheetsOnly, $worksheet_ss['Name']) && |
||||
|
(!in_array($worksheet_ss['Name'], $this->loadSheetsOnly)) |
||||
|
) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// Create new Worksheet |
||||
|
$spreadsheet->createSheet(); |
||||
|
$spreadsheet->setActiveSheetIndex($worksheetID); |
||||
|
$worksheetName = ''; |
||||
|
if (isset($worksheet_ss['Name'])) { |
||||
|
$worksheetName = (string) $worksheet_ss['Name']; |
||||
|
// Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in |
||||
|
// formula cells... during the load, all formulae should be correct, and we're simply bringing |
||||
|
// the worksheet name in line with the formula, not the reverse |
||||
|
$spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); |
||||
|
} |
||||
|
|
||||
|
// locally scoped defined names |
||||
|
if (isset($worksheet->Names[0])) { |
||||
|
foreach ($worksheet->Names[0] as $definedName) { |
||||
|
$definedName_ss = self::getAttributes($definedName, $namespaces['ss']); |
||||
|
$name = (string) $definedName_ss['Name']; |
||||
|
$definedValue = (string) $definedName_ss['RefersTo']; |
||||
|
$convertedValue = AddressHelper::convertFormulaToA1($definedValue); |
||||
|
if ($convertedValue[0] === '=') { |
||||
|
$convertedValue = substr($convertedValue, 1); |
||||
|
} |
||||
|
$spreadsheet->addDefinedName(DefinedName::createInstance($name, $spreadsheet->getActiveSheet(), $convertedValue, true)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$columnID = 'A'; |
||||
|
if (isset($worksheet->Table->Column)) { |
||||
|
foreach ($worksheet->Table->Column as $columnData) { |
||||
|
$columnData_ss = self::getAttributes($columnData, $namespaces['ss']); |
||||
|
if (isset($columnData_ss['Index'])) { |
||||
|
$columnID = Coordinate::stringFromColumnIndex((int) $columnData_ss['Index']); |
||||
|
} |
||||
|
if (isset($columnData_ss['Width'])) { |
||||
|
$columnWidth = $columnData_ss['Width']; |
||||
|
$spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4); |
||||
|
} |
||||
|
++$columnID; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$rowID = 1; |
||||
|
if (isset($worksheet->Table->Row)) { |
||||
|
$additionalMergedCells = 0; |
||||
|
foreach ($worksheet->Table->Row as $rowData) { |
||||
|
$rowHasData = false; |
||||
|
$row_ss = self::getAttributes($rowData, $namespaces['ss']); |
||||
|
if (isset($row_ss['Index'])) { |
||||
|
$rowID = (int) $row_ss['Index']; |
||||
|
} |
||||
|
|
||||
|
$columnID = 'A'; |
||||
|
foreach ($rowData->Cell as $cell) { |
||||
|
$cell_ss = self::getAttributes($cell, $namespaces['ss']); |
||||
|
if (isset($cell_ss['Index'])) { |
||||
|
$columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']); |
||||
|
} |
||||
|
$cellRange = $columnID . $rowID; |
||||
|
|
||||
|
if ($this->getReadFilter() !== null) { |
||||
|
if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { |
||||
|
++$columnID; |
||||
|
|
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isset($cell_ss['HRef'])) { |
||||
|
$spreadsheet->getActiveSheet()->getCell($cellRange)->getHyperlink()->setUrl((string) $cell_ss['HRef']); |
||||
|
} |
||||
|
|
||||
|
if ((isset($cell_ss['MergeAcross'])) || (isset($cell_ss['MergeDown']))) { |
||||
|
$columnTo = $columnID; |
||||
|
if (isset($cell_ss['MergeAcross'])) { |
||||
|
$additionalMergedCells += (int) $cell_ss['MergeAcross']; |
||||
|
$columnTo = Coordinate::stringFromColumnIndex((int) (Coordinate::columnIndexFromString($columnID) + $cell_ss['MergeAcross'])); |
||||
|
} |
||||
|
$rowTo = $rowID; |
||||
|
if (isset($cell_ss['MergeDown'])) { |
||||
|
$rowTo = $rowTo + $cell_ss['MergeDown']; |
||||
|
} |
||||
|
$cellRange .= ':' . $columnTo . $rowTo; |
||||
|
$spreadsheet->getActiveSheet()->mergeCells($cellRange); |
||||
|
} |
||||
|
|
||||
|
$hasCalculatedValue = false; |
||||
|
$cellDataFormula = ''; |
||||
|
if (isset($cell_ss['Formula'])) { |
||||
|
$cellDataFormula = $cell_ss['Formula']; |
||||
|
$hasCalculatedValue = true; |
||||
|
} |
||||
|
if (isset($cell->Data)) { |
||||
|
$cellData = $cell->Data; |
||||
|
$cellValue = (string) $cellData; |
||||
|
$type = DataType::TYPE_NULL; |
||||
|
$cellData_ss = self::getAttributes($cellData, $namespaces['ss']); |
||||
|
if (isset($cellData_ss['Type'])) { |
||||
|
$cellDataType = $cellData_ss['Type']; |
||||
|
switch ($cellDataType) { |
||||
|
/* |
||||
|
const TYPE_STRING = 's'; |
||||
|
const TYPE_FORMULA = 'f'; |
||||
|
const TYPE_NUMERIC = 'n'; |
||||
|
const TYPE_BOOL = 'b'; |
||||
|
const TYPE_NULL = 'null'; |
||||
|
const TYPE_INLINE = 'inlineStr'; |
||||
|
const TYPE_ERROR = 'e'; |
||||
|
*/ |
||||
|
case 'String': |
||||
|
$type = DataType::TYPE_STRING; |
||||
|
|
||||
|
break; |
||||
|
case 'Number': |
||||
|
$type = DataType::TYPE_NUMERIC; |
||||
|
$cellValue = (float) $cellValue; |
||||
|
if (floor($cellValue) == $cellValue) { |
||||
|
$cellValue = (int) $cellValue; |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
case 'Boolean': |
||||
|
$type = DataType::TYPE_BOOL; |
||||
|
$cellValue = ($cellValue != 0); |
||||
|
|
||||
|
break; |
||||
|
case 'DateTime': |
||||
|
$type = DataType::TYPE_NUMERIC; |
||||
|
$dateTime = new DateTime($cellValue, new DateTimeZone('UTC')); |
||||
|
$cellValue = Date::PHPToExcel($dateTime); |
||||
|
|
||||
|
break; |
||||
|
case 'Error': |
||||
|
$type = DataType::TYPE_ERROR; |
||||
|
$hasCalculatedValue = false; |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($hasCalculatedValue) { |
||||
|
$type = DataType::TYPE_FORMULA; |
||||
|
$columnNumber = Coordinate::columnIndexFromString($columnID); |
||||
|
$cellDataFormula = AddressHelper::convertFormulaToA1($cellDataFormula, $rowID, $columnNumber); |
||||
|
} |
||||
|
|
||||
|
$spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValueExplicit((($hasCalculatedValue) ? $cellDataFormula : $cellValue), $type); |
||||
|
if ($hasCalculatedValue) { |
||||
|
$spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setCalculatedValue($cellValue); |
||||
|
} |
||||
|
$rowHasData = true; |
||||
|
} |
||||
|
|
||||
|
if (isset($cell->Comment)) { |
||||
|
$this->parseCellComment($cell->Comment, $namespaces, $spreadsheet, $columnID, $rowID); |
||||
|
} |
||||
|
|
||||
|
if (isset($cell_ss['StyleID'])) { |
||||
|
$style = (string) $cell_ss['StyleID']; |
||||
|
if ((isset($this->styles[$style])) && (!empty($this->styles[$style]))) { |
||||
|
//if (!$spreadsheet->getActiveSheet()->cellExists($columnID . $rowID)) { |
||||
|
// $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValue(null); |
||||
|
//} |
||||
|
$spreadsheet->getActiveSheet()->getStyle($cellRange) |
||||
|
->applyFromArray($this->styles[$style]); |
||||
|
} |
||||
|
} |
||||
|
++$columnID; |
||||
|
while ($additionalMergedCells > 0) { |
||||
|
++$columnID; |
||||
|
--$additionalMergedCells; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($rowHasData) { |
||||
|
if (isset($row_ss['Height'])) { |
||||
|
$rowHeight = $row_ss['Height']; |
||||
|
$spreadsheet->getActiveSheet()->getRowDimension($rowID)->setRowHeight((float) $rowHeight); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
++$rowID; |
||||
|
} |
||||
|
|
||||
|
if (isset($namespaces['x'])) { |
||||
|
$xmlX = $worksheet->children($namespaces['x']); |
||||
|
if (isset($xmlX->WorksheetOptions)) { |
||||
|
(new PageSettings($xmlX, $namespaces))->loadPageSettings($spreadsheet); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
++$worksheetID; |
||||
|
} |
||||
|
|
||||
|
// Globally scoped defined names |
||||
|
$activeWorksheet = $spreadsheet->setActiveSheetIndex(0); |
||||
|
if (isset($xml->Names[0])) { |
||||
|
foreach ($xml->Names[0] as $definedName) { |
||||
|
$definedName_ss = self::getAttributes($definedName, $namespaces['ss']); |
||||
|
$name = (string) $definedName_ss['Name']; |
||||
|
$definedValue = (string) $definedName_ss['RefersTo']; |
||||
|
$convertedValue = AddressHelper::convertFormulaToA1($definedValue); |
||||
|
if ($convertedValue[0] === '=') { |
||||
|
$convertedValue = substr($convertedValue, 1); |
||||
|
} |
||||
|
$spreadsheet->addDefinedName(DefinedName::createInstance($name, $activeWorksheet, $convertedValue)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Return |
||||
|
return $spreadsheet; |
||||
|
} |
||||
|
|
||||
|
protected function parseCellComment( |
||||
|
SimpleXMLElement $comment, |
||||
|
array $namespaces, |
||||
|
Spreadsheet $spreadsheet, |
||||
|
string $columnID, |
||||
|
int $rowID |
||||
|
): void { |
||||
|
$commentAttributes = $comment->attributes($namespaces['ss']); |
||||
|
$author = 'unknown'; |
||||
|
if (isset($commentAttributes->Author)) { |
||||
|
$author = (string) $commentAttributes->Author; |
||||
|
} |
||||
|
|
||||
|
$node = $comment->Data->asXML(); |
||||
|
$annotation = strip_tags((string) $node); |
||||
|
$spreadsheet->getActiveSheet()->getComment($columnID . $rowID) |
||||
|
->setAuthor($author) |
||||
|
->setText($this->parseRichText($annotation)); |
||||
|
} |
||||
|
|
||||
|
protected function parseRichText(string $annotation): RichText |
||||
|
{ |
||||
|
$value = new RichText(); |
||||
|
|
||||
|
$value->createText($annotation); |
||||
|
|
||||
|
return $value; |
||||
|
} |
||||
|
|
||||
|
private static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement |
||||
|
{ |
||||
|
return ($simple === null) |
||||
|
? new SimpleXMLElement('<xml></xml>') |
||||
|
: ($simple->attributes($node) ?? new SimpleXMLElement('<xml></xml>')); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,157 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Xml; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Document\Properties as DocumentProperties; |
||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet; |
||||
|
use SimpleXMLElement; |
||||
|
|
||||
|
class Properties |
||||
|
{ |
||||
|
/** |
||||
|
* @var Spreadsheet |
||||
|
*/ |
||||
|
protected $spreadsheet; |
||||
|
|
||||
|
public function __construct(Spreadsheet $spreadsheet) |
||||
|
{ |
||||
|
$this->spreadsheet = $spreadsheet; |
||||
|
} |
||||
|
|
||||
|
public function readProperties(SimpleXMLElement $xml, array $namespaces): void |
||||
|
{ |
||||
|
$this->readStandardProperties($xml); |
||||
|
$this->readCustomProperties($xml, $namespaces); |
||||
|
} |
||||
|
|
||||
|
protected function readStandardProperties(SimpleXMLElement $xml): void |
||||
|
{ |
||||
|
if (isset($xml->DocumentProperties[0])) { |
||||
|
$docProps = $this->spreadsheet->getProperties(); |
||||
|
|
||||
|
foreach ($xml->DocumentProperties[0] as $propertyName => $propertyValue) { |
||||
|
$propertyValue = (string) $propertyValue; |
||||
|
|
||||
|
$this->processStandardProperty($docProps, $propertyName, $propertyValue); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected function readCustomProperties(SimpleXMLElement $xml, array $namespaces): void |
||||
|
{ |
||||
|
if (isset($xml->CustomDocumentProperties)) { |
||||
|
$docProps = $this->spreadsheet->getProperties(); |
||||
|
|
||||
|
foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) { |
||||
|
$propertyAttributes = self::getAttributes($propertyValue, $namespaces['dt']); |
||||
|
$propertyName = preg_replace_callback('/_x([0-9a-f]{4})_/i', [$this, 'hex2str'], $propertyName); |
||||
|
|
||||
|
$this->processCustomProperty($docProps, $propertyName, $propertyValue, $propertyAttributes); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected function processStandardProperty( |
||||
|
DocumentProperties $docProps, |
||||
|
string $propertyName, |
||||
|
string $stringValue |
||||
|
): void { |
||||
|
switch ($propertyName) { |
||||
|
case 'Title': |
||||
|
$docProps->setTitle($stringValue); |
||||
|
|
||||
|
break; |
||||
|
case 'Subject': |
||||
|
$docProps->setSubject($stringValue); |
||||
|
|
||||
|
break; |
||||
|
case 'Author': |
||||
|
$docProps->setCreator($stringValue); |
||||
|
|
||||
|
break; |
||||
|
case 'Created': |
||||
|
$docProps->setCreated($stringValue); |
||||
|
|
||||
|
break; |
||||
|
case 'LastAuthor': |
||||
|
$docProps->setLastModifiedBy($stringValue); |
||||
|
|
||||
|
break; |
||||
|
case 'LastSaved': |
||||
|
$docProps->setModified($stringValue); |
||||
|
|
||||
|
break; |
||||
|
case 'Company': |
||||
|
$docProps->setCompany($stringValue); |
||||
|
|
||||
|
break; |
||||
|
case 'Category': |
||||
|
$docProps->setCategory($stringValue); |
||||
|
|
||||
|
break; |
||||
|
case 'Manager': |
||||
|
$docProps->setManager($stringValue); |
||||
|
|
||||
|
break; |
||||
|
case 'Keywords': |
||||
|
$docProps->setKeywords($stringValue); |
||||
|
|
||||
|
break; |
||||
|
case 'Description': |
||||
|
$docProps->setDescription($stringValue); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected function processCustomProperty( |
||||
|
DocumentProperties $docProps, |
||||
|
string $propertyName, |
||||
|
?SimpleXMLElement $propertyValue, |
||||
|
SimpleXMLElement $propertyAttributes |
||||
|
): void { |
||||
|
$propertyType = DocumentProperties::PROPERTY_TYPE_UNKNOWN; |
||||
|
|
||||
|
switch ((string) $propertyAttributes) { |
||||
|
case 'string': |
||||
|
$propertyType = DocumentProperties::PROPERTY_TYPE_STRING; |
||||
|
$propertyValue = trim((string) $propertyValue); |
||||
|
|
||||
|
break; |
||||
|
case 'boolean': |
||||
|
$propertyType = DocumentProperties::PROPERTY_TYPE_BOOLEAN; |
||||
|
$propertyValue = (bool) $propertyValue; |
||||
|
|
||||
|
break; |
||||
|
case 'integer': |
||||
|
$propertyType = DocumentProperties::PROPERTY_TYPE_INTEGER; |
||||
|
$propertyValue = (int) $propertyValue; |
||||
|
|
||||
|
break; |
||||
|
case 'float': |
||||
|
$propertyType = DocumentProperties::PROPERTY_TYPE_FLOAT; |
||||
|
$propertyValue = (float) $propertyValue; |
||||
|
|
||||
|
break; |
||||
|
case 'dateTime.tz': |
||||
|
$propertyType = DocumentProperties::PROPERTY_TYPE_DATE; |
||||
|
$propertyValue = trim((string) $propertyValue); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
$docProps->setCustomProperty($propertyName, $propertyValue, $propertyType); |
||||
|
} |
||||
|
|
||||
|
protected function hex2str(array $hex): string |
||||
|
{ |
||||
|
return mb_chr((int) hexdec($hex[1]), 'UTF-8'); |
||||
|
} |
||||
|
|
||||
|
private static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement |
||||
|
{ |
||||
|
return ($simple === null) |
||||
|
? new SimpleXMLElement('<xml></xml>') |
||||
|
: ($simple->attributes($node) ?? new SimpleXMLElement('<xml></xml>')); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Xml; |
||||
|
|
||||
|
use SimpleXMLElement; |
||||
|
|
||||
|
class Style |
||||
|
{ |
||||
|
/** |
||||
|
* Formats. |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
protected $styles = []; |
||||
|
|
||||
|
public function parseStyles(SimpleXMLElement $xml, array $namespaces): array |
||||
|
{ |
||||
|
if (!isset($xml->Styles)) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
$alignmentStyleParser = new Style\Alignment(); |
||||
|
$borderStyleParser = new Style\Border(); |
||||
|
$fontStyleParser = new Style\Font(); |
||||
|
$fillStyleParser = new Style\Fill(); |
||||
|
$numberFormatStyleParser = new Style\NumberFormat(); |
||||
|
|
||||
|
foreach ($xml->Styles[0] as $style) { |
||||
|
$style_ss = self::getAttributes($style, $namespaces['ss']); |
||||
|
$styleID = (string) $style_ss['ID']; |
||||
|
$this->styles[$styleID] = $this->styles['Default'] ?? []; |
||||
|
|
||||
|
$alignment = $border = $font = $fill = $numberFormat = []; |
||||
|
|
||||
|
foreach ($style as $styleType => $styleDatax) { |
||||
|
$styleData = $styleDatax ?? new SimpleXMLElement('<xml></xml>'); |
||||
|
$styleAttributes = $styleData->attributes($namespaces['ss']); |
||||
|
|
||||
|
switch ($styleType) { |
||||
|
case 'Alignment': |
||||
|
if ($styleAttributes) { |
||||
|
$alignment = $alignmentStyleParser->parseStyle($styleAttributes); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
case 'Borders': |
||||
|
$border = $borderStyleParser->parseStyle($styleData, $namespaces); |
||||
|
|
||||
|
break; |
||||
|
case 'Font': |
||||
|
if ($styleAttributes) { |
||||
|
$font = $fontStyleParser->parseStyle($styleAttributes); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
case 'Interior': |
||||
|
if ($styleAttributes) { |
||||
|
$fill = $fillStyleParser->parseStyle($styleAttributes); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
case 'NumberFormat': |
||||
|
if ($styleAttributes) { |
||||
|
$numberFormat = $numberFormatStyleParser->parseStyle($styleAttributes); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$this->styles[$styleID] = array_merge($alignment, $border, $font, $fill, $numberFormat); |
||||
|
} |
||||
|
|
||||
|
return $this->styles; |
||||
|
} |
||||
|
|
||||
|
protected static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement |
||||
|
{ |
||||
|
return ($simple === null) |
||||
|
? new SimpleXMLElement('<xml></xml>') |
||||
|
: ($simple->attributes($node) ?? new SimpleXMLElement('<xml></xml>')); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Reader\Xml\Style; |
||||
|
|
||||
|
use SimpleXMLElement; |
||||
|
|
||||
|
abstract class StyleBase |
||||
|
{ |
||||
|
protected static function identifyFixedStyleValue(array $styleList, string &$styleAttributeValue): bool |
||||
|
{ |
||||
|
$returnValue = false; |
||||
|
|
||||
|
$styleAttributeValue = strtolower($styleAttributeValue); |
||||
|
foreach ($styleList as $style) { |
||||
|
if ($styleAttributeValue == strtolower($style)) { |
||||
|
$styleAttributeValue = $style; |
||||
|
$returnValue = true; |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $returnValue; |
||||
|
} |
||||
|
|
||||
|
protected static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement |
||||
|
{ |
||||
|
return ($simple === null) |
||||
|
? new SimpleXMLElement('<xml></xml>') |
||||
|
: ($simple->attributes($node) ?? new SimpleXMLElement('<xml></xml>')); |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,168 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\RichText; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Cell\Cell; |
||||
|
use PhpOffice\PhpSpreadsheet\Cell\DataType; |
||||
|
use PhpOffice\PhpSpreadsheet\IComparable; |
||||
|
|
||||
|
class RichText implements IComparable |
||||
|
{ |
||||
|
/** |
||||
|
* Rich text elements. |
||||
|
* |
||||
|
* @var ITextElement[] |
||||
|
*/ |
||||
|
private $richTextElements; |
||||
|
|
||||
|
/** |
||||
|
* Create a new RichText instance. |
||||
|
*/ |
||||
|
public function __construct(?Cell $cell = null) |
||||
|
{ |
||||
|
// Initialise variables |
||||
|
$this->richTextElements = []; |
||||
|
|
||||
|
// Rich-Text string attached to cell? |
||||
|
if ($cell !== null) { |
||||
|
// Add cell text and style |
||||
|
if ($cell->getValue() != '') { |
||||
|
$objRun = new Run($cell->getValue()); |
||||
|
$objRun->setFont(clone $cell->getWorksheet()->getStyle($cell->getCoordinate())->getFont()); |
||||
|
$this->addText($objRun); |
||||
|
} |
||||
|
|
||||
|
// Set parent value |
||||
|
$cell->setValueExplicit($this, DataType::TYPE_STRING); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Add text. |
||||
|
* |
||||
|
* @param ITextElement $text Rich text element |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function addText(ITextElement $text) |
||||
|
{ |
||||
|
$this->richTextElements[] = $text; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Create text. |
||||
|
* |
||||
|
* @param string $text Text |
||||
|
* |
||||
|
* @return TextElement |
||||
|
*/ |
||||
|
public function createText($text) |
||||
|
{ |
||||
|
$objText = new TextElement($text); |
||||
|
$this->addText($objText); |
||||
|
|
||||
|
return $objText; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Create text run. |
||||
|
* |
||||
|
* @param string $text Text |
||||
|
* |
||||
|
* @return Run |
||||
|
*/ |
||||
|
public function createTextRun($text) |
||||
|
{ |
||||
|
$objText = new Run($text); |
||||
|
$this->addText($objText); |
||||
|
|
||||
|
return $objText; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get plain text. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getPlainText() |
||||
|
{ |
||||
|
// Return value |
||||
|
$returnValue = ''; |
||||
|
|
||||
|
// Loop through all ITextElements |
||||
|
foreach ($this->richTextElements as $text) { |
||||
|
$returnValue .= $text->getText(); |
||||
|
} |
||||
|
|
||||
|
return $returnValue; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert to string. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function __toString() |
||||
|
{ |
||||
|
return $this->getPlainText(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get Rich Text elements. |
||||
|
* |
||||
|
* @return ITextElement[] |
||||
|
*/ |
||||
|
public function getRichTextElements() |
||||
|
{ |
||||
|
return $this->richTextElements; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set Rich Text elements. |
||||
|
* |
||||
|
* @param ITextElement[] $textElements Array of elements |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setRichTextElements(array $textElements) |
||||
|
{ |
||||
|
$this->richTextElements = $textElements; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get hash code. |
||||
|
* |
||||
|
* @return string Hash code |
||||
|
*/ |
||||
|
public function getHashCode() |
||||
|
{ |
||||
|
$hashElements = ''; |
||||
|
foreach ($this->richTextElements as $element) { |
||||
|
$hashElements .= $element->getHashCode(); |
||||
|
} |
||||
|
|
||||
|
return md5( |
||||
|
$hashElements . |
||||
|
__CLASS__ |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Implement PHP __clone to create a deep clone, not just a shallow copy. |
||||
|
*/ |
||||
|
public function __clone() |
||||
|
{ |
||||
|
$vars = get_object_vars($this); |
||||
|
foreach ($vars as $key => $value) { |
||||
|
if (is_object($value)) { |
||||
|
$this->$key = clone $value; |
||||
|
} else { |
||||
|
$this->$key = $value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\RichText; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Style\Font; |
||||
|
|
||||
|
class Run extends TextElement implements ITextElement |
||||
|
{ |
||||
|
/** |
||||
|
* Font. |
||||
|
* |
||||
|
* @var Font |
||||
|
*/ |
||||
|
private $font; |
||||
|
|
||||
|
/** |
||||
|
* Create a new Run instance. |
||||
|
* |
||||
|
* @param string $text Text |
||||
|
*/ |
||||
|
public function __construct($text = '') |
||||
|
{ |
||||
|
parent::__construct($text); |
||||
|
// Initialise variables |
||||
|
$this->font = new Font(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get font. |
||||
|
* |
||||
|
* @return null|\PhpOffice\PhpSpreadsheet\Style\Font |
||||
|
*/ |
||||
|
public function getFont() |
||||
|
{ |
||||
|
return $this->font; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set font. |
||||
|
* |
||||
|
* @param Font $font Font |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setFont(?Font $font = null) |
||||
|
{ |
||||
|
$this->font = $font; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get hash code. |
||||
|
* |
||||
|
* @return string Hash code |
||||
|
*/ |
||||
|
public function getHashCode() |
||||
|
{ |
||||
|
return md5( |
||||
|
$this->getText() . |
||||
|
$this->font->getHashCode() . |
||||
|
__CLASS__ |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\RichText; |
||||
|
|
||||
|
class TextElement implements ITextElement |
||||
|
{ |
||||
|
/** |
||||
|
* Text. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $text; |
||||
|
|
||||
|
/** |
||||
|
* Create a new TextElement instance. |
||||
|
* |
||||
|
* @param string $text Text |
||||
|
*/ |
||||
|
public function __construct($text = '') |
||||
|
{ |
||||
|
// Initialise variables |
||||
|
$this->text = $text; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get text. |
||||
|
* |
||||
|
* @return string Text |
||||
|
*/ |
||||
|
public function getText() |
||||
|
{ |
||||
|
return $this->text; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set text. |
||||
|
* |
||||
|
* @param string $text Text |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function setText($text) |
||||
|
{ |
||||
|
$this->text = $text; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get font. |
||||
|
* |
||||
|
* @return null|\PhpOffice\PhpSpreadsheet\Style\Font |
||||
|
*/ |
||||
|
public function getFont() |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get hash code. |
||||
|
* |
||||
|
* @return string Hash code |
||||
|
*/ |
||||
|
public function getHashCode() |
||||
|
{ |
||||
|
return md5( |
||||
|
$this->text . |
||||
|
__CLASS__ |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Implement PHP __clone to create a deep clone, not just a shallow copy. |
||||
|
*/ |
||||
|
public function __clone() |
||||
|
{ |
||||
|
$vars = get_object_vars($this); |
||||
|
foreach ($vars as $key => $value) { |
||||
|
if (is_object($value)) { |
||||
|
$this->$key = clone $value; |
||||
|
} else { |
||||
|
$this->$key = $value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,214 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Calculation; |
||||
|
use PhpOffice\PhpSpreadsheet\Chart\Renderer\IRenderer; |
||||
|
use PhpOffice\PhpSpreadsheet\Collection\Memory; |
||||
|
use Psr\Http\Client\ClientInterface; |
||||
|
use Psr\Http\Message\RequestFactoryInterface; |
||||
|
use Psr\SimpleCache\CacheInterface; |
||||
|
|
||||
|
class Settings |
||||
|
{ |
||||
|
/** |
||||
|
* Class name of the chart renderer used for rendering charts |
||||
|
* eg: PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private static $chartRenderer; |
||||
|
|
||||
|
/** |
||||
|
* Default options for libxml loader. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private static $libXmlLoaderOptions; |
||||
|
|
||||
|
/** |
||||
|
* The cache implementation to be used for cell collection. |
||||
|
* |
||||
|
* @var CacheInterface |
||||
|
*/ |
||||
|
private static $cache; |
||||
|
|
||||
|
/** |
||||
|
* The HTTP client implementation to be used for network request. |
||||
|
* |
||||
|
* @var null|ClientInterface |
||||
|
*/ |
||||
|
private static $httpClient; |
||||
|
|
||||
|
/** |
||||
|
* @var null|RequestFactoryInterface |
||||
|
*/ |
||||
|
private static $requestFactory; |
||||
|
|
||||
|
/** |
||||
|
* Set the locale code to use for formula translations and any special formatting. |
||||
|
* |
||||
|
* @param string $locale The locale code to use (e.g. "fr" or "pt_br" or "en_uk") |
||||
|
* |
||||
|
* @return bool Success or failure |
||||
|
*/ |
||||
|
public static function setLocale(string $locale) |
||||
|
{ |
||||
|
return Calculation::getInstance()->setLocale($locale); |
||||
|
} |
||||
|
|
||||
|
public static function getLocale(): string |
||||
|
{ |
||||
|
return Calculation::getInstance()->getLocale(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Identify to PhpSpreadsheet the external library to use for rendering charts. |
||||
|
* |
||||
|
* @param string $rendererClassName Class name of the chart renderer |
||||
|
* eg: PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph |
||||
|
*/ |
||||
|
public static function setChartRenderer(string $rendererClassName): void |
||||
|
{ |
||||
|
if (!is_a($rendererClassName, IRenderer::class, true)) { |
||||
|
throw new Exception('Chart renderer must implement ' . IRenderer::class); |
||||
|
} |
||||
|
|
||||
|
self::$chartRenderer = $rendererClassName; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the Chart Rendering Library that PhpSpreadsheet is currently configured to use. |
||||
|
* |
||||
|
* @return null|string Class name of the chart renderer |
||||
|
* eg: PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph |
||||
|
*/ |
||||
|
public static function getChartRenderer(): ?string |
||||
|
{ |
||||
|
return self::$chartRenderer; |
||||
|
} |
||||
|
|
||||
|
public static function htmlEntityFlags(): int |
||||
|
{ |
||||
|
return \ENT_COMPAT; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set default options for libxml loader. |
||||
|
* |
||||
|
* @param int $options Default options for libxml loader |
||||
|
*/ |
||||
|
public static function setLibXmlLoaderOptions($options): void |
||||
|
{ |
||||
|
if ($options === null && defined('LIBXML_DTDLOAD')) { |
||||
|
$options = LIBXML_DTDLOAD | LIBXML_DTDATTR; |
||||
|
} |
||||
|
self::$libXmlLoaderOptions = $options; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get default options for libxml loader. |
||||
|
* Defaults to LIBXML_DTDLOAD | LIBXML_DTDATTR when not set explicitly. |
||||
|
* |
||||
|
* @return int Default options for libxml loader |
||||
|
*/ |
||||
|
public static function getLibXmlLoaderOptions(): int |
||||
|
{ |
||||
|
if (self::$libXmlLoaderOptions === null && defined('LIBXML_DTDLOAD')) { |
||||
|
self::setLibXmlLoaderOptions(LIBXML_DTDLOAD | LIBXML_DTDATTR); |
||||
|
} elseif (self::$libXmlLoaderOptions === null) { |
||||
|
self::$libXmlLoaderOptions = 0; |
||||
|
} |
||||
|
|
||||
|
return self::$libXmlLoaderOptions; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Deprecated, has no effect. |
||||
|
* |
||||
|
* @param bool $state |
||||
|
* |
||||
|
* @deprecated will be removed without replacement as it is no longer necessary on PHP 7.3.0+ |
||||
|
*/ |
||||
|
public static function setLibXmlDisableEntityLoader($state): void |
||||
|
{ |
||||
|
// noop |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Deprecated, has no effect. |
||||
|
* |
||||
|
* @return bool $state |
||||
|
* |
||||
|
* @deprecated will be removed without replacement as it is no longer necessary on PHP 7.3.0+ |
||||
|
*/ |
||||
|
public static function getLibXmlDisableEntityLoader(): bool |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Sets the implementation of cache that should be used for cell collection. |
||||
|
*/ |
||||
|
public static function setCache(CacheInterface $cache): void |
||||
|
{ |
||||
|
self::$cache = $cache; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Gets the implementation of cache that is being used for cell collection. |
||||
|
*/ |
||||
|
public static function getCache(): CacheInterface |
||||
|
{ |
||||
|
if (!self::$cache) { |
||||
|
self::$cache = new Memory(); |
||||
|
} |
||||
|
|
||||
|
return self::$cache; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the HTTP client implementation to be used for network request. |
||||
|
*/ |
||||
|
public static function setHttpClient(ClientInterface $httpClient, RequestFactoryInterface $requestFactory): void |
||||
|
{ |
||||
|
self::$httpClient = $httpClient; |
||||
|
self::$requestFactory = $requestFactory; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Unset the HTTP client configuration. |
||||
|
*/ |
||||
|
public static function unsetHttpClient(): void |
||||
|
{ |
||||
|
self::$httpClient = null; |
||||
|
self::$requestFactory = null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the HTTP client implementation to be used for network request. |
||||
|
*/ |
||||
|
public static function getHttpClient(): ClientInterface |
||||
|
{ |
||||
|
self::assertHttpClient(); |
||||
|
|
||||
|
return self::$httpClient; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the HTTP request factory. |
||||
|
*/ |
||||
|
public static function getRequestFactory(): RequestFactoryInterface |
||||
|
{ |
||||
|
self::assertHttpClient(); |
||||
|
|
||||
|
return self::$requestFactory; |
||||
|
} |
||||
|
|
||||
|
private static function assertHttpClient(): void |
||||
|
{ |
||||
|
if (!self::$httpClient || !self::$requestFactory) { |
||||
|
throw new Exception('HTTP client must be configured via Settings::setHttpClient() to be able to use WEBSERVICE function.'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer; |
||||
|
|
||||
|
class SpgrContainer |
||||
|
{ |
||||
|
/** |
||||
|
* Parent Shape Group Container. |
||||
|
* |
||||
|
* @var null|SpgrContainer |
||||
|
*/ |
||||
|
private $parent; |
||||
|
|
||||
|
/** |
||||
|
* Shape Container collection. |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
private $children = []; |
||||
|
|
||||
|
/** |
||||
|
* Set parent Shape Group Container. |
||||
|
*/ |
||||
|
public function setParent(?self $parent): void |
||||
|
{ |
||||
|
$this->parent = $parent; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the parent Shape Group Container if any. |
||||
|
*/ |
||||
|
public function getParent(): ?self |
||||
|
{ |
||||
|
return $this->parent; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Add a child. This will be either spgrContainer or spContainer. |
||||
|
* |
||||
|
* @param mixed $child |
||||
|
*/ |
||||
|
public function addChild($child): void |
||||
|
{ |
||||
|
$this->children[] = $child; |
||||
|
$child->setParent($this); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get collection of Shape Containers. |
||||
|
*/ |
||||
|
public function getChildren() |
||||
|
{ |
||||
|
return $this->children; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Recursively get all spContainers within this spgrContainer. |
||||
|
* |
||||
|
* @return SpgrContainer\SpContainer[] |
||||
|
*/ |
||||
|
public function getAllSpContainers() |
||||
|
{ |
||||
|
$allSpContainers = []; |
||||
|
|
||||
|
foreach ($this->children as $child) { |
||||
|
if ($child instanceof self) { |
||||
|
$allSpContainers = array_merge($allSpContainers, $child->getAllSpContainers()); |
||||
|
} else { |
||||
|
$allSpContainers[] = $child; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $allSpContainers; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,369 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer; |
||||
|
|
||||
|
class SpContainer |
||||
|
{ |
||||
|
/** |
||||
|
* Parent Shape Group Container. |
||||
|
* |
||||
|
* @var SpgrContainer |
||||
|
*/ |
||||
|
private $parent; |
||||
|
|
||||
|
/** |
||||
|
* Is this a group shape? |
||||
|
* |
||||
|
* @var bool |
||||
|
*/ |
||||
|
private $spgr = false; |
||||
|
|
||||
|
/** |
||||
|
* Shape type. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $spType; |
||||
|
|
||||
|
/** |
||||
|
* Shape flag. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $spFlag; |
||||
|
|
||||
|
/** |
||||
|
* Shape index (usually group shape has index 0, and the rest: 1,2,3...). |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $spId; |
||||
|
|
||||
|
/** |
||||
|
* Array of options. |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
private $OPT; |
||||
|
|
||||
|
/** |
||||
|
* Cell coordinates of upper-left corner of shape, e.g. 'A1'. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $startCoordinates; |
||||
|
|
||||
|
/** |
||||
|
* Horizontal offset of upper-left corner of shape measured in 1/1024 of column width. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $startOffsetX; |
||||
|
|
||||
|
/** |
||||
|
* Vertical offset of upper-left corner of shape measured in 1/256 of row height. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $startOffsetY; |
||||
|
|
||||
|
/** |
||||
|
* Cell coordinates of bottom-right corner of shape, e.g. 'B2'. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $endCoordinates; |
||||
|
|
||||
|
/** |
||||
|
* Horizontal offset of bottom-right corner of shape measured in 1/1024 of column width. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $endOffsetX; |
||||
|
|
||||
|
/** |
||||
|
* Vertical offset of bottom-right corner of shape measured in 1/256 of row height. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $endOffsetY; |
||||
|
|
||||
|
/** |
||||
|
* Set parent Shape Group Container. |
||||
|
* |
||||
|
* @param SpgrContainer $parent |
||||
|
*/ |
||||
|
public function setParent($parent): void |
||||
|
{ |
||||
|
$this->parent = $parent; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the parent Shape Group Container. |
||||
|
* |
||||
|
* @return SpgrContainer |
||||
|
*/ |
||||
|
public function getParent() |
||||
|
{ |
||||
|
return $this->parent; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set whether this is a group shape. |
||||
|
* |
||||
|
* @param bool $value |
||||
|
*/ |
||||
|
public function setSpgr($value): void |
||||
|
{ |
||||
|
$this->spgr = $value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get whether this is a group shape. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function getSpgr() |
||||
|
{ |
||||
|
return $this->spgr; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the shape type. |
||||
|
* |
||||
|
* @param int $value |
||||
|
*/ |
||||
|
public function setSpType($value): void |
||||
|
{ |
||||
|
$this->spType = $value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the shape type. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function getSpType() |
||||
|
{ |
||||
|
return $this->spType; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the shape flag. |
||||
|
* |
||||
|
* @param int $value |
||||
|
*/ |
||||
|
public function setSpFlag($value): void |
||||
|
{ |
||||
|
$this->spFlag = $value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the shape flag. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function getSpFlag() |
||||
|
{ |
||||
|
return $this->spFlag; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the shape index. |
||||
|
* |
||||
|
* @param int $value |
||||
|
*/ |
||||
|
public function setSpId($value): void |
||||
|
{ |
||||
|
$this->spId = $value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the shape index. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function getSpId() |
||||
|
{ |
||||
|
return $this->spId; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set an option for the Shape Group Container. |
||||
|
* |
||||
|
* @param int $property The number specifies the option |
||||
|
* @param mixed $value |
||||
|
*/ |
||||
|
public function setOPT($property, $value): void |
||||
|
{ |
||||
|
$this->OPT[$property] = $value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get an option for the Shape Group Container. |
||||
|
* |
||||
|
* @param int $property The number specifies the option |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function getOPT($property) |
||||
|
{ |
||||
|
if (isset($this->OPT[$property])) { |
||||
|
return $this->OPT[$property]; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the collection of options. |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getOPTCollection() |
||||
|
{ |
||||
|
return $this->OPT; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set cell coordinates of upper-left corner of shape. |
||||
|
* |
||||
|
* @param string $value eg: 'A1' |
||||
|
*/ |
||||
|
public function setStartCoordinates($value): void |
||||
|
{ |
||||
|
$this->startCoordinates = $value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get cell coordinates of upper-left corner of shape. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getStartCoordinates() |
||||
|
{ |
||||
|
return $this->startCoordinates; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set offset in x-direction of upper-left corner of shape measured in 1/1024 of column width. |
||||
|
* |
||||
|
* @param int $startOffsetX |
||||
|
*/ |
||||
|
public function setStartOffsetX($startOffsetX): void |
||||
|
{ |
||||
|
$this->startOffsetX = $startOffsetX; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get offset in x-direction of upper-left corner of shape measured in 1/1024 of column width. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function getStartOffsetX() |
||||
|
{ |
||||
|
return $this->startOffsetX; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set offset in y-direction of upper-left corner of shape measured in 1/256 of row height. |
||||
|
* |
||||
|
* @param int $startOffsetY |
||||
|
*/ |
||||
|
public function setStartOffsetY($startOffsetY): void |
||||
|
{ |
||||
|
$this->startOffsetY = $startOffsetY; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get offset in y-direction of upper-left corner of shape measured in 1/256 of row height. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function getStartOffsetY() |
||||
|
{ |
||||
|
return $this->startOffsetY; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set cell coordinates of bottom-right corner of shape. |
||||
|
* |
||||
|
* @param string $value eg: 'A1' |
||||
|
*/ |
||||
|
public function setEndCoordinates($value): void |
||||
|
{ |
||||
|
$this->endCoordinates = $value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get cell coordinates of bottom-right corner of shape. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getEndCoordinates() |
||||
|
{ |
||||
|
return $this->endCoordinates; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set offset in x-direction of bottom-right corner of shape measured in 1/1024 of column width. |
||||
|
* |
||||
|
* @param int $endOffsetX |
||||
|
*/ |
||||
|
public function setEndOffsetX($endOffsetX): void |
||||
|
{ |
||||
|
$this->endOffsetX = $endOffsetX; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get offset in x-direction of bottom-right corner of shape measured in 1/1024 of column width. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function getEndOffsetX() |
||||
|
{ |
||||
|
return $this->endOffsetX; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set offset in y-direction of bottom-right corner of shape measured in 1/256 of row height. |
||||
|
* |
||||
|
* @param int $endOffsetY |
||||
|
*/ |
||||
|
public function setEndOffsetY($endOffsetY): void |
||||
|
{ |
||||
|
$this->endOffsetY = $endOffsetY; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get offset in y-direction of bottom-right corner of shape measured in 1/256 of row height. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function getEndOffsetY() |
||||
|
{ |
||||
|
return $this->endOffsetY; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the nesting level of this spContainer. This is the number of spgrContainers between this spContainer and |
||||
|
* the dgContainer. A value of 1 = immediately within first spgrContainer |
||||
|
* Higher nesting level occurs if and only if spContainer is part of a shape group. |
||||
|
* |
||||
|
* @return int Nesting level |
||||
|
*/ |
||||
|
public function getNestingLevel() |
||||
|
{ |
||||
|
$nestingLevel = 0; |
||||
|
|
||||
|
$parent = $this->getParent(); |
||||
|
while ($parent instanceof SpgrContainer) { |
||||
|
++$nestingLevel; |
||||
|
$parent = $parent->getParent(); |
||||
|
} |
||||
|
|
||||
|
return $nestingLevel; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,245 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared\JAMA; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; |
||||
|
|
||||
|
/** |
||||
|
* For an m-by-n matrix A with m >= n, the QR decomposition is an m-by-n |
||||
|
* orthogonal matrix Q and an n-by-n upper triangular matrix R so that |
||||
|
* A = Q*R. |
||||
|
* |
||||
|
* The QR decompostion always exists, even if the matrix does not have |
||||
|
* full rank, so the constructor will never fail. The primary use of the |
||||
|
* QR decomposition is in the least squares solution of nonsquare systems |
||||
|
* of simultaneous linear equations. This will fail if isFullRank() |
||||
|
* returns false. |
||||
|
* |
||||
|
* @author Paul Meagher |
||||
|
* |
||||
|
* @version 1.1 |
||||
|
*/ |
||||
|
class QRDecomposition |
||||
|
{ |
||||
|
const MATRIX_RANK_EXCEPTION = 'Can only perform operation on full-rank matrix.'; |
||||
|
|
||||
|
/** |
||||
|
* Array for internal storage of decomposition. |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
private $QR = []; |
||||
|
|
||||
|
/** |
||||
|
* Row dimension. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $m; |
||||
|
|
||||
|
/** |
||||
|
* Column dimension. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $n; |
||||
|
|
||||
|
/** |
||||
|
* Array for internal storage of diagonal of R. |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
private $Rdiag = []; |
||||
|
|
||||
|
/** |
||||
|
* QR Decomposition computed by Householder reflections. |
||||
|
* |
||||
|
* @param Matrix $A Rectangular matrix |
||||
|
*/ |
||||
|
public function __construct(Matrix $A) |
||||
|
{ |
||||
|
// Initialize. |
||||
|
$this->QR = $A->getArray(); |
||||
|
$this->m = $A->getRowDimension(); |
||||
|
$this->n = $A->getColumnDimension(); |
||||
|
// Main loop. |
||||
|
for ($k = 0; $k < $this->n; ++$k) { |
||||
|
// Compute 2-norm of k-th column without under/overflow. |
||||
|
$nrm = 0.0; |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$nrm = hypo($nrm, $this->QR[$i][$k]); |
||||
|
} |
||||
|
if ($nrm != 0.0) { |
||||
|
// Form k-th Householder vector. |
||||
|
if ($this->QR[$k][$k] < 0) { |
||||
|
$nrm = -$nrm; |
||||
|
} |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$this->QR[$i][$k] /= $nrm; |
||||
|
} |
||||
|
$this->QR[$k][$k] += 1.0; |
||||
|
// Apply transformation to remaining columns. |
||||
|
for ($j = $k + 1; $j < $this->n; ++$j) { |
||||
|
$s = 0.0; |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$s += $this->QR[$i][$k] * $this->QR[$i][$j]; |
||||
|
} |
||||
|
$s = -$s / $this->QR[$k][$k]; |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$this->QR[$i][$j] += $s * $this->QR[$i][$k]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
$this->Rdiag[$k] = -$nrm; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// function __construct() |
||||
|
|
||||
|
/** |
||||
|
* Is the matrix full rank? |
||||
|
* |
||||
|
* @return bool true if R, and hence A, has full rank, else false |
||||
|
*/ |
||||
|
public function isFullRank() |
||||
|
{ |
||||
|
for ($j = 0; $j < $this->n; ++$j) { |
||||
|
if ($this->Rdiag[$j] == 0) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// function isFullRank() |
||||
|
|
||||
|
/** |
||||
|
* Return the Householder vectors. |
||||
|
* |
||||
|
* @return Matrix Lower trapezoidal matrix whose columns define the reflections |
||||
|
*/ |
||||
|
public function getH() |
||||
|
{ |
||||
|
$H = []; |
||||
|
for ($i = 0; $i < $this->m; ++$i) { |
||||
|
for ($j = 0; $j < $this->n; ++$j) { |
||||
|
if ($i >= $j) { |
||||
|
$H[$i][$j] = $this->QR[$i][$j]; |
||||
|
} else { |
||||
|
$H[$i][$j] = 0.0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new Matrix($H); |
||||
|
} |
||||
|
|
||||
|
// function getH() |
||||
|
|
||||
|
/** |
||||
|
* Return the upper triangular factor. |
||||
|
* |
||||
|
* @return Matrix upper triangular factor |
||||
|
*/ |
||||
|
public function getR() |
||||
|
{ |
||||
|
$R = []; |
||||
|
for ($i = 0; $i < $this->n; ++$i) { |
||||
|
for ($j = 0; $j < $this->n; ++$j) { |
||||
|
if ($i < $j) { |
||||
|
$R[$i][$j] = $this->QR[$i][$j]; |
||||
|
} elseif ($i == $j) { |
||||
|
$R[$i][$j] = $this->Rdiag[$i]; |
||||
|
} else { |
||||
|
$R[$i][$j] = 0.0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new Matrix($R); |
||||
|
} |
||||
|
|
||||
|
// function getR() |
||||
|
|
||||
|
/** |
||||
|
* Generate and return the (economy-sized) orthogonal factor. |
||||
|
* |
||||
|
* @return Matrix orthogonal factor |
||||
|
*/ |
||||
|
public function getQ() |
||||
|
{ |
||||
|
$Q = []; |
||||
|
for ($k = $this->n - 1; $k >= 0; --$k) { |
||||
|
for ($i = 0; $i < $this->m; ++$i) { |
||||
|
$Q[$i][$k] = 0.0; |
||||
|
} |
||||
|
$Q[$k][$k] = 1.0; |
||||
|
for ($j = $k; $j < $this->n; ++$j) { |
||||
|
if ($this->QR[$k][$k] != 0) { |
||||
|
$s = 0.0; |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$s += $this->QR[$i][$k] * $Q[$i][$j]; |
||||
|
} |
||||
|
$s = -$s / $this->QR[$k][$k]; |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$Q[$i][$j] += $s * $this->QR[$i][$k]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new Matrix($Q); |
||||
|
} |
||||
|
|
||||
|
// function getQ() |
||||
|
|
||||
|
/** |
||||
|
* Least squares solution of A*X = B. |
||||
|
* |
||||
|
* @param Matrix $B a Matrix with as many rows as A and any number of columns |
||||
|
* |
||||
|
* @return Matrix matrix that minimizes the two norm of Q*R*X-B |
||||
|
*/ |
||||
|
public function solve(Matrix $B) |
||||
|
{ |
||||
|
if ($B->getRowDimension() == $this->m) { |
||||
|
if ($this->isFullRank()) { |
||||
|
// Copy right hand side |
||||
|
$nx = $B->getColumnDimension(); |
||||
|
$X = $B->getArray(); |
||||
|
// Compute Y = transpose(Q)*B |
||||
|
for ($k = 0; $k < $this->n; ++$k) { |
||||
|
for ($j = 0; $j < $nx; ++$j) { |
||||
|
$s = 0.0; |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$s += $this->QR[$i][$k] * $X[$i][$j]; |
||||
|
} |
||||
|
$s = -$s / $this->QR[$k][$k]; |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$X[$i][$j] += $s * $this->QR[$i][$k]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// Solve R*X = Y; |
||||
|
for ($k = $this->n - 1; $k >= 0; --$k) { |
||||
|
for ($j = 0; $j < $nx; ++$j) { |
||||
|
$X[$k][$j] /= $this->Rdiag[$k]; |
||||
|
} |
||||
|
for ($i = 0; $i < $k; ++$i) { |
||||
|
for ($j = 0; $j < $nx; ++$j) { |
||||
|
$X[$i][$j] -= $X[$k][$j] * $this->QR[$i][$k]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
$X = new Matrix($X); |
||||
|
|
||||
|
return $X->getMatrix(0, $this->n - 1, 0, $nx); |
||||
|
} |
||||
|
|
||||
|
throw new CalculationException(self::MATRIX_RANK_EXCEPTION); |
||||
|
} |
||||
|
|
||||
|
throw new CalculationException(Matrix::MATRIX_DIMENSION_EXCEPTION); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,529 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared\JAMA; |
||||
|
|
||||
|
/** |
||||
|
* For an m-by-n matrix A with m >= n, the singular value decomposition is |
||||
|
* an m-by-n orthogonal matrix U, an n-by-n diagonal matrix S, and |
||||
|
* an n-by-n orthogonal matrix V so that A = U*S*V'. |
||||
|
* |
||||
|
* The singular values, sigma[$k] = S[$k][$k], are ordered so that |
||||
|
* sigma[0] >= sigma[1] >= ... >= sigma[n-1]. |
||||
|
* |
||||
|
* The singular value decompostion always exists, so the constructor will |
||||
|
* never fail. The matrix condition number and the effective numerical |
||||
|
* rank can be computed from this decomposition. |
||||
|
* |
||||
|
* @author Paul Meagher |
||||
|
* |
||||
|
* @version 1.1 |
||||
|
*/ |
||||
|
class SingularValueDecomposition |
||||
|
{ |
||||
|
/** |
||||
|
* Internal storage of U. |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
private $U = []; |
||||
|
|
||||
|
/** |
||||
|
* Internal storage of V. |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
private $V = []; |
||||
|
|
||||
|
/** |
||||
|
* Internal storage of singular values. |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
private $s = []; |
||||
|
|
||||
|
/** |
||||
|
* Row dimension. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $m; |
||||
|
|
||||
|
/** |
||||
|
* Column dimension. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $n; |
||||
|
|
||||
|
/** |
||||
|
* Construct the singular value decomposition. |
||||
|
* |
||||
|
* Derived from LINPACK code. |
||||
|
* |
||||
|
* @param mixed $Arg Rectangular matrix |
||||
|
*/ |
||||
|
public function __construct($Arg) |
||||
|
{ |
||||
|
// Initialize. |
||||
|
$A = $Arg->getArray(); |
||||
|
$this->m = $Arg->getRowDimension(); |
||||
|
$this->n = $Arg->getColumnDimension(); |
||||
|
$nu = min($this->m, $this->n); |
||||
|
$e = []; |
||||
|
$work = []; |
||||
|
$wantu = true; |
||||
|
$wantv = true; |
||||
|
$nct = min($this->m - 1, $this->n); |
||||
|
$nrt = max(0, min($this->n - 2, $this->m)); |
||||
|
|
||||
|
// Reduce A to bidiagonal form, storing the diagonal elements |
||||
|
// in s and the super-diagonal elements in e. |
||||
|
$kMax = max($nct, $nrt); |
||||
|
for ($k = 0; $k < $kMax; ++$k) { |
||||
|
if ($k < $nct) { |
||||
|
// Compute the transformation for the k-th column and |
||||
|
// place the k-th diagonal in s[$k]. |
||||
|
// Compute 2-norm of k-th column without under/overflow. |
||||
|
$this->s[$k] = 0; |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$this->s[$k] = hypo($this->s[$k], $A[$i][$k]); |
||||
|
} |
||||
|
if ($this->s[$k] != 0.0) { |
||||
|
if ($A[$k][$k] < 0.0) { |
||||
|
$this->s[$k] = -$this->s[$k]; |
||||
|
} |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$A[$i][$k] /= $this->s[$k]; |
||||
|
} |
||||
|
$A[$k][$k] += 1.0; |
||||
|
} |
||||
|
$this->s[$k] = -$this->s[$k]; |
||||
|
} |
||||
|
|
||||
|
for ($j = $k + 1; $j < $this->n; ++$j) { |
||||
|
if (($k < $nct) & ($this->s[$k] != 0.0)) { |
||||
|
// Apply the transformation. |
||||
|
$t = 0; |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$t += $A[$i][$k] * $A[$i][$j]; |
||||
|
} |
||||
|
$t = -$t / $A[$k][$k]; |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$A[$i][$j] += $t * $A[$i][$k]; |
||||
|
} |
||||
|
// Place the k-th row of A into e for the |
||||
|
// subsequent calculation of the row transformation. |
||||
|
$e[$j] = $A[$k][$j]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($wantu && ($k < $nct)) { |
||||
|
// Place the transformation in U for subsequent back |
||||
|
// multiplication. |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$this->U[$i][$k] = $A[$i][$k]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($k < $nrt) { |
||||
|
// Compute the k-th row transformation and place the |
||||
|
// k-th super-diagonal in e[$k]. |
||||
|
// Compute 2-norm without under/overflow. |
||||
|
$e[$k] = 0; |
||||
|
for ($i = $k + 1; $i < $this->n; ++$i) { |
||||
|
$e[$k] = hypo($e[$k], $e[$i]); |
||||
|
} |
||||
|
if ($e[$k] != 0.0) { |
||||
|
if ($e[$k + 1] < 0.0) { |
||||
|
$e[$k] = -$e[$k]; |
||||
|
} |
||||
|
for ($i = $k + 1; $i < $this->n; ++$i) { |
||||
|
$e[$i] /= $e[$k]; |
||||
|
} |
||||
|
$e[$k + 1] += 1.0; |
||||
|
} |
||||
|
$e[$k] = -$e[$k]; |
||||
|
if (($k + 1 < $this->m) && ($e[$k] != 0.0)) { |
||||
|
// Apply the transformation. |
||||
|
for ($i = $k + 1; $i < $this->m; ++$i) { |
||||
|
$work[$i] = 0.0; |
||||
|
} |
||||
|
for ($j = $k + 1; $j < $this->n; ++$j) { |
||||
|
for ($i = $k + 1; $i < $this->m; ++$i) { |
||||
|
$work[$i] += $e[$j] * $A[$i][$j]; |
||||
|
} |
||||
|
} |
||||
|
for ($j = $k + 1; $j < $this->n; ++$j) { |
||||
|
$t = -$e[$j] / $e[$k + 1]; |
||||
|
for ($i = $k + 1; $i < $this->m; ++$i) { |
||||
|
$A[$i][$j] += $t * $work[$i]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if ($wantv) { |
||||
|
// Place the transformation in V for subsequent |
||||
|
// back multiplication. |
||||
|
for ($i = $k + 1; $i < $this->n; ++$i) { |
||||
|
$this->V[$i][$k] = $e[$i]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Set up the final bidiagonal matrix or order p. |
||||
|
$p = min($this->n, $this->m + 1); |
||||
|
if ($nct < $this->n) { |
||||
|
$this->s[$nct] = $A[$nct][$nct]; |
||||
|
} |
||||
|
if ($this->m < $p) { |
||||
|
$this->s[$p - 1] = 0.0; |
||||
|
} |
||||
|
if ($nrt + 1 < $p) { |
||||
|
$e[$nrt] = $A[$nrt][$p - 1]; |
||||
|
} |
||||
|
$e[$p - 1] = 0.0; |
||||
|
// If required, generate U. |
||||
|
if ($wantu) { |
||||
|
for ($j = $nct; $j < $nu; ++$j) { |
||||
|
for ($i = 0; $i < $this->m; ++$i) { |
||||
|
$this->U[$i][$j] = 0.0; |
||||
|
} |
||||
|
$this->U[$j][$j] = 1.0; |
||||
|
} |
||||
|
for ($k = $nct - 1; $k >= 0; --$k) { |
||||
|
if ($this->s[$k] != 0.0) { |
||||
|
for ($j = $k + 1; $j < $nu; ++$j) { |
||||
|
$t = 0; |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$t += $this->U[$i][$k] * $this->U[$i][$j]; |
||||
|
} |
||||
|
$t = -$t / $this->U[$k][$k]; |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$this->U[$i][$j] += $t * $this->U[$i][$k]; |
||||
|
} |
||||
|
} |
||||
|
for ($i = $k; $i < $this->m; ++$i) { |
||||
|
$this->U[$i][$k] = -$this->U[$i][$k]; |
||||
|
} |
||||
|
$this->U[$k][$k] = 1.0 + $this->U[$k][$k]; |
||||
|
for ($i = 0; $i < $k - 1; ++$i) { |
||||
|
$this->U[$i][$k] = 0.0; |
||||
|
} |
||||
|
} else { |
||||
|
for ($i = 0; $i < $this->m; ++$i) { |
||||
|
$this->U[$i][$k] = 0.0; |
||||
|
} |
||||
|
$this->U[$k][$k] = 1.0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// If required, generate V. |
||||
|
if ($wantv) { |
||||
|
for ($k = $this->n - 1; $k >= 0; --$k) { |
||||
|
if (($k < $nrt) && ($e[$k] != 0.0)) { |
||||
|
for ($j = $k + 1; $j < $nu; ++$j) { |
||||
|
$t = 0; |
||||
|
for ($i = $k + 1; $i < $this->n; ++$i) { |
||||
|
$t += $this->V[$i][$k] * $this->V[$i][$j]; |
||||
|
} |
||||
|
$t = -$t / $this->V[$k + 1][$k]; |
||||
|
for ($i = $k + 1; $i < $this->n; ++$i) { |
||||
|
$this->V[$i][$j] += $t * $this->V[$i][$k]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
for ($i = 0; $i < $this->n; ++$i) { |
||||
|
$this->V[$i][$k] = 0.0; |
||||
|
} |
||||
|
$this->V[$k][$k] = 1.0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Main iteration loop for the singular values. |
||||
|
$pp = $p - 1; |
||||
|
$iter = 0; |
||||
|
$eps = 2.0 ** (-52.0); |
||||
|
|
||||
|
while ($p > 0) { |
||||
|
// Here is where a test for too many iterations would go. |
||||
|
// This section of the program inspects for negligible |
||||
|
// elements in the s and e arrays. On completion the |
||||
|
// variables kase and k are set as follows: |
||||
|
// kase = 1 if s(p) and e[k-1] are negligible and k<p |
||||
|
// kase = 2 if s(k) is negligible and k<p |
||||
|
// kase = 3 if e[k-1] is negligible, k<p, and |
||||
|
// s(k), ..., s(p) are not negligible (qr step). |
||||
|
// kase = 4 if e(p-1) is negligible (convergence). |
||||
|
for ($k = $p - 2; $k >= -1; --$k) { |
||||
|
if ($k == -1) { |
||||
|
break; |
||||
|
} |
||||
|
if (abs($e[$k]) <= $eps * (abs($this->s[$k]) + abs($this->s[$k + 1]))) { |
||||
|
$e[$k] = 0.0; |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if ($k == $p - 2) { |
||||
|
$kase = 4; |
||||
|
} else { |
||||
|
for ($ks = $p - 1; $ks >= $k; --$ks) { |
||||
|
if ($ks == $k) { |
||||
|
break; |
||||
|
} |
||||
|
$t = ($ks != $p ? abs($e[$ks]) : 0.) + ($ks != $k + 1 ? abs($e[$ks - 1]) : 0.); |
||||
|
if (abs($this->s[$ks]) <= $eps * $t) { |
||||
|
$this->s[$ks] = 0.0; |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if ($ks == $k) { |
||||
|
$kase = 3; |
||||
|
} elseif ($ks == $p - 1) { |
||||
|
$kase = 1; |
||||
|
} else { |
||||
|
$kase = 2; |
||||
|
$k = $ks; |
||||
|
} |
||||
|
} |
||||
|
++$k; |
||||
|
|
||||
|
// Perform the task indicated by kase. |
||||
|
switch ($kase) { |
||||
|
// Deflate negligible s(p). |
||||
|
case 1: |
||||
|
$f = $e[$p - 2]; |
||||
|
$e[$p - 2] = 0.0; |
||||
|
for ($j = $p - 2; $j >= $k; --$j) { |
||||
|
$t = hypo($this->s[$j], $f); |
||||
|
$cs = $this->s[$j] / $t; |
||||
|
$sn = $f / $t; |
||||
|
$this->s[$j] = $t; |
||||
|
if ($j != $k) { |
||||
|
$f = -$sn * $e[$j - 1]; |
||||
|
$e[$j - 1] = $cs * $e[$j - 1]; |
||||
|
} |
||||
|
if ($wantv) { |
||||
|
for ($i = 0; $i < $this->n; ++$i) { |
||||
|
$t = $cs * $this->V[$i][$j] + $sn * $this->V[$i][$p - 1]; |
||||
|
$this->V[$i][$p - 1] = -$sn * $this->V[$i][$j] + $cs * $this->V[$i][$p - 1]; |
||||
|
$this->V[$i][$j] = $t; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
// Split at negligible s(k). |
||||
|
case 2: |
||||
|
$f = $e[$k - 1]; |
||||
|
$e[$k - 1] = 0.0; |
||||
|
for ($j = $k; $j < $p; ++$j) { |
||||
|
$t = hypo($this->s[$j], $f); |
||||
|
$cs = $this->s[$j] / $t; |
||||
|
$sn = $f / $t; |
||||
|
$this->s[$j] = $t; |
||||
|
$f = -$sn * $e[$j]; |
||||
|
$e[$j] = $cs * $e[$j]; |
||||
|
if ($wantu) { |
||||
|
for ($i = 0; $i < $this->m; ++$i) { |
||||
|
$t = $cs * $this->U[$i][$j] + $sn * $this->U[$i][$k - 1]; |
||||
|
$this->U[$i][$k - 1] = -$sn * $this->U[$i][$j] + $cs * $this->U[$i][$k - 1]; |
||||
|
$this->U[$i][$j] = $t; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
// Perform one qr step. |
||||
|
case 3: |
||||
|
// Calculate the shift. |
||||
|
$scale = max(max(max(max(abs($this->s[$p - 1]), abs($this->s[$p - 2])), abs($e[$p - 2])), abs($this->s[$k])), abs($e[$k])); |
||||
|
$sp = $this->s[$p - 1] / $scale; |
||||
|
$spm1 = $this->s[$p - 2] / $scale; |
||||
|
$epm1 = $e[$p - 2] / $scale; |
||||
|
$sk = $this->s[$k] / $scale; |
||||
|
$ek = $e[$k] / $scale; |
||||
|
$b = (($spm1 + $sp) * ($spm1 - $sp) + $epm1 * $epm1) / 2.0; |
||||
|
$c = ($sp * $epm1) * ($sp * $epm1); |
||||
|
$shift = 0.0; |
||||
|
if (($b != 0.0) || ($c != 0.0)) { |
||||
|
$shift = sqrt($b * $b + $c); |
||||
|
if ($b < 0.0) { |
||||
|
$shift = -$shift; |
||||
|
} |
||||
|
$shift = $c / ($b + $shift); |
||||
|
} |
||||
|
$f = ($sk + $sp) * ($sk - $sp) + $shift; |
||||
|
$g = $sk * $ek; |
||||
|
// Chase zeros. |
||||
|
for ($j = $k; $j < $p - 1; ++$j) { |
||||
|
$t = hypo($f, $g); |
||||
|
$cs = $f / $t; |
||||
|
$sn = $g / $t; |
||||
|
if ($j != $k) { |
||||
|
$e[$j - 1] = $t; |
||||
|
} |
||||
|
$f = $cs * $this->s[$j] + $sn * $e[$j]; |
||||
|
$e[$j] = $cs * $e[$j] - $sn * $this->s[$j]; |
||||
|
$g = $sn * $this->s[$j + 1]; |
||||
|
$this->s[$j + 1] = $cs * $this->s[$j + 1]; |
||||
|
if ($wantv) { |
||||
|
for ($i = 0; $i < $this->n; ++$i) { |
||||
|
$t = $cs * $this->V[$i][$j] + $sn * $this->V[$i][$j + 1]; |
||||
|
$this->V[$i][$j + 1] = -$sn * $this->V[$i][$j] + $cs * $this->V[$i][$j + 1]; |
||||
|
$this->V[$i][$j] = $t; |
||||
|
} |
||||
|
} |
||||
|
$t = hypo($f, $g); |
||||
|
$cs = $f / $t; |
||||
|
$sn = $g / $t; |
||||
|
$this->s[$j] = $t; |
||||
|
$f = $cs * $e[$j] + $sn * $this->s[$j + 1]; |
||||
|
$this->s[$j + 1] = -$sn * $e[$j] + $cs * $this->s[$j + 1]; |
||||
|
$g = $sn * $e[$j + 1]; |
||||
|
$e[$j + 1] = $cs * $e[$j + 1]; |
||||
|
if ($wantu && ($j < $this->m - 1)) { |
||||
|
for ($i = 0; $i < $this->m; ++$i) { |
||||
|
$t = $cs * $this->U[$i][$j] + $sn * $this->U[$i][$j + 1]; |
||||
|
$this->U[$i][$j + 1] = -$sn * $this->U[$i][$j] + $cs * $this->U[$i][$j + 1]; |
||||
|
$this->U[$i][$j] = $t; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
$e[$p - 2] = $f; |
||||
|
$iter = $iter + 1; |
||||
|
|
||||
|
break; |
||||
|
// Convergence. |
||||
|
case 4: |
||||
|
// Make the singular values positive. |
||||
|
if ($this->s[$k] <= 0.0) { |
||||
|
$this->s[$k] = ($this->s[$k] < 0.0 ? -$this->s[$k] : 0.0); |
||||
|
if ($wantv) { |
||||
|
for ($i = 0; $i <= $pp; ++$i) { |
||||
|
$this->V[$i][$k] = -$this->V[$i][$k]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// Order the singular values. |
||||
|
while ($k < $pp) { |
||||
|
if ($this->s[$k] >= $this->s[$k + 1]) { |
||||
|
break; |
||||
|
} |
||||
|
$t = $this->s[$k]; |
||||
|
$this->s[$k] = $this->s[$k + 1]; |
||||
|
$this->s[$k + 1] = $t; |
||||
|
if ($wantv && ($k < $this->n - 1)) { |
||||
|
for ($i = 0; $i < $this->n; ++$i) { |
||||
|
$t = $this->V[$i][$k + 1]; |
||||
|
$this->V[$i][$k + 1] = $this->V[$i][$k]; |
||||
|
$this->V[$i][$k] = $t; |
||||
|
} |
||||
|
} |
||||
|
if ($wantu && ($k < $this->m - 1)) { |
||||
|
for ($i = 0; $i < $this->m; ++$i) { |
||||
|
$t = $this->U[$i][$k + 1]; |
||||
|
$this->U[$i][$k + 1] = $this->U[$i][$k]; |
||||
|
$this->U[$i][$k] = $t; |
||||
|
} |
||||
|
} |
||||
|
++$k; |
||||
|
} |
||||
|
$iter = 0; |
||||
|
--$p; |
||||
|
|
||||
|
break; |
||||
|
} // end switch |
||||
|
} // end while |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the left singular vectors. |
||||
|
* |
||||
|
* @return Matrix U |
||||
|
*/ |
||||
|
public function getU() |
||||
|
{ |
||||
|
return new Matrix($this->U, $this->m, min($this->m + 1, $this->n)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the right singular vectors. |
||||
|
* |
||||
|
* @return Matrix V |
||||
|
*/ |
||||
|
public function getV() |
||||
|
{ |
||||
|
return new Matrix($this->V); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the one-dimensional array of singular values. |
||||
|
* |
||||
|
* @return array diagonal of S |
||||
|
*/ |
||||
|
public function getSingularValues() |
||||
|
{ |
||||
|
return $this->s; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the diagonal matrix of singular values. |
||||
|
* |
||||
|
* @return Matrix S |
||||
|
*/ |
||||
|
public function getS() |
||||
|
{ |
||||
|
$S = []; |
||||
|
for ($i = 0; $i < $this->n; ++$i) { |
||||
|
for ($j = 0; $j < $this->n; ++$j) { |
||||
|
$S[$i][$j] = 0.0; |
||||
|
} |
||||
|
$S[$i][$i] = $this->s[$i]; |
||||
|
} |
||||
|
|
||||
|
return new Matrix($S); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Two norm. |
||||
|
* |
||||
|
* @return float max(S) |
||||
|
*/ |
||||
|
public function norm2() |
||||
|
{ |
||||
|
return $this->s[0]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Two norm condition number. |
||||
|
* |
||||
|
* @return float max(S)/min(S) |
||||
|
*/ |
||||
|
public function cond() |
||||
|
{ |
||||
|
return $this->s[0] / $this->s[min($this->m, $this->n) - 1]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Effective numerical matrix rank. |
||||
|
* |
||||
|
* @return int Number of nonnegligible singular values |
||||
|
*/ |
||||
|
public function rank() |
||||
|
{ |
||||
|
$eps = 2.0 ** (-52.0); |
||||
|
$tol = max($this->m, $this->n) * $this->s[0] * $eps; |
||||
|
$r = 0; |
||||
|
$iMax = count($this->s); |
||||
|
for ($i = 0; $i < $iMax; ++$i) { |
||||
|
if ($this->s[$i] > $tol) { |
||||
|
++$r; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $r; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,237 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared\OLE; |
||||
|
|
||||
|
// vim: set expandtab tabstop=4 shiftwidth=4: |
||||
|
// +----------------------------------------------------------------------+ |
||||
|
// | PHP Version 4 | |
||||
|
// +----------------------------------------------------------------------+ |
||||
|
// | Copyright (c) 1997-2002 The PHP Group | |
||||
|
// +----------------------------------------------------------------------+ |
||||
|
// | This source file is subject to version 2.02 of the PHP license, | |
||||
|
// | that is bundled with this package in the file LICENSE, and is | |
||||
|
// | available at through the world-wide-web at | |
||||
|
// | http://www.php.net/license/2_02.txt. | |
||||
|
// | If you did not receive a copy of the PHP license and are unable to | |
||||
|
// | obtain it through the world-wide-web, please send a note to | |
||||
|
// | license@php.net so we can mail you a copy immediately. | |
||||
|
// +----------------------------------------------------------------------+ |
||||
|
// | Author: Xavier Noguer <xnoguer@php.net> | |
||||
|
// | Based on OLE::Storage_Lite by Kawai, Takanori | |
||||
|
// +----------------------------------------------------------------------+ |
||||
|
// |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\OLE; |
||||
|
|
||||
|
/** |
||||
|
* Class for creating PPS's for OLE containers. |
||||
|
* |
||||
|
* @author Xavier Noguer <xnoguer@php.net> |
||||
|
*/ |
||||
|
class PPS |
||||
|
{ |
||||
|
/** |
||||
|
* The PPS index. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
public $No; |
||||
|
|
||||
|
/** |
||||
|
* The PPS name (in Unicode). |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
public $Name; |
||||
|
|
||||
|
/** |
||||
|
* The PPS type. Dir, Root or File. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
public $Type; |
||||
|
|
||||
|
/** |
||||
|
* The index of the previous PPS. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
public $PrevPps; |
||||
|
|
||||
|
/** |
||||
|
* The index of the next PPS. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
public $NextPps; |
||||
|
|
||||
|
/** |
||||
|
* The index of it's first child if this is a Dir or Root PPS. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
public $DirPps; |
||||
|
|
||||
|
/** |
||||
|
* A timestamp. |
||||
|
* |
||||
|
* @var float|int |
||||
|
*/ |
||||
|
public $Time1st; |
||||
|
|
||||
|
/** |
||||
|
* A timestamp. |
||||
|
* |
||||
|
* @var float|int |
||||
|
*/ |
||||
|
public $Time2nd; |
||||
|
|
||||
|
/** |
||||
|
* Starting block (small or big) for this PPS's data inside the container. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
public $startBlock; |
||||
|
|
||||
|
/** |
||||
|
* The size of the PPS's data (in bytes). |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
public $Size; |
||||
|
|
||||
|
/** |
||||
|
* The PPS's data (only used if it's not using a temporary file). |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
public $_data; |
||||
|
|
||||
|
/** |
||||
|
* Array of child PPS's (only used by Root and Dir PPS's). |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
public $children = []; |
||||
|
|
||||
|
/** |
||||
|
* Pointer to OLE container. |
||||
|
* |
||||
|
* @var OLE |
||||
|
*/ |
||||
|
public $ole; |
||||
|
|
||||
|
/** |
||||
|
* The constructor. |
||||
|
* |
||||
|
* @param int $No The PPS index |
||||
|
* @param string $name The PPS name |
||||
|
* @param int $type The PPS type. Dir, Root or File |
||||
|
* @param int $prev The index of the previous PPS |
||||
|
* @param int $next The index of the next PPS |
||||
|
* @param int $dir The index of it's first child if this is a Dir or Root PPS |
||||
|
* @param null|float|int $time_1st A timestamp |
||||
|
* @param null|float|int $time_2nd A timestamp |
||||
|
* @param string $data The (usually binary) source data of the PPS |
||||
|
* @param array $children Array containing children PPS for this PPS |
||||
|
*/ |
||||
|
public function __construct($No, $name, $type, $prev, $next, $dir, $time_1st, $time_2nd, $data, $children) |
||||
|
{ |
||||
|
$this->No = $No; |
||||
|
$this->Name = $name; |
||||
|
$this->Type = $type; |
||||
|
$this->PrevPps = $prev; |
||||
|
$this->NextPps = $next; |
||||
|
$this->DirPps = $dir; |
||||
|
$this->Time1st = $time_1st ?? 0; |
||||
|
$this->Time2nd = $time_2nd ?? 0; |
||||
|
$this->_data = $data; |
||||
|
$this->children = $children; |
||||
|
if ($data != '') { |
||||
|
$this->Size = strlen($data); |
||||
|
} else { |
||||
|
$this->Size = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the amount of data saved for this PPS. |
||||
|
* |
||||
|
* @return int The amount of data (in bytes) |
||||
|
*/ |
||||
|
public function getDataLen() |
||||
|
{ |
||||
|
if (!isset($this->_data)) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
return strlen($this->_data); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns a string with the PPS's WK (What is a WK?). |
||||
|
* |
||||
|
* @return string The binary string |
||||
|
*/ |
||||
|
public function getPpsWk() |
||||
|
{ |
||||
|
$ret = str_pad($this->Name, 64, "\x00"); |
||||
|
|
||||
|
$ret .= pack('v', strlen($this->Name) + 2) // 66 |
||||
|
. pack('c', $this->Type) // 67 |
||||
|
. pack('c', 0x00) //UK // 68 |
||||
|
. pack('V', $this->PrevPps) //Prev // 72 |
||||
|
. pack('V', $this->NextPps) //Next // 76 |
||||
|
. pack('V', $this->DirPps) //Dir // 80 |
||||
|
. "\x00\x09\x02\x00" // 84 |
||||
|
. "\x00\x00\x00\x00" // 88 |
||||
|
. "\xc0\x00\x00\x00" // 92 |
||||
|
. "\x00\x00\x00\x46" // 96 // Seems to be ok only for Root |
||||
|
. "\x00\x00\x00\x00" // 100 |
||||
|
. OLE::localDateToOLE($this->Time1st) // 108 |
||||
|
. OLE::localDateToOLE($this->Time2nd) // 116 |
||||
|
. pack('V', $this->startBlock ?? 0) // 120 |
||||
|
. pack('V', $this->Size) // 124 |
||||
|
. pack('V', 0); // 128 |
||||
|
|
||||
|
return $ret; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Updates index and pointers to previous, next and children PPS's for this |
||||
|
* PPS. I don't think it'll work with Dir PPS's. |
||||
|
* |
||||
|
* @param array $raList Reference to the array of PPS's for the whole OLE |
||||
|
* container |
||||
|
* @param mixed $to_save |
||||
|
* @param mixed $depth |
||||
|
* |
||||
|
* @return int The index for this PPS |
||||
|
*/ |
||||
|
public static function savePpsSetPnt(&$raList, $to_save, $depth = 0) |
||||
|
{ |
||||
|
if (!is_array($to_save) || (empty($to_save))) { |
||||
|
return 0xFFFFFFFF; |
||||
|
} elseif (count($to_save) == 1) { |
||||
|
$cnt = count($raList); |
||||
|
// If the first entry, it's the root... Don't clone it! |
||||
|
$raList[$cnt] = ($depth == 0) ? $to_save[0] : clone $to_save[0]; |
||||
|
$raList[$cnt]->No = $cnt; |
||||
|
$raList[$cnt]->PrevPps = 0xFFFFFFFF; |
||||
|
$raList[$cnt]->NextPps = 0xFFFFFFFF; |
||||
|
$raList[$cnt]->DirPps = self::savePpsSetPnt($raList, @$raList[$cnt]->children, $depth++); |
||||
|
} else { |
||||
|
$iPos = floor(count($to_save) / 2); |
||||
|
$aPrev = array_slice($to_save, 0, $iPos); |
||||
|
$aNext = array_slice($to_save, $iPos + 1); |
||||
|
$cnt = count($raList); |
||||
|
// If the first entry, it's the root... Don't clone it! |
||||
|
$raList[$cnt] = ($depth == 0) ? $to_save[$iPos] : clone $to_save[$iPos]; |
||||
|
$raList[$cnt]->No = $cnt; |
||||
|
$raList[$cnt]->PrevPps = self::savePpsSetPnt($raList, $aPrev, $depth++); |
||||
|
$raList[$cnt]->NextPps = self::savePpsSetPnt($raList, $aNext, $depth++); |
||||
|
$raList[$cnt]->DirPps = self::savePpsSetPnt($raList, @$raList[$cnt]->children, $depth++); |
||||
|
} |
||||
|
|
||||
|
return $cnt; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,426 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared\OLE\PPS; |
||||
|
|
||||
|
// vim: set expandtab tabstop=4 shiftwidth=4: |
||||
|
// +----------------------------------------------------------------------+ |
||||
|
// | PHP Version 4 | |
||||
|
// +----------------------------------------------------------------------+ |
||||
|
// | Copyright (c) 1997-2002 The PHP Group | |
||||
|
// +----------------------------------------------------------------------+ |
||||
|
// | This source file is subject to version 2.02 of the PHP license, | |
||||
|
// | that is bundled with this package in the file LICENSE, and is | |
||||
|
// | available at through the world-wide-web at | |
||||
|
// | http://www.php.net/license/2_02.txt. | |
||||
|
// | If you did not receive a copy of the PHP license and are unable to | |
||||
|
// | obtain it through the world-wide-web, please send a note to | |
||||
|
// | license@php.net so we can mail you a copy immediately. | |
||||
|
// +----------------------------------------------------------------------+ |
||||
|
// | Author: Xavier Noguer <xnoguer@php.net> | |
||||
|
// | Based on OLE::Storage_Lite by Kawai, Takanori | |
||||
|
// +----------------------------------------------------------------------+ |
||||
|
// |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\OLE; |
||||
|
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS; |
||||
|
|
||||
|
/** |
||||
|
* Class for creating Root PPS's for OLE containers. |
||||
|
* |
||||
|
* @author Xavier Noguer <xnoguer@php.net> |
||||
|
*/ |
||||
|
class Root extends PPS |
||||
|
{ |
||||
|
/** |
||||
|
* @var resource |
||||
|
*/ |
||||
|
private $fileHandle; |
||||
|
|
||||
|
/** |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $smallBlockSize; |
||||
|
|
||||
|
/** |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $bigBlockSize; |
||||
|
|
||||
|
/** |
||||
|
* @param null|float|int $time_1st A timestamp |
||||
|
* @param null|float|int $time_2nd A timestamp |
||||
|
* @param File[] $raChild |
||||
|
*/ |
||||
|
public function __construct($time_1st, $time_2nd, $raChild) |
||||
|
{ |
||||
|
parent::__construct(null, OLE::ascToUcs('Root Entry'), OLE::OLE_PPS_TYPE_ROOT, null, null, null, $time_1st, $time_2nd, null, $raChild); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Method for saving the whole OLE container (including files). |
||||
|
* In fact, if called with an empty argument (or '-'), it saves to a |
||||
|
* temporary file and then outputs it's contents to stdout. |
||||
|
* If a resource pointer to a stream created by fopen() is passed |
||||
|
* it will be used, but you have to close such stream by yourself. |
||||
|
* |
||||
|
* @param resource $fileHandle the name of the file or stream where to save the OLE container |
||||
|
* |
||||
|
* @return bool true on success |
||||
|
*/ |
||||
|
public function save($fileHandle) |
||||
|
{ |
||||
|
$this->fileHandle = $fileHandle; |
||||
|
|
||||
|
// Initial Setting for saving |
||||
|
$this->bigBlockSize = (int) (2 ** ( |
||||
|
(isset($this->bigBlockSize)) ? self::adjust2($this->bigBlockSize) : 9 |
||||
|
)); |
||||
|
$this->smallBlockSize = (int) (2 ** ( |
||||
|
(isset($this->smallBlockSize)) ? self::adjust2($this->smallBlockSize) : 6 |
||||
|
)); |
||||
|
|
||||
|
// Make an array of PPS's (for Save) |
||||
|
$aList = []; |
||||
|
PPS::savePpsSetPnt($aList, [$this]); |
||||
|
// calculate values for header |
||||
|
[$iSBDcnt, $iBBcnt, $iPPScnt] = $this->calcSize($aList); //, $rhInfo); |
||||
|
// Save Header |
||||
|
$this->saveHeader($iSBDcnt, $iBBcnt, $iPPScnt); |
||||
|
|
||||
|
// Make Small Data string (write SBD) |
||||
|
$this->_data = $this->makeSmallData($aList); |
||||
|
|
||||
|
// Write BB |
||||
|
$this->saveBigData($iSBDcnt, $aList); |
||||
|
// Write PPS |
||||
|
$this->savePps($aList); |
||||
|
// Write Big Block Depot and BDList and Adding Header informations |
||||
|
$this->saveBbd($iSBDcnt, $iBBcnt, $iPPScnt); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Calculate some numbers. |
||||
|
* |
||||
|
* @param array $raList Reference to an array of PPS's |
||||
|
* |
||||
|
* @return float[] The array of numbers |
||||
|
*/ |
||||
|
private function calcSize(&$raList) |
||||
|
{ |
||||
|
// Calculate Basic Setting |
||||
|
[$iSBDcnt, $iBBcnt, $iPPScnt] = [0, 0, 0]; |
||||
|
$iSmallLen = 0; |
||||
|
$iSBcnt = 0; |
||||
|
$iCount = count($raList); |
||||
|
for ($i = 0; $i < $iCount; ++$i) { |
||||
|
if ($raList[$i]->Type == OLE::OLE_PPS_TYPE_FILE) { |
||||
|
$raList[$i]->Size = $raList[$i]->getDataLen(); |
||||
|
if ($raList[$i]->Size < OLE::OLE_DATA_SIZE_SMALL) { |
||||
|
$iSBcnt += floor($raList[$i]->Size / $this->smallBlockSize) |
||||
|
+ (($raList[$i]->Size % $this->smallBlockSize) ? 1 : 0); |
||||
|
} else { |
||||
|
$iBBcnt += (floor($raList[$i]->Size / $this->bigBlockSize) + |
||||
|
(($raList[$i]->Size % $this->bigBlockSize) ? 1 : 0)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
$iSmallLen = $iSBcnt * $this->smallBlockSize; |
||||
|
$iSlCnt = floor($this->bigBlockSize / OLE::OLE_LONG_INT_SIZE); |
||||
|
$iSBDcnt = floor($iSBcnt / $iSlCnt) + (($iSBcnt % $iSlCnt) ? 1 : 0); |
||||
|
$iBBcnt += (floor($iSmallLen / $this->bigBlockSize) + |
||||
|
(($iSmallLen % $this->bigBlockSize) ? 1 : 0)); |
||||
|
$iCnt = count($raList); |
||||
|
$iBdCnt = $this->bigBlockSize / OLE::OLE_PPS_SIZE; |
||||
|
$iPPScnt = (floor($iCnt / $iBdCnt) + (($iCnt % $iBdCnt) ? 1 : 0)); |
||||
|
|
||||
|
return [$iSBDcnt, $iBBcnt, $iPPScnt]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Helper function for caculating a magic value for block sizes. |
||||
|
* |
||||
|
* @param int $i2 The argument |
||||
|
* |
||||
|
* @return float |
||||
|
* |
||||
|
* @see save() |
||||
|
*/ |
||||
|
private static function adjust2($i2) |
||||
|
{ |
||||
|
$iWk = log($i2) / log(2); |
||||
|
|
||||
|
return ($iWk > floor($iWk)) ? floor($iWk) + 1 : $iWk; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Save OLE header. |
||||
|
* |
||||
|
* @param int $iSBDcnt |
||||
|
* @param int $iBBcnt |
||||
|
* @param int $iPPScnt |
||||
|
*/ |
||||
|
private function saveHeader($iSBDcnt, $iBBcnt, $iPPScnt): void |
||||
|
{ |
||||
|
$FILE = $this->fileHandle; |
||||
|
|
||||
|
// Calculate Basic Setting |
||||
|
$iBlCnt = $this->bigBlockSize / OLE::OLE_LONG_INT_SIZE; |
||||
|
$i1stBdL = ($this->bigBlockSize - 0x4C) / OLE::OLE_LONG_INT_SIZE; |
||||
|
|
||||
|
$iBdExL = 0; |
||||
|
$iAll = $iBBcnt + $iPPScnt + $iSBDcnt; |
||||
|
$iAllW = $iAll; |
||||
|
$iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0); |
||||
|
$iBdCnt = floor(($iAll + $iBdCntW) / $iBlCnt) + ((($iAllW + $iBdCntW) % $iBlCnt) ? 1 : 0); |
||||
|
|
||||
|
// Calculate BD count |
||||
|
if ($iBdCnt > $i1stBdL) { |
||||
|
while (1) { |
||||
|
++$iBdExL; |
||||
|
++$iAllW; |
||||
|
$iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0); |
||||
|
$iBdCnt = floor(($iAllW + $iBdCntW) / $iBlCnt) + ((($iAllW + $iBdCntW) % $iBlCnt) ? 1 : 0); |
||||
|
if ($iBdCnt <= ($iBdExL * $iBlCnt + $i1stBdL)) { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Save Header |
||||
|
fwrite( |
||||
|
$FILE, |
||||
|
"\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" |
||||
|
. "\x00\x00\x00\x00" |
||||
|
. "\x00\x00\x00\x00" |
||||
|
. "\x00\x00\x00\x00" |
||||
|
. "\x00\x00\x00\x00" |
||||
|
. pack('v', 0x3b) |
||||
|
. pack('v', 0x03) |
||||
|
. pack('v', -2) |
||||
|
. pack('v', 9) |
||||
|
. pack('v', 6) |
||||
|
. pack('v', 0) |
||||
|
. "\x00\x00\x00\x00" |
||||
|
. "\x00\x00\x00\x00" |
||||
|
. pack('V', $iBdCnt) |
||||
|
. pack('V', $iBBcnt + $iSBDcnt) //ROOT START |
||||
|
. pack('V', 0) |
||||
|
. pack('V', 0x1000) |
||||
|
. pack('V', $iSBDcnt ? 0 : -2) //Small Block Depot |
||||
|
. pack('V', $iSBDcnt) |
||||
|
); |
||||
|
// Extra BDList Start, Count |
||||
|
if ($iBdCnt < $i1stBdL) { |
||||
|
fwrite( |
||||
|
$FILE, |
||||
|
pack('V', -2) // Extra BDList Start |
||||
|
. pack('V', 0)// Extra BDList Count |
||||
|
); |
||||
|
} else { |
||||
|
fwrite($FILE, pack('V', $iAll + $iBdCnt) . pack('V', $iBdExL)); |
||||
|
} |
||||
|
|
||||
|
// BDList |
||||
|
for ($i = 0; $i < $i1stBdL && $i < $iBdCnt; ++$i) { |
||||
|
fwrite($FILE, pack('V', $iAll + $i)); |
||||
|
} |
||||
|
if ($i < $i1stBdL) { |
||||
|
$jB = $i1stBdL - $i; |
||||
|
for ($j = 0; $j < $jB; ++$j) { |
||||
|
fwrite($FILE, (pack('V', -1))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Saving big data (PPS's with data bigger than \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE_DATA_SIZE_SMALL). |
||||
|
* |
||||
|
* @param int $iStBlk |
||||
|
* @param array $raList Reference to array of PPS's |
||||
|
*/ |
||||
|
private function saveBigData($iStBlk, &$raList): void |
||||
|
{ |
||||
|
$FILE = $this->fileHandle; |
||||
|
|
||||
|
// cycle through PPS's |
||||
|
$iCount = count($raList); |
||||
|
for ($i = 0; $i < $iCount; ++$i) { |
||||
|
if ($raList[$i]->Type != OLE::OLE_PPS_TYPE_DIR) { |
||||
|
$raList[$i]->Size = $raList[$i]->getDataLen(); |
||||
|
if (($raList[$i]->Size >= OLE::OLE_DATA_SIZE_SMALL) || (($raList[$i]->Type == OLE::OLE_PPS_TYPE_ROOT) && isset($raList[$i]->_data))) { |
||||
|
fwrite($FILE, $raList[$i]->_data); |
||||
|
|
||||
|
if ($raList[$i]->Size % $this->bigBlockSize) { |
||||
|
fwrite($FILE, str_repeat("\x00", $this->bigBlockSize - ($raList[$i]->Size % $this->bigBlockSize))); |
||||
|
} |
||||
|
// Set For PPS |
||||
|
$raList[$i]->startBlock = $iStBlk; |
||||
|
$iStBlk += |
||||
|
(floor($raList[$i]->Size / $this->bigBlockSize) + |
||||
|
(($raList[$i]->Size % $this->bigBlockSize) ? 1 : 0)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* get small data (PPS's with data smaller than \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE_DATA_SIZE_SMALL). |
||||
|
* |
||||
|
* @param array $raList Reference to array of PPS's |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
private function makeSmallData(&$raList) |
||||
|
{ |
||||
|
$sRes = ''; |
||||
|
$FILE = $this->fileHandle; |
||||
|
$iSmBlk = 0; |
||||
|
|
||||
|
$iCount = count($raList); |
||||
|
for ($i = 0; $i < $iCount; ++$i) { |
||||
|
// Make SBD, small data string |
||||
|
if ($raList[$i]->Type == OLE::OLE_PPS_TYPE_FILE) { |
||||
|
if ($raList[$i]->Size <= 0) { |
||||
|
continue; |
||||
|
} |
||||
|
if ($raList[$i]->Size < OLE::OLE_DATA_SIZE_SMALL) { |
||||
|
$iSmbCnt = floor($raList[$i]->Size / $this->smallBlockSize) |
||||
|
+ (($raList[$i]->Size % $this->smallBlockSize) ? 1 : 0); |
||||
|
// Add to SBD |
||||
|
$jB = $iSmbCnt - 1; |
||||
|
for ($j = 0; $j < $jB; ++$j) { |
||||
|
fwrite($FILE, pack('V', $j + $iSmBlk + 1)); |
||||
|
} |
||||
|
fwrite($FILE, pack('V', -2)); |
||||
|
|
||||
|
// Add to Data String(this will be written for RootEntry) |
||||
|
$sRes .= $raList[$i]->_data; |
||||
|
if ($raList[$i]->Size % $this->smallBlockSize) { |
||||
|
$sRes .= str_repeat("\x00", $this->smallBlockSize - ($raList[$i]->Size % $this->smallBlockSize)); |
||||
|
} |
||||
|
// Set for PPS |
||||
|
$raList[$i]->startBlock = $iSmBlk; |
||||
|
$iSmBlk += $iSmbCnt; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
$iSbCnt = floor($this->bigBlockSize / OLE::OLE_LONG_INT_SIZE); |
||||
|
if ($iSmBlk % $iSbCnt) { |
||||
|
$iB = $iSbCnt - ($iSmBlk % $iSbCnt); |
||||
|
for ($i = 0; $i < $iB; ++$i) { |
||||
|
fwrite($FILE, pack('V', -1)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $sRes; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Saves all the PPS's WKs. |
||||
|
* |
||||
|
* @param array $raList Reference to an array with all PPS's |
||||
|
*/ |
||||
|
private function savePps(&$raList): void |
||||
|
{ |
||||
|
// Save each PPS WK |
||||
|
$iC = count($raList); |
||||
|
for ($i = 0; $i < $iC; ++$i) { |
||||
|
fwrite($this->fileHandle, $raList[$i]->getPpsWk()); |
||||
|
} |
||||
|
// Adjust for Block |
||||
|
$iCnt = count($raList); |
||||
|
$iBCnt = $this->bigBlockSize / OLE::OLE_PPS_SIZE; |
||||
|
if ($iCnt % $iBCnt) { |
||||
|
fwrite($this->fileHandle, str_repeat("\x00", ($iBCnt - ($iCnt % $iBCnt)) * OLE::OLE_PPS_SIZE)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Saving Big Block Depot. |
||||
|
* |
||||
|
* @param int $iSbdSize |
||||
|
* @param int $iBsize |
||||
|
* @param int $iPpsCnt |
||||
|
*/ |
||||
|
private function saveBbd($iSbdSize, $iBsize, $iPpsCnt): void |
||||
|
{ |
||||
|
$FILE = $this->fileHandle; |
||||
|
// Calculate Basic Setting |
||||
|
$iBbCnt = $this->bigBlockSize / OLE::OLE_LONG_INT_SIZE; |
||||
|
$i1stBdL = ($this->bigBlockSize - 0x4C) / OLE::OLE_LONG_INT_SIZE; |
||||
|
|
||||
|
$iBdExL = 0; |
||||
|
$iAll = $iBsize + $iPpsCnt + $iSbdSize; |
||||
|
$iAllW = $iAll; |
||||
|
$iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0); |
||||
|
$iBdCnt = floor(($iAll + $iBdCntW) / $iBbCnt) + ((($iAllW + $iBdCntW) % $iBbCnt) ? 1 : 0); |
||||
|
// Calculate BD count |
||||
|
if ($iBdCnt > $i1stBdL) { |
||||
|
while (1) { |
||||
|
++$iBdExL; |
||||
|
++$iAllW; |
||||
|
$iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0); |
||||
|
$iBdCnt = floor(($iAllW + $iBdCntW) / $iBbCnt) + ((($iAllW + $iBdCntW) % $iBbCnt) ? 1 : 0); |
||||
|
if ($iBdCnt <= ($iBdExL * $iBbCnt + $i1stBdL)) { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Making BD |
||||
|
// Set for SBD |
||||
|
if ($iSbdSize > 0) { |
||||
|
for ($i = 0; $i < ($iSbdSize - 1); ++$i) { |
||||
|
fwrite($FILE, pack('V', $i + 1)); |
||||
|
} |
||||
|
fwrite($FILE, pack('V', -2)); |
||||
|
} |
||||
|
// Set for B |
||||
|
for ($i = 0; $i < ($iBsize - 1); ++$i) { |
||||
|
fwrite($FILE, pack('V', $i + $iSbdSize + 1)); |
||||
|
} |
||||
|
fwrite($FILE, pack('V', -2)); |
||||
|
|
||||
|
// Set for PPS |
||||
|
for ($i = 0; $i < ($iPpsCnt - 1); ++$i) { |
||||
|
fwrite($FILE, pack('V', $i + $iSbdSize + $iBsize + 1)); |
||||
|
} |
||||
|
fwrite($FILE, pack('V', -2)); |
||||
|
// Set for BBD itself ( 0xFFFFFFFD : BBD) |
||||
|
for ($i = 0; $i < $iBdCnt; ++$i) { |
||||
|
fwrite($FILE, pack('V', 0xFFFFFFFD)); |
||||
|
} |
||||
|
// Set for ExtraBDList |
||||
|
for ($i = 0; $i < $iBdExL; ++$i) { |
||||
|
fwrite($FILE, pack('V', 0xFFFFFFFC)); |
||||
|
} |
||||
|
// Adjust for Block |
||||
|
if (($iAllW + $iBdCnt) % $iBbCnt) { |
||||
|
$iBlock = ($iBbCnt - (($iAllW + $iBdCnt) % $iBbCnt)); |
||||
|
for ($i = 0; $i < $iBlock; ++$i) { |
||||
|
fwrite($FILE, pack('V', -1)); |
||||
|
} |
||||
|
} |
||||
|
// Extra BDList |
||||
|
if ($iBdCnt > $i1stBdL) { |
||||
|
$iN = 0; |
||||
|
$iNb = 0; |
||||
|
for ($i = $i1stBdL; $i < $iBdCnt; $i++, ++$iN) { |
||||
|
if ($iN >= ($iBbCnt - 1)) { |
||||
|
$iN = 0; |
||||
|
++$iNb; |
||||
|
fwrite($FILE, pack('V', $iAll + $iBdCnt + $iNb)); |
||||
|
} |
||||
|
fwrite($FILE, pack('V', $iBsize + $iSbdSize + $iPpsCnt + $i)); |
||||
|
} |
||||
|
if (($iBdCnt - $i1stBdL) % ($iBbCnt - 1)) { |
||||
|
$iB = ($iBbCnt - 1) - (($iBdCnt - $i1stBdL) % ($iBbCnt - 1)); |
||||
|
for ($i = 0; $i < $iB; ++$i) { |
||||
|
fwrite($FILE, pack('V', -1)); |
||||
|
} |
||||
|
} |
||||
|
fwrite($FILE, pack('V', -2)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,722 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Calculation\Calculation; |
||||
|
|
||||
|
class StringHelper |
||||
|
{ |
||||
|
/** Constants */ |
||||
|
/** Regular Expressions */ |
||||
|
// Fraction |
||||
|
const STRING_REGEXP_FRACTION = '(-?)(\d+)\s+(\d+\/\d+)'; |
||||
|
|
||||
|
/** |
||||
|
* Control characters array. |
||||
|
* |
||||
|
* @var string[] |
||||
|
*/ |
||||
|
private static $controlCharacters = []; |
||||
|
|
||||
|
/** |
||||
|
* SYLK Characters array. |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
private static $SYLKCharacters = []; |
||||
|
|
||||
|
/** |
||||
|
* Decimal separator. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private static $decimalSeparator; |
||||
|
|
||||
|
/** |
||||
|
* Thousands separator. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private static $thousandsSeparator; |
||||
|
|
||||
|
/** |
||||
|
* Currency code. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private static $currencyCode; |
||||
|
|
||||
|
/** |
||||
|
* Is iconv extension avalable? |
||||
|
* |
||||
|
* @var ?bool |
||||
|
*/ |
||||
|
private static $isIconvEnabled; |
||||
|
|
||||
|
/** |
||||
|
* iconv options. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private static $iconvOptions = '//IGNORE//TRANSLIT'; |
||||
|
|
||||
|
/** |
||||
|
* Build control characters array. |
||||
|
*/ |
||||
|
private static function buildControlCharacters(): void |
||||
|
{ |
||||
|
for ($i = 0; $i <= 31; ++$i) { |
||||
|
if ($i != 9 && $i != 10 && $i != 13) { |
||||
|
$find = '_x' . sprintf('%04s', strtoupper(dechex($i))) . '_'; |
||||
|
$replace = chr($i); |
||||
|
self::$controlCharacters[$find] = $replace; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Build SYLK characters array. |
||||
|
*/ |
||||
|
private static function buildSYLKCharacters(): void |
||||
|
{ |
||||
|
self::$SYLKCharacters = [ |
||||
|
"\x1B 0" => chr(0), |
||||
|
"\x1B 1" => chr(1), |
||||
|
"\x1B 2" => chr(2), |
||||
|
"\x1B 3" => chr(3), |
||||
|
"\x1B 4" => chr(4), |
||||
|
"\x1B 5" => chr(5), |
||||
|
"\x1B 6" => chr(6), |
||||
|
"\x1B 7" => chr(7), |
||||
|
"\x1B 8" => chr(8), |
||||
|
"\x1B 9" => chr(9), |
||||
|
"\x1B :" => chr(10), |
||||
|
"\x1B ;" => chr(11), |
||||
|
"\x1B <" => chr(12), |
||||
|
"\x1B =" => chr(13), |
||||
|
"\x1B >" => chr(14), |
||||
|
"\x1B ?" => chr(15), |
||||
|
"\x1B!0" => chr(16), |
||||
|
"\x1B!1" => chr(17), |
||||
|
"\x1B!2" => chr(18), |
||||
|
"\x1B!3" => chr(19), |
||||
|
"\x1B!4" => chr(20), |
||||
|
"\x1B!5" => chr(21), |
||||
|
"\x1B!6" => chr(22), |
||||
|
"\x1B!7" => chr(23), |
||||
|
"\x1B!8" => chr(24), |
||||
|
"\x1B!9" => chr(25), |
||||
|
"\x1B!:" => chr(26), |
||||
|
"\x1B!;" => chr(27), |
||||
|
"\x1B!<" => chr(28), |
||||
|
"\x1B!=" => chr(29), |
||||
|
"\x1B!>" => chr(30), |
||||
|
"\x1B!?" => chr(31), |
||||
|
"\x1B'?" => chr(127), |
||||
|
"\x1B(0" => '€', // 128 in CP1252 |
||||
|
"\x1B(2" => '‚', // 130 in CP1252 |
||||
|
"\x1B(3" => 'ƒ', // 131 in CP1252 |
||||
|
"\x1B(4" => '„', // 132 in CP1252 |
||||
|
"\x1B(5" => '…', // 133 in CP1252 |
||||
|
"\x1B(6" => '†', // 134 in CP1252 |
||||
|
"\x1B(7" => '‡', // 135 in CP1252 |
||||
|
"\x1B(8" => 'ˆ', // 136 in CP1252 |
||||
|
"\x1B(9" => '‰', // 137 in CP1252 |
||||
|
"\x1B(:" => 'Š', // 138 in CP1252 |
||||
|
"\x1B(;" => '‹', // 139 in CP1252 |
||||
|
"\x1BNj" => 'Œ', // 140 in CP1252 |
||||
|
"\x1B(>" => 'Ž', // 142 in CP1252 |
||||
|
"\x1B)1" => '‘', // 145 in CP1252 |
||||
|
"\x1B)2" => '’', // 146 in CP1252 |
||||
|
"\x1B)3" => '“', // 147 in CP1252 |
||||
|
"\x1B)4" => '”', // 148 in CP1252 |
||||
|
"\x1B)5" => '•', // 149 in CP1252 |
||||
|
"\x1B)6" => '–', // 150 in CP1252 |
||||
|
"\x1B)7" => '—', // 151 in CP1252 |
||||
|
"\x1B)8" => '˜', // 152 in CP1252 |
||||
|
"\x1B)9" => '™', // 153 in CP1252 |
||||
|
"\x1B):" => 'š', // 154 in CP1252 |
||||
|
"\x1B);" => '›', // 155 in CP1252 |
||||
|
"\x1BNz" => 'œ', // 156 in CP1252 |
||||
|
"\x1B)>" => 'ž', // 158 in CP1252 |
||||
|
"\x1B)?" => 'Ÿ', // 159 in CP1252 |
||||
|
"\x1B*0" => ' ', // 160 in CP1252 |
||||
|
"\x1BN!" => '¡', // 161 in CP1252 |
||||
|
"\x1BN\"" => '¢', // 162 in CP1252 |
||||
|
"\x1BN#" => '£', // 163 in CP1252 |
||||
|
"\x1BN(" => '¤', // 164 in CP1252 |
||||
|
"\x1BN%" => '¥', // 165 in CP1252 |
||||
|
"\x1B*6" => '¦', // 166 in CP1252 |
||||
|
"\x1BN'" => '§', // 167 in CP1252 |
||||
|
"\x1BNH " => '¨', // 168 in CP1252 |
||||
|
"\x1BNS" => '©', // 169 in CP1252 |
||||
|
"\x1BNc" => 'ª', // 170 in CP1252 |
||||
|
"\x1BN+" => '«', // 171 in CP1252 |
||||
|
"\x1B*<" => '¬', // 172 in CP1252 |
||||
|
"\x1B*=" => '', // 173 in CP1252 |
||||
|
"\x1BNR" => '®', // 174 in CP1252 |
||||
|
"\x1B*?" => '¯', // 175 in CP1252 |
||||
|
"\x1BN0" => '°', // 176 in CP1252 |
||||
|
"\x1BN1" => '±', // 177 in CP1252 |
||||
|
"\x1BN2" => '²', // 178 in CP1252 |
||||
|
"\x1BN3" => '³', // 179 in CP1252 |
||||
|
"\x1BNB " => '´', // 180 in CP1252 |
||||
|
"\x1BN5" => 'µ', // 181 in CP1252 |
||||
|
"\x1BN6" => '¶', // 182 in CP1252 |
||||
|
"\x1BN7" => '·', // 183 in CP1252 |
||||
|
"\x1B+8" => '¸', // 184 in CP1252 |
||||
|
"\x1BNQ" => '¹', // 185 in CP1252 |
||||
|
"\x1BNk" => 'º', // 186 in CP1252 |
||||
|
"\x1BN;" => '»', // 187 in CP1252 |
||||
|
"\x1BN<" => '¼', // 188 in CP1252 |
||||
|
"\x1BN=" => '½', // 189 in CP1252 |
||||
|
"\x1BN>" => '¾', // 190 in CP1252 |
||||
|
"\x1BN?" => '¿', // 191 in CP1252 |
||||
|
"\x1BNAA" => 'À', // 192 in CP1252 |
||||
|
"\x1BNBA" => 'Á', // 193 in CP1252 |
||||
|
"\x1BNCA" => 'Â', // 194 in CP1252 |
||||
|
"\x1BNDA" => 'Ã', // 195 in CP1252 |
||||
|
"\x1BNHA" => 'Ä', // 196 in CP1252 |
||||
|
"\x1BNJA" => 'Å', // 197 in CP1252 |
||||
|
"\x1BNa" => 'Æ', // 198 in CP1252 |
||||
|
"\x1BNKC" => 'Ç', // 199 in CP1252 |
||||
|
"\x1BNAE" => 'È', // 200 in CP1252 |
||||
|
"\x1BNBE" => 'É', // 201 in CP1252 |
||||
|
"\x1BNCE" => 'Ê', // 202 in CP1252 |
||||
|
"\x1BNHE" => 'Ë', // 203 in CP1252 |
||||
|
"\x1BNAI" => 'Ì', // 204 in CP1252 |
||||
|
"\x1BNBI" => 'Í', // 205 in CP1252 |
||||
|
"\x1BNCI" => 'Î', // 206 in CP1252 |
||||
|
"\x1BNHI" => 'Ï', // 207 in CP1252 |
||||
|
"\x1BNb" => 'Ð', // 208 in CP1252 |
||||
|
"\x1BNDN" => 'Ñ', // 209 in CP1252 |
||||
|
"\x1BNAO" => 'Ò', // 210 in CP1252 |
||||
|
"\x1BNBO" => 'Ó', // 211 in CP1252 |
||||
|
"\x1BNCO" => 'Ô', // 212 in CP1252 |
||||
|
"\x1BNDO" => 'Õ', // 213 in CP1252 |
||||
|
"\x1BNHO" => 'Ö', // 214 in CP1252 |
||||
|
"\x1B-7" => '×', // 215 in CP1252 |
||||
|
"\x1BNi" => 'Ø', // 216 in CP1252 |
||||
|
"\x1BNAU" => 'Ù', // 217 in CP1252 |
||||
|
"\x1BNBU" => 'Ú', // 218 in CP1252 |
||||
|
"\x1BNCU" => 'Û', // 219 in CP1252 |
||||
|
"\x1BNHU" => 'Ü', // 220 in CP1252 |
||||
|
"\x1B-=" => 'Ý', // 221 in CP1252 |
||||
|
"\x1BNl" => 'Þ', // 222 in CP1252 |
||||
|
"\x1BN{" => 'ß', // 223 in CP1252 |
||||
|
"\x1BNAa" => 'à', // 224 in CP1252 |
||||
|
"\x1BNBa" => 'á', // 225 in CP1252 |
||||
|
"\x1BNCa" => 'â', // 226 in CP1252 |
||||
|
"\x1BNDa" => 'ã', // 227 in CP1252 |
||||
|
"\x1BNHa" => 'ä', // 228 in CP1252 |
||||
|
"\x1BNJa" => 'å', // 229 in CP1252 |
||||
|
"\x1BNq" => 'æ', // 230 in CP1252 |
||||
|
"\x1BNKc" => 'ç', // 231 in CP1252 |
||||
|
"\x1BNAe" => 'è', // 232 in CP1252 |
||||
|
"\x1BNBe" => 'é', // 233 in CP1252 |
||||
|
"\x1BNCe" => 'ê', // 234 in CP1252 |
||||
|
"\x1BNHe" => 'ë', // 235 in CP1252 |
||||
|
"\x1BNAi" => 'ì', // 236 in CP1252 |
||||
|
"\x1BNBi" => 'í', // 237 in CP1252 |
||||
|
"\x1BNCi" => 'î', // 238 in CP1252 |
||||
|
"\x1BNHi" => 'ï', // 239 in CP1252 |
||||
|
"\x1BNs" => 'ð', // 240 in CP1252 |
||||
|
"\x1BNDn" => 'ñ', // 241 in CP1252 |
||||
|
"\x1BNAo" => 'ò', // 242 in CP1252 |
||||
|
"\x1BNBo" => 'ó', // 243 in CP1252 |
||||
|
"\x1BNCo" => 'ô', // 244 in CP1252 |
||||
|
"\x1BNDo" => 'õ', // 245 in CP1252 |
||||
|
"\x1BNHo" => 'ö', // 246 in CP1252 |
||||
|
"\x1B/7" => '÷', // 247 in CP1252 |
||||
|
"\x1BNy" => 'ø', // 248 in CP1252 |
||||
|
"\x1BNAu" => 'ù', // 249 in CP1252 |
||||
|
"\x1BNBu" => 'ú', // 250 in CP1252 |
||||
|
"\x1BNCu" => 'û', // 251 in CP1252 |
||||
|
"\x1BNHu" => 'ü', // 252 in CP1252 |
||||
|
"\x1B/=" => 'ý', // 253 in CP1252 |
||||
|
"\x1BN|" => 'þ', // 254 in CP1252 |
||||
|
"\x1BNHy" => 'ÿ', // 255 in CP1252 |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get whether iconv extension is available. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public static function getIsIconvEnabled() |
||||
|
{ |
||||
|
if (isset(self::$isIconvEnabled)) { |
||||
|
return self::$isIconvEnabled; |
||||
|
} |
||||
|
|
||||
|
// Assume no problems with iconv |
||||
|
self::$isIconvEnabled = true; |
||||
|
|
||||
|
// Fail if iconv doesn't exist |
||||
|
if (!function_exists('iconv')) { |
||||
|
self::$isIconvEnabled = false; |
||||
|
} elseif (!@iconv('UTF-8', 'UTF-16LE', 'x')) { |
||||
|
// Sometimes iconv is not working, and e.g. iconv('UTF-8', 'UTF-16LE', 'x') just returns false, |
||||
|
self::$isIconvEnabled = false; |
||||
|
} elseif (defined('PHP_OS') && @stristr(PHP_OS, 'AIX') && defined('ICONV_IMPL') && (@strcasecmp(ICONV_IMPL, 'unknown') == 0) && defined('ICONV_VERSION') && (@strcasecmp(ICONV_VERSION, 'unknown') == 0)) { |
||||
|
// CUSTOM: IBM AIX iconv() does not work |
||||
|
self::$isIconvEnabled = false; |
||||
|
} |
||||
|
|
||||
|
// Deactivate iconv default options if they fail (as seen on IMB i) |
||||
|
if (self::$isIconvEnabled && !@iconv('UTF-8', 'UTF-16LE' . self::$iconvOptions, 'x')) { |
||||
|
self::$iconvOptions = ''; |
||||
|
} |
||||
|
|
||||
|
return self::$isIconvEnabled; |
||||
|
} |
||||
|
|
||||
|
private static function buildCharacterSets(): void |
||||
|
{ |
||||
|
if (empty(self::$controlCharacters)) { |
||||
|
self::buildControlCharacters(); |
||||
|
} |
||||
|
|
||||
|
if (empty(self::$SYLKCharacters)) { |
||||
|
self::buildSYLKCharacters(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert from OpenXML escaped control character to PHP control character. |
||||
|
* |
||||
|
* Excel 2007 team: |
||||
|
* ---------------- |
||||
|
* That's correct, control characters are stored directly in the shared-strings table. |
||||
|
* We do encode characters that cannot be represented in XML using the following escape sequence: |
||||
|
* _xHHHH_ where H represents a hexadecimal character in the character's value... |
||||
|
* So you could end up with something like _x0008_ in a string (either in a cell value (<v>) |
||||
|
* element or in the shared string <t> element. |
||||
|
* |
||||
|
* @param string $textValue Value to unescape |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function controlCharacterOOXML2PHP($textValue) |
||||
|
{ |
||||
|
self::buildCharacterSets(); |
||||
|
|
||||
|
return str_replace(array_keys(self::$controlCharacters), array_values(self::$controlCharacters), $textValue); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert from PHP control character to OpenXML escaped control character. |
||||
|
* |
||||
|
* Excel 2007 team: |
||||
|
* ---------------- |
||||
|
* That's correct, control characters are stored directly in the shared-strings table. |
||||
|
* We do encode characters that cannot be represented in XML using the following escape sequence: |
||||
|
* _xHHHH_ where H represents a hexadecimal character in the character's value... |
||||
|
* So you could end up with something like _x0008_ in a string (either in a cell value (<v>) |
||||
|
* element or in the shared string <t> element. |
||||
|
* |
||||
|
* @param string $textValue Value to escape |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function controlCharacterPHP2OOXML($textValue) |
||||
|
{ |
||||
|
self::buildCharacterSets(); |
||||
|
|
||||
|
return str_replace(array_values(self::$controlCharacters), array_keys(self::$controlCharacters), $textValue); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Try to sanitize UTF8, stripping invalid byte sequences. Not perfect. Does not surrogate characters. |
||||
|
* |
||||
|
* @param string $textValue |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function sanitizeUTF8($textValue) |
||||
|
{ |
||||
|
if (self::getIsIconvEnabled()) { |
||||
|
$textValue = @iconv('UTF-8', 'UTF-8', $textValue); |
||||
|
|
||||
|
return $textValue; |
||||
|
} |
||||
|
|
||||
|
$textValue = mb_convert_encoding($textValue, 'UTF-8', 'UTF-8'); |
||||
|
|
||||
|
return $textValue; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if a string contains UTF8 data. |
||||
|
* |
||||
|
* @param string $textValue |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public static function isUTF8($textValue) |
||||
|
{ |
||||
|
return $textValue === '' || preg_match('/^./su', $textValue) === 1; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Formats a numeric value as a string for output in various output writers forcing |
||||
|
* point as decimal separator in case locale is other than English. |
||||
|
* |
||||
|
* @param mixed $numericValue |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function formatNumber($numericValue) |
||||
|
{ |
||||
|
if (is_float($numericValue)) { |
||||
|
return str_replace(',', '.', $numericValue); |
||||
|
} |
||||
|
|
||||
|
return (string) $numericValue; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Converts a UTF-8 string into BIFF8 Unicode string data (8-bit string length) |
||||
|
* Writes the string using uncompressed notation, no rich text, no Asian phonetics |
||||
|
* If mbstring extension is not available, ASCII is assumed, and compressed notation is used |
||||
|
* although this will give wrong results for non-ASCII strings |
||||
|
* see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3. |
||||
|
* |
||||
|
* @param string $textValue UTF-8 encoded string |
||||
|
* @param mixed[] $arrcRuns Details of rich text runs in $value |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function UTF8toBIFF8UnicodeShort($textValue, $arrcRuns = []) |
||||
|
{ |
||||
|
// character count |
||||
|
$ln = self::countCharacters($textValue, 'UTF-8'); |
||||
|
// option flags |
||||
|
if (empty($arrcRuns)) { |
||||
|
$data = pack('CC', $ln, 0x0001); |
||||
|
// characters |
||||
|
$data .= self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8'); |
||||
|
} else { |
||||
|
$data = pack('vC', $ln, 0x09); |
||||
|
$data .= pack('v', count($arrcRuns)); |
||||
|
// characters |
||||
|
$data .= self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8'); |
||||
|
foreach ($arrcRuns as $cRun) { |
||||
|
$data .= pack('v', $cRun['strlen']); |
||||
|
$data .= pack('v', $cRun['fontidx']); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Converts a UTF-8 string into BIFF8 Unicode string data (16-bit string length) |
||||
|
* Writes the string using uncompressed notation, no rich text, no Asian phonetics |
||||
|
* If mbstring extension is not available, ASCII is assumed, and compressed notation is used |
||||
|
* although this will give wrong results for non-ASCII strings |
||||
|
* see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3. |
||||
|
* |
||||
|
* @param string $textValue UTF-8 encoded string |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function UTF8toBIFF8UnicodeLong($textValue) |
||||
|
{ |
||||
|
// character count |
||||
|
$ln = self::countCharacters($textValue, 'UTF-8'); |
||||
|
|
||||
|
// characters |
||||
|
$chars = self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8'); |
||||
|
|
||||
|
return pack('vC', $ln, 0x0001) . $chars; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert string from one encoding to another. |
||||
|
* |
||||
|
* @param string $textValue |
||||
|
* @param string $to Encoding to convert to, e.g. 'UTF-8' |
||||
|
* @param string $from Encoding to convert from, e.g. 'UTF-16LE' |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function convertEncoding($textValue, $to, $from) |
||||
|
{ |
||||
|
if (self::getIsIconvEnabled()) { |
||||
|
$result = iconv($from, $to . self::$iconvOptions, $textValue); |
||||
|
if (false !== $result) { |
||||
|
return $result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return mb_convert_encoding($textValue, $to, $from); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get character count. |
||||
|
* |
||||
|
* @param string $textValue |
||||
|
* @param string $encoding Encoding |
||||
|
* |
||||
|
* @return int Character count |
||||
|
*/ |
||||
|
public static function countCharacters($textValue, $encoding = 'UTF-8') |
||||
|
{ |
||||
|
return mb_strlen($textValue ?? '', $encoding); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get a substring of a UTF-8 encoded string. |
||||
|
* |
||||
|
* @param string $textValue UTF-8 encoded string |
||||
|
* @param int $offset Start offset |
||||
|
* @param int $length Maximum number of characters in substring |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function substring($textValue, $offset, $length = 0) |
||||
|
{ |
||||
|
return mb_substr($textValue, $offset, $length, 'UTF-8'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert a UTF-8 encoded string to upper case. |
||||
|
* |
||||
|
* @param string $textValue UTF-8 encoded string |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function strToUpper($textValue) |
||||
|
{ |
||||
|
return mb_convert_case($textValue ?? '', MB_CASE_UPPER, 'UTF-8'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert a UTF-8 encoded string to lower case. |
||||
|
* |
||||
|
* @param string $textValue UTF-8 encoded string |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function strToLower($textValue) |
||||
|
{ |
||||
|
return mb_convert_case($textValue ?? '', MB_CASE_LOWER, 'UTF-8'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert a UTF-8 encoded string to title/proper case |
||||
|
* (uppercase every first character in each word, lower case all other characters). |
||||
|
* |
||||
|
* @param string $textValue UTF-8 encoded string |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function strToTitle($textValue) |
||||
|
{ |
||||
|
return mb_convert_case($textValue, MB_CASE_TITLE, 'UTF-8'); |
||||
|
} |
||||
|
|
||||
|
public static function mbIsUpper($character) |
||||
|
{ |
||||
|
return mb_strtolower($character, 'UTF-8') != $character; |
||||
|
} |
||||
|
|
||||
|
public static function mbStrSplit($string) |
||||
|
{ |
||||
|
// Split at all position not after the start: ^ |
||||
|
// and not before the end: $ |
||||
|
return preg_split('/(?<!^)(?!$)/u', $string); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Reverse the case of a string, so that all uppercase characters become lowercase |
||||
|
* and all lowercase characters become uppercase. |
||||
|
* |
||||
|
* @param string $textValue UTF-8 encoded string |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function strCaseReverse($textValue) |
||||
|
{ |
||||
|
$characters = self::mbStrSplit($textValue); |
||||
|
foreach ($characters as &$character) { |
||||
|
if (self::mbIsUpper($character)) { |
||||
|
$character = mb_strtolower($character, 'UTF-8'); |
||||
|
} else { |
||||
|
$character = mb_strtoupper($character, 'UTF-8'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return implode('', $characters); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Identify whether a string contains a fractional numeric value, |
||||
|
* and convert it to a numeric if it is. |
||||
|
* |
||||
|
* @param string $operand string value to test |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public static function convertToNumberIfFraction(&$operand) |
||||
|
{ |
||||
|
if (preg_match('/^' . self::STRING_REGEXP_FRACTION . '$/i', $operand, $match)) { |
||||
|
$sign = ($match[1] == '-') ? '-' : '+'; |
||||
|
$fractionFormula = '=' . $sign . $match[2] . $sign . $match[3]; |
||||
|
$operand = Calculation::getInstance()->_calculateFormulaValue($fractionFormula); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// function convertToNumberIfFraction() |
||||
|
|
||||
|
/** |
||||
|
* Get the decimal separator. If it has not yet been set explicitly, try to obtain number |
||||
|
* formatting information from locale. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function getDecimalSeparator() |
||||
|
{ |
||||
|
if (!isset(self::$decimalSeparator)) { |
||||
|
$localeconv = localeconv(); |
||||
|
self::$decimalSeparator = ($localeconv['decimal_point'] != '') |
||||
|
? $localeconv['decimal_point'] : $localeconv['mon_decimal_point']; |
||||
|
|
||||
|
if (self::$decimalSeparator == '') { |
||||
|
// Default to . |
||||
|
self::$decimalSeparator = '.'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return self::$decimalSeparator; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the decimal separator. Only used by NumberFormat::toFormattedString() |
||||
|
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf. |
||||
|
* |
||||
|
* @param string $separator Character for decimal separator |
||||
|
*/ |
||||
|
public static function setDecimalSeparator($separator): void |
||||
|
{ |
||||
|
self::$decimalSeparator = $separator; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the thousands separator. If it has not yet been set explicitly, try to obtain number |
||||
|
* formatting information from locale. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function getThousandsSeparator() |
||||
|
{ |
||||
|
if (!isset(self::$thousandsSeparator)) { |
||||
|
$localeconv = localeconv(); |
||||
|
self::$thousandsSeparator = ($localeconv['thousands_sep'] != '') |
||||
|
? $localeconv['thousands_sep'] : $localeconv['mon_thousands_sep']; |
||||
|
|
||||
|
if (self::$thousandsSeparator == '') { |
||||
|
// Default to . |
||||
|
self::$thousandsSeparator = ','; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return self::$thousandsSeparator; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the thousands separator. Only used by NumberFormat::toFormattedString() |
||||
|
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf. |
||||
|
* |
||||
|
* @param string $separator Character for thousands separator |
||||
|
*/ |
||||
|
public static function setThousandsSeparator($separator): void |
||||
|
{ |
||||
|
self::$thousandsSeparator = $separator; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the currency code. If it has not yet been set explicitly, try to obtain the |
||||
|
* symbol information from locale. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function getCurrencyCode() |
||||
|
{ |
||||
|
if (!empty(self::$currencyCode)) { |
||||
|
return self::$currencyCode; |
||||
|
} |
||||
|
self::$currencyCode = '$'; |
||||
|
$localeconv = localeconv(); |
||||
|
if (!empty($localeconv['currency_symbol'])) { |
||||
|
self::$currencyCode = $localeconv['currency_symbol']; |
||||
|
|
||||
|
return self::$currencyCode; |
||||
|
} |
||||
|
if (!empty($localeconv['int_curr_symbol'])) { |
||||
|
self::$currencyCode = $localeconv['int_curr_symbol']; |
||||
|
|
||||
|
return self::$currencyCode; |
||||
|
} |
||||
|
|
||||
|
return self::$currencyCode; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the currency code. Only used by NumberFormat::toFormattedString() |
||||
|
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf. |
||||
|
* |
||||
|
* @param string $currencyCode Character for currency code |
||||
|
*/ |
||||
|
public static function setCurrencyCode($currencyCode): void |
||||
|
{ |
||||
|
self::$currencyCode = $currencyCode; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert SYLK encoded string to UTF-8. |
||||
|
* |
||||
|
* @param string $textValue |
||||
|
* |
||||
|
* @return string UTF-8 encoded string |
||||
|
*/ |
||||
|
public static function SYLKtoUTF8($textValue) |
||||
|
{ |
||||
|
self::buildCharacterSets(); |
||||
|
|
||||
|
// If there is no escape character in the string there is nothing to do |
||||
|
if (strpos($textValue, '') === false) { |
||||
|
return $textValue; |
||||
|
} |
||||
|
|
||||
|
foreach (self::$SYLKCharacters as $k => $v) { |
||||
|
$textValue = str_replace($k, $v, $textValue); |
||||
|
} |
||||
|
|
||||
|
return $textValue; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Retrieve any leading numeric part of a string, or return the full string if no leading numeric |
||||
|
* (handles basic integer or float, but not exponent or non decimal). |
||||
|
* |
||||
|
* @param string $textValue |
||||
|
* |
||||
|
* @return mixed string or only the leading numeric part of the string |
||||
|
*/ |
||||
|
public static function testStringAsNumeric($textValue) |
||||
|
{ |
||||
|
if (is_numeric($textValue)) { |
||||
|
return $textValue; |
||||
|
} |
||||
|
$v = (float) $textValue; |
||||
|
|
||||
|
return (is_numeric(substr($textValue, 0, strlen($v)))) ? $v : $textValue; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared; |
||||
|
|
||||
|
use DateTimeZone; |
||||
|
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; |
||||
|
|
||||
|
class TimeZone |
||||
|
{ |
||||
|
/** |
||||
|
* Default Timezone used for date/time conversions. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected static $timezone = 'UTC'; |
||||
|
|
||||
|
/** |
||||
|
* Validate a Timezone name. |
||||
|
* |
||||
|
* @param string $timezoneName Time zone (e.g. 'Europe/London') |
||||
|
* |
||||
|
* @return bool Success or failure |
||||
|
*/ |
||||
|
private static function validateTimeZone($timezoneName) |
||||
|
{ |
||||
|
return in_array($timezoneName, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the Default Timezone used for date/time conversions. |
||||
|
* |
||||
|
* @param string $timezoneName Time zone (e.g. 'Europe/London') |
||||
|
* |
||||
|
* @return bool Success or failure |
||||
|
*/ |
||||
|
public static function setTimeZone($timezoneName) |
||||
|
{ |
||||
|
if (self::validateTimezone($timezoneName)) { |
||||
|
self::$timezone = $timezoneName; |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the Default Timezone used for date/time conversions. |
||||
|
* |
||||
|
* @return string Timezone (e.g. 'Europe/London') |
||||
|
*/ |
||||
|
public static function getTimeZone() |
||||
|
{ |
||||
|
return self::$timezone; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the Timezone offset used for date/time conversions to/from UST |
||||
|
* This requires both the timezone and the calculated date/time to allow for local DST. |
||||
|
* |
||||
|
* @param ?string $timezoneName The timezone for finding the adjustment to UST |
||||
|
* @param float|int $timestamp PHP date/time value |
||||
|
* |
||||
|
* @return int Number of seconds for timezone adjustment |
||||
|
*/ |
||||
|
public static function getTimeZoneAdjustment($timezoneName, $timestamp) |
||||
|
{ |
||||
|
$timezoneName = $timezoneName ?? self::$timezone; |
||||
|
$dtobj = Date::dateTimeFromTimestamp("$timestamp"); |
||||
|
if (!self::validateTimezone($timezoneName)) { |
||||
|
throw new PhpSpreadsheetException("Invalid timezone $timezoneName"); |
||||
|
} |
||||
|
$dtobj->setTimeZone(new DateTimeZone($timezoneName)); |
||||
|
|
||||
|
return $dtobj->getOffset(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,202 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared\Trend; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Shared\JAMA\Matrix; |
||||
|
|
||||
|
class PolynomialBestFit extends BestFit |
||||
|
{ |
||||
|
/** |
||||
|
* Algorithm type to use for best-fit |
||||
|
* (Name of this Trend class). |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $bestFitType = 'polynomial'; |
||||
|
|
||||
|
/** |
||||
|
* Polynomial order. |
||||
|
* |
||||
|
* @var int |
||||
|
*/ |
||||
|
protected $order = 0; |
||||
|
|
||||
|
/** |
||||
|
* Return the order of this polynomial. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function getOrder() |
||||
|
{ |
||||
|
return $this->order; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the Y-Value for a specified value of X. |
||||
|
* |
||||
|
* @param float $xValue X-Value |
||||
|
* |
||||
|
* @return float Y-Value |
||||
|
*/ |
||||
|
public function getValueOfYForX($xValue) |
||||
|
{ |
||||
|
$retVal = $this->getIntersect(); |
||||
|
$slope = $this->getSlope(); |
||||
|
// @phpstan-ignore-next-line |
||||
|
foreach ($slope as $key => $value) { |
||||
|
if ($value != 0.0) { |
||||
|
$retVal += $value * $xValue ** ($key + 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $retVal; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the X-Value for a specified value of Y. |
||||
|
* |
||||
|
* @param float $yValue Y-Value |
||||
|
* |
||||
|
* @return float X-Value |
||||
|
*/ |
||||
|
public function getValueOfXForY($yValue) |
||||
|
{ |
||||
|
return ($yValue - $this->getIntersect()) / $this->getSlope(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the Equation of the best-fit line. |
||||
|
* |
||||
|
* @param int $dp Number of places of decimal precision to display |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getEquation($dp = 0) |
||||
|
{ |
||||
|
$slope = $this->getSlope($dp); |
||||
|
$intersect = $this->getIntersect($dp); |
||||
|
|
||||
|
$equation = 'Y = ' . $intersect; |
||||
|
// @phpstan-ignore-next-line |
||||
|
foreach ($slope as $key => $value) { |
||||
|
if ($value != 0.0) { |
||||
|
$equation .= ' + ' . $value . ' * X'; |
||||
|
if ($key > 0) { |
||||
|
$equation .= '^' . ($key + 1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $equation; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the Slope of the line. |
||||
|
* |
||||
|
* @param int $dp Number of places of decimal precision to display |
||||
|
* |
||||
|
* @return float |
||||
|
*/ |
||||
|
public function getSlope($dp = 0) |
||||
|
{ |
||||
|
if ($dp != 0) { |
||||
|
$coefficients = []; |
||||
|
foreach ($this->slope as $coefficient) { |
||||
|
$coefficients[] = round($coefficient, $dp); |
||||
|
} |
||||
|
|
||||
|
// @phpstan-ignore-next-line |
||||
|
return $coefficients; |
||||
|
} |
||||
|
|
||||
|
return $this->slope; |
||||
|
} |
||||
|
|
||||
|
public function getCoefficients($dp = 0) |
||||
|
{ |
||||
|
return array_merge([$this->getIntersect($dp)], $this->getSlope($dp)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Execute the regression and calculate the goodness of fit for a set of X and Y data values. |
||||
|
* |
||||
|
* @param int $order Order of Polynomial for this regression |
||||
|
* @param float[] $yValues The set of Y-values for this regression |
||||
|
* @param float[] $xValues The set of X-values for this regression |
||||
|
*/ |
||||
|
private function polynomialRegression($order, $yValues, $xValues): void |
||||
|
{ |
||||
|
// calculate sums |
||||
|
$x_sum = array_sum($xValues); |
||||
|
$y_sum = array_sum($yValues); |
||||
|
$xx_sum = $xy_sum = $yy_sum = 0; |
||||
|
for ($i = 0; $i < $this->valueCount; ++$i) { |
||||
|
$xy_sum += $xValues[$i] * $yValues[$i]; |
||||
|
$xx_sum += $xValues[$i] * $xValues[$i]; |
||||
|
$yy_sum += $yValues[$i] * $yValues[$i]; |
||||
|
} |
||||
|
/* |
||||
|
* This routine uses logic from the PHP port of polyfit version 0.1 |
||||
|
* written by Michael Bommarito and Paul Meagher |
||||
|
* |
||||
|
* The function fits a polynomial function of order $order through |
||||
|
* a series of x-y data points using least squares. |
||||
|
* |
||||
|
*/ |
||||
|
$A = []; |
||||
|
$B = []; |
||||
|
for ($i = 0; $i < $this->valueCount; ++$i) { |
||||
|
for ($j = 0; $j <= $order; ++$j) { |
||||
|
$A[$i][$j] = $xValues[$i] ** $j; |
||||
|
} |
||||
|
} |
||||
|
for ($i = 0; $i < $this->valueCount; ++$i) { |
||||
|
$B[$i] = [$yValues[$i]]; |
||||
|
} |
||||
|
$matrixA = new Matrix($A); |
||||
|
$matrixB = new Matrix($B); |
||||
|
$C = $matrixA->solve($matrixB); |
||||
|
|
||||
|
$coefficients = []; |
||||
|
for ($i = 0; $i < $C->getRowDimension(); ++$i) { |
||||
|
$r = $C->get($i, 0); |
||||
|
if (abs($r) <= 10 ** (-9)) { |
||||
|
$r = 0; |
||||
|
} |
||||
|
$coefficients[] = $r; |
||||
|
} |
||||
|
|
||||
|
$this->intersect = array_shift($coefficients); |
||||
|
$this->slope = $coefficients; |
||||
|
|
||||
|
$this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, 0, 0, 0); |
||||
|
foreach ($this->xValues as $xKey => $xValue) { |
||||
|
$this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Define the regression and calculate the goodness of fit for a set of X and Y data values. |
||||
|
* |
||||
|
* @param int $order Order of Polynomial for this regression |
||||
|
* @param float[] $yValues The set of Y-values for this regression |
||||
|
* @param float[] $xValues The set of X-values for this regression |
||||
|
*/ |
||||
|
public function __construct($order, $yValues, $xValues = []) |
||||
|
{ |
||||
|
parent::__construct($yValues, $xValues); |
||||
|
|
||||
|
if (!$this->error) { |
||||
|
if ($order < $this->valueCount) { |
||||
|
$this->bestFitType .= '_' . $order; |
||||
|
$this->order = $order; |
||||
|
$this->polynomialRegression($order, $yValues, $xValues); |
||||
|
if (($this->getGoodnessOfFit() < 0.0) || ($this->getGoodnessOfFit() > 1.0)) { |
||||
|
$this->error = true; |
||||
|
} |
||||
|
} else { |
||||
|
$this->error = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,109 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared\Trend; |
||||
|
|
||||
|
class PowerBestFit extends BestFit |
||||
|
{ |
||||
|
/** |
||||
|
* Algorithm type to use for best-fit |
||||
|
* (Name of this Trend class). |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $bestFitType = 'power'; |
||||
|
|
||||
|
/** |
||||
|
* Return the Y-Value for a specified value of X. |
||||
|
* |
||||
|
* @param float $xValue X-Value |
||||
|
* |
||||
|
* @return float Y-Value |
||||
|
*/ |
||||
|
public function getValueOfYForX($xValue) |
||||
|
{ |
||||
|
return $this->getIntersect() * ($xValue - $this->xOffset) ** $this->getSlope(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the X-Value for a specified value of Y. |
||||
|
* |
||||
|
* @param float $yValue Y-Value |
||||
|
* |
||||
|
* @return float X-Value |
||||
|
*/ |
||||
|
public function getValueOfXForY($yValue) |
||||
|
{ |
||||
|
return (($yValue + $this->yOffset) / $this->getIntersect()) ** (1 / $this->getSlope()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the Equation of the best-fit line. |
||||
|
* |
||||
|
* @param int $dp Number of places of decimal precision to display |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getEquation($dp = 0) |
||||
|
{ |
||||
|
$slope = $this->getSlope($dp); |
||||
|
$intersect = $this->getIntersect($dp); |
||||
|
|
||||
|
return 'Y = ' . $intersect . ' * X^' . $slope; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the Value of X where it intersects Y = 0. |
||||
|
* |
||||
|
* @param int $dp Number of places of decimal precision to display |
||||
|
* |
||||
|
* @return float |
||||
|
*/ |
||||
|
public function getIntersect($dp = 0) |
||||
|
{ |
||||
|
if ($dp != 0) { |
||||
|
return round(exp($this->intersect), $dp); |
||||
|
} |
||||
|
|
||||
|
return exp($this->intersect); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Execute the regression and calculate the goodness of fit for a set of X and Y data values. |
||||
|
* |
||||
|
* @param float[] $yValues The set of Y-values for this regression |
||||
|
* @param float[] $xValues The set of X-values for this regression |
||||
|
*/ |
||||
|
private function powerRegression(array $yValues, array $xValues, bool $const): void |
||||
|
{ |
||||
|
$adjustedYValues = array_map( |
||||
|
function ($value) { |
||||
|
return ($value < 0.0) ? 0 - log(abs($value)) : log($value); |
||||
|
}, |
||||
|
$yValues |
||||
|
); |
||||
|
$adjustedXValues = array_map( |
||||
|
function ($value) { |
||||
|
return ($value < 0.0) ? 0 - log(abs($value)) : log($value); |
||||
|
}, |
||||
|
$xValues |
||||
|
); |
||||
|
|
||||
|
$this->leastSquareFit($adjustedYValues, $adjustedXValues, $const); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Define the regression and calculate the goodness of fit for a set of X and Y data values. |
||||
|
* |
||||
|
* @param float[] $yValues The set of Y-values for this regression |
||||
|
* @param float[] $xValues The set of X-values for this regression |
||||
|
* @param bool $const |
||||
|
*/ |
||||
|
public function __construct($yValues, $xValues = [], $const = true) |
||||
|
{ |
||||
|
parent::__construct($yValues, $xValues); |
||||
|
|
||||
|
if (!$this->error) { |
||||
|
$this->powerRegression($yValues, $xValues, (bool) $const); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,122 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared\Trend; |
||||
|
|
||||
|
class Trend |
||||
|
{ |
||||
|
const TREND_LINEAR = 'Linear'; |
||||
|
const TREND_LOGARITHMIC = 'Logarithmic'; |
||||
|
const TREND_EXPONENTIAL = 'Exponential'; |
||||
|
const TREND_POWER = 'Power'; |
||||
|
const TREND_POLYNOMIAL_2 = 'Polynomial_2'; |
||||
|
const TREND_POLYNOMIAL_3 = 'Polynomial_3'; |
||||
|
const TREND_POLYNOMIAL_4 = 'Polynomial_4'; |
||||
|
const TREND_POLYNOMIAL_5 = 'Polynomial_5'; |
||||
|
const TREND_POLYNOMIAL_6 = 'Polynomial_6'; |
||||
|
const TREND_BEST_FIT = 'Bestfit'; |
||||
|
const TREND_BEST_FIT_NO_POLY = 'Bestfit_no_Polynomials'; |
||||
|
|
||||
|
/** |
||||
|
* Names of the best-fit Trend analysis methods. |
||||
|
* |
||||
|
* @var string[] |
||||
|
*/ |
||||
|
private static $trendTypes = [ |
||||
|
self::TREND_LINEAR, |
||||
|
self::TREND_LOGARITHMIC, |
||||
|
self::TREND_EXPONENTIAL, |
||||
|
self::TREND_POWER, |
||||
|
]; |
||||
|
|
||||
|
/** |
||||
|
* Names of the best-fit Trend polynomial orders. |
||||
|
* |
||||
|
* @var string[] |
||||
|
*/ |
||||
|
private static $trendTypePolynomialOrders = [ |
||||
|
self::TREND_POLYNOMIAL_2, |
||||
|
self::TREND_POLYNOMIAL_3, |
||||
|
self::TREND_POLYNOMIAL_4, |
||||
|
self::TREND_POLYNOMIAL_5, |
||||
|
self::TREND_POLYNOMIAL_6, |
||||
|
]; |
||||
|
|
||||
|
/** |
||||
|
* Cached results for each method when trying to identify which provides the best fit. |
||||
|
* |
||||
|
* @var BestFit[] |
||||
|
*/ |
||||
|
private static $trendCache = []; |
||||
|
|
||||
|
public static function calculate($trendType = self::TREND_BEST_FIT, $yValues = [], $xValues = [], $const = true) |
||||
|
{ |
||||
|
// Calculate number of points in each dataset |
||||
|
$nY = count($yValues); |
||||
|
$nX = count($xValues); |
||||
|
|
||||
|
// Define X Values if necessary |
||||
|
if ($nX === 0) { |
||||
|
$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); |
||||
|
} |
||||
|
|
||||
|
$key = md5($trendType . $const . serialize($yValues) . serialize($xValues)); |
||||
|
// Determine which Trend method has been requested |
||||
|
switch ($trendType) { |
||||
|
// Instantiate and return the class for the requested Trend method |
||||
|
case self::TREND_LINEAR: |
||||
|
case self::TREND_LOGARITHMIC: |
||||
|
case self::TREND_EXPONENTIAL: |
||||
|
case self::TREND_POWER: |
||||
|
if (!isset(self::$trendCache[$key])) { |
||||
|
$className = '\PhpOffice\PhpSpreadsheet\Shared\Trend\\' . $trendType . 'BestFit'; |
||||
|
// @phpstan-ignore-next-line |
||||
|
self::$trendCache[$key] = new $className($yValues, $xValues, $const); |
||||
|
} |
||||
|
|
||||
|
return self::$trendCache[$key]; |
||||
|
case self::TREND_POLYNOMIAL_2: |
||||
|
case self::TREND_POLYNOMIAL_3: |
||||
|
case self::TREND_POLYNOMIAL_4: |
||||
|
case self::TREND_POLYNOMIAL_5: |
||||
|
case self::TREND_POLYNOMIAL_6: |
||||
|
if (!isset(self::$trendCache[$key])) { |
||||
|
$order = substr($trendType, -1); |
||||
|
self::$trendCache[$key] = new PolynomialBestFit($order, $yValues, $xValues); |
||||
|
} |
||||
|
|
||||
|
return self::$trendCache[$key]; |
||||
|
case self::TREND_BEST_FIT: |
||||
|
case self::TREND_BEST_FIT_NO_POLY: |
||||
|
// If the request is to determine the best fit regression, then we test each Trend line in turn |
||||
|
// Start by generating an instance of each available Trend method |
||||
|
$bestFit = []; |
||||
|
$bestFitValue = []; |
||||
|
foreach (self::$trendTypes as $trendMethod) { |
||||
|
$className = '\PhpOffice\PhpSpreadsheet\Shared\Trend\\' . $trendType . 'BestFit'; |
||||
|
$bestFit[$trendMethod] = new $className($yValues, $xValues, $const); |
||||
|
$bestFitValue[$trendMethod] = $bestFit[$trendMethod]->getGoodnessOfFit(); |
||||
|
} |
||||
|
if ($trendType != self::TREND_BEST_FIT_NO_POLY) { |
||||
|
foreach (self::$trendTypePolynomialOrders as $trendMethod) { |
||||
|
$order = substr($trendMethod, -1); |
||||
|
$bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues); |
||||
|
if ($bestFit[$trendMethod]->getError()) { |
||||
|
unset($bestFit[$trendMethod]); |
||||
|
} else { |
||||
|
$bestFitValue[$trendMethod] = $bestFit[$trendMethod]->getGoodnessOfFit(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// Determine which of our Trend lines is the best fit, and then we return the instance of that Trend class |
||||
|
arsort($bestFitValue); |
||||
|
$bestFitType = key($bestFitValue); |
||||
|
|
||||
|
return $bestFit[$bestFitType]; |
||||
|
default: |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared; |
||||
|
|
||||
|
class XMLWriter extends \XMLWriter |
||||
|
{ |
||||
|
public static $debugEnabled = false; |
||||
|
|
||||
|
/** Temporary storage method */ |
||||
|
const STORAGE_MEMORY = 1; |
||||
|
const STORAGE_DISK = 2; |
||||
|
|
||||
|
/** |
||||
|
* Temporary filename. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $tempFileName = ''; |
||||
|
|
||||
|
/** |
||||
|
* Create a new XMLWriter instance. |
||||
|
* |
||||
|
* @param int $temporaryStorage Temporary storage location |
||||
|
* @param string $temporaryStorageFolder Temporary storage folder |
||||
|
*/ |
||||
|
public function __construct($temporaryStorage = self::STORAGE_MEMORY, $temporaryStorageFolder = null) |
||||
|
{ |
||||
|
// Open temporary storage |
||||
|
if ($temporaryStorage == self::STORAGE_MEMORY) { |
||||
|
$this->openMemory(); |
||||
|
} else { |
||||
|
// Create temporary filename |
||||
|
if ($temporaryStorageFolder === null) { |
||||
|
$temporaryStorageFolder = File::sysGetTempDir(); |
||||
|
} |
||||
|
$this->tempFileName = @tempnam($temporaryStorageFolder, 'xml'); |
||||
|
|
||||
|
// Open storage |
||||
|
if ($this->openUri($this->tempFileName) === false) { |
||||
|
// Fallback to memory... |
||||
|
$this->openMemory(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Set default values |
||||
|
if (self::$debugEnabled) { |
||||
|
$this->setIndent(true); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Destructor. |
||||
|
*/ |
||||
|
public function __destruct() |
||||
|
{ |
||||
|
// Unlink temporary files |
||||
|
if ($this->tempFileName != '') { |
||||
|
@unlink($this->tempFileName); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get written data. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getData() |
||||
|
{ |
||||
|
if ($this->tempFileName == '') { |
||||
|
return $this->outputMemory(true); |
||||
|
} |
||||
|
$this->flush(); |
||||
|
|
||||
|
return file_get_contents($this->tempFileName); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Wrapper method for writeRaw. |
||||
|
* |
||||
|
* @param null|string|string[] $rawTextData |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function writeRawData($rawTextData) |
||||
|
{ |
||||
|
if (is_array($rawTextData)) { |
||||
|
$rawTextData = implode("\n", $rawTextData); |
||||
|
} |
||||
|
|
||||
|
return $this->writeRaw(htmlspecialchars($rawTextData ?? '')); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,277 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace PhpOffice\PhpSpreadsheet\Shared; |
||||
|
|
||||
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; |
||||
|
use PhpOffice\PhpSpreadsheet\Helper\Dimension; |
||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; |
||||
|
|
||||
|
class Xls |
||||
|
{ |
||||
|
/** |
||||
|
* Get the width of a column in pixels. We use the relationship y = ceil(7x) where |
||||
|
* x is the width in intrinsic Excel units (measuring width in number of normal characters) |
||||
|
* This holds for Arial 10. |
||||
|
* |
||||
|
* @param Worksheet $worksheet The sheet |
||||
|
* @param string $col The column |
||||
|
* |
||||
|
* @return int The width in pixels |
||||
|
*/ |
||||
|
public static function sizeCol(Worksheet $worksheet, $col = 'A') |
||||
|
{ |
||||
|
// default font of the workbook |
||||
|
$font = $worksheet->getParent()->getDefaultStyle()->getFont(); |
||||
|
|
||||
|
$columnDimensions = $worksheet->getColumnDimensions(); |
||||
|
|
||||
|
// first find the true column width in pixels (uncollapsed and unhidden) |
||||
|
if (isset($columnDimensions[$col]) && $columnDimensions[$col]->getWidth() != -1) { |
||||
|
// then we have column dimension with explicit width |
||||
|
$columnDimension = $columnDimensions[$col]; |
||||
|
$width = $columnDimension->getWidth(); |
||||
|
$pixelWidth = Drawing::cellDimensionToPixels($width, $font); |
||||
|
} elseif ($worksheet->getDefaultColumnDimension()->getWidth() != -1) { |
||||
|
// then we have default column dimension with explicit width |
||||
|
$defaultColumnDimension = $worksheet->getDefaultColumnDimension(); |
||||
|
$width = $defaultColumnDimension->getWidth(); |
||||
|
$pixelWidth = Drawing::cellDimensionToPixels($width, $font); |
||||
|
} else { |
||||
|
// we don't even have any default column dimension. Width depends on default font |
||||
|
$pixelWidth = Font::getDefaultColumnWidthByFont($font, true); |
||||
|
} |
||||
|
|
||||
|
// now find the effective column width in pixels |
||||
|
if (isset($columnDimensions[$col]) && !$columnDimensions[$col]->getVisible()) { |
||||
|
$effectivePixelWidth = 0; |
||||
|
} else { |
||||
|
$effectivePixelWidth = $pixelWidth; |
||||
|
} |
||||
|
|
||||
|
return $effectivePixelWidth; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert the height of a cell from user's units to pixels. By interpolation |
||||
|
* the relationship is: y = 4/3x. If the height hasn't been set by the user we |
||||
|
* use the default value. If the row is hidden we use a value of zero. |
||||
|
* |
||||
|
* @param Worksheet $worksheet The sheet |
||||
|
* @param int $row The row index (1-based) |
||||
|
* |
||||
|
* @return int The width in pixels |
||||
|
*/ |
||||
|
public static function sizeRow(Worksheet $worksheet, $row = 1) |
||||
|
{ |
||||
|
// default font of the workbook |
||||
|
$font = $worksheet->getParent()->getDefaultStyle()->getFont(); |
||||
|
|
||||
|
$rowDimensions = $worksheet->getRowDimensions(); |
||||
|
|
||||
|
// first find the true row height in pixels (uncollapsed and unhidden) |
||||
|
if (isset($rowDimensions[$row]) && $rowDimensions[$row]->getRowHeight() != -1) { |
||||
|
// then we have a row dimension |
||||
|
$rowDimension = $rowDimensions[$row]; |
||||
|
$rowHeight = $rowDimension->getRowHeight(); |
||||
|
$pixelRowHeight = (int) ceil(4 * $rowHeight / 3); // here we assume Arial 10 |
||||
|
} elseif ($worksheet->getDefaultRowDimension()->getRowHeight() != -1) { |
||||
|
// then we have a default row dimension with explicit height |
||||
|
$defaultRowDimension = $worksheet->getDefaultRowDimension(); |
||||
|
$pixelRowHeight = $defaultRowDimension->getRowHeight(Dimension::UOM_PIXELS); |
||||
|
} else { |
||||
|
// we don't even have any default row dimension. Height depends on default font |
||||
|
$pointRowHeight = Font::getDefaultRowHeightByFont($font); |
||||
|
$pixelRowHeight = Font::fontSizeToPixels((int) $pointRowHeight); |
||||
|
} |
||||
|
|
||||
|
// now find the effective row height in pixels |
||||
|
if (isset($rowDimensions[$row]) && !$rowDimensions[$row]->getVisible()) { |
||||
|
$effectivePixelRowHeight = 0; |
||||
|
} else { |
||||
|
$effectivePixelRowHeight = $pixelRowHeight; |
||||
|
} |
||||
|
|
||||
|
return (int) $effectivePixelRowHeight; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the horizontal distance in pixels between two anchors |
||||
|
* The distanceX is found as sum of all the spanning columns widths minus correction for the two offsets. |
||||
|
* |
||||
|
* @param string $startColumn |
||||
|
* @param int $startOffsetX Offset within start cell measured in 1/1024 of the cell width |
||||
|
* @param string $endColumn |
||||
|
* @param int $endOffsetX Offset within end cell measured in 1/1024 of the cell width |
||||
|
* |
||||
|
* @return int Horizontal measured in pixels |
||||
|
*/ |
||||
|
public static function getDistanceX(Worksheet $worksheet, $startColumn = 'A', $startOffsetX = 0, $endColumn = 'A', $endOffsetX = 0) |
||||
|
{ |
||||
|
$distanceX = 0; |
||||
|
|
||||
|
// add the widths of the spanning columns |
||||
|
$startColumnIndex = Coordinate::columnIndexFromString($startColumn); |
||||
|
$endColumnIndex = Coordinate::columnIndexFromString($endColumn); |
||||
|
for ($i = $startColumnIndex; $i <= $endColumnIndex; ++$i) { |
||||
|
$distanceX += self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($i)); |
||||
|
} |
||||
|
|
||||
|
// correct for offsetX in startcell |
||||
|
$distanceX -= (int) floor(self::sizeCol($worksheet, $startColumn) * $startOffsetX / 1024); |
||||
|
|
||||
|
// correct for offsetX in endcell |
||||
|
$distanceX -= (int) floor(self::sizeCol($worksheet, $endColumn) * (1 - $endOffsetX / 1024)); |
||||
|
|
||||
|
return $distanceX; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the vertical distance in pixels between two anchors |
||||
|
* The distanceY is found as sum of all the spanning rows minus two offsets. |
||||
|
* |
||||
|
* @param int $startRow (1-based) |
||||
|
* @param int $startOffsetY Offset within start cell measured in 1/256 of the cell height |
||||
|
* @param int $endRow (1-based) |
||||
|
* @param int $endOffsetY Offset within end cell measured in 1/256 of the cell height |
||||
|
* |
||||
|
* @return int Vertical distance measured in pixels |
||||
|
*/ |
||||
|
public static function getDistanceY(Worksheet $worksheet, $startRow = 1, $startOffsetY = 0, $endRow = 1, $endOffsetY = 0) |
||||
|
{ |
||||
|
$distanceY = 0; |
||||
|
|
||||
|
// add the widths of the spanning rows |
||||
|
for ($row = $startRow; $row <= $endRow; ++$row) { |
||||
|
$distanceY += self::sizeRow($worksheet, $row); |
||||
|
} |
||||
|
|
||||
|
// correct for offsetX in startcell |
||||
|
$distanceY -= (int) floor(self::sizeRow($worksheet, $startRow) * $startOffsetY / 256); |
||||
|
|
||||
|
// correct for offsetX in endcell |
||||
|
$distanceY -= (int) floor(self::sizeRow($worksheet, $endRow) * (1 - $endOffsetY / 256)); |
||||
|
|
||||
|
return $distanceY; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert 1-cell anchor coordinates to 2-cell anchor coordinates |
||||
|
* This function is ported from PEAR Spreadsheet_Writer_Excel with small modifications. |
||||
|
* |
||||
|
* Calculate the vertices that define the position of the image as required by |
||||
|
* the OBJ record. |
||||
|
* |
||||
|
* +------------+------------+ |
||||
|
* | A | B | |
||||
|
* +-----+------------+------------+ |
||||
|
* | |(x1,y1) | | |
||||
|
* | 1 |(A1)._______|______ | |
||||
|
* | | | | | |
||||
|
* | | | | | |
||||
|
* +-----+----| BITMAP |-----+ |
||||
|
* | | | | | |
||||
|
* | 2 | |______________. | |
||||
|
* | | | (B2)| |
||||
|
* | | | (x2,y2)| |
||||
|
* +---- +------------+------------+ |
||||
|
* |
||||
|
* Example of a bitmap that covers some of the area from cell A1 to cell B2. |
||||
|
* |
||||
|
* Based on the width and height of the bitmap we need to calculate 8 vars: |
||||
|
* $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2. |
||||
|
* The width and height of the cells are also variable and have to be taken into |
||||
|
* account. |
||||
|
* The values of $col_start and $row_start are passed in from the calling |
||||
|
* function. The values of $col_end and $row_end are calculated by subtracting |
||||
|
* the width and height of the bitmap from the width and height of the |
||||
|
* underlying cells. |
||||
|
* The vertices are expressed as a percentage of the underlying cell width as |
||||
|
* follows (rhs values are in pixels): |
||||
|
* |
||||
|
* x1 = X / W *1024 |
||||
|
* y1 = Y / H *256 |
||||
|
* x2 = (X-1) / W *1024 |
||||
|
* y2 = (Y-1) / H *256 |
||||
|
* |
||||
|
* Where: X is distance from the left side of the underlying cell |
||||
|
* Y is distance from the top of the underlying cell |
||||
|
* W is the width of the cell |
||||
|
* H is the height of the cell |
||||
|
* |
||||
|
* @param string $coordinates E.g. 'A1' |
||||
|
* @param int $offsetX Horizontal offset in pixels |
||||
|
* @param int $offsetY Vertical offset in pixels |
||||
|
* @param int $width Width in pixels |
||||
|
* @param int $height Height in pixels |
||||
|
* |
||||
|
* @return null|array |
||||
|
*/ |
||||
|
public static function oneAnchor2twoAnchor(Worksheet $worksheet, $coordinates, $offsetX, $offsetY, $width, $height) |
||||
|
{ |
||||
|
[$col_start, $row] = Coordinate::indexesFromString($coordinates); |
||||
|
$row_start = $row - 1; |
||||
|
|
||||
|
$x1 = $offsetX; |
||||
|
$y1 = $offsetY; |
||||
|
|
||||
|
// Initialise end cell to the same as the start cell |
||||
|
$col_end = $col_start; // Col containing lower right corner of object |
||||
|
$row_end = $row_start; // Row containing bottom right corner of object |
||||
|
|
||||
|
// Zero the specified offset if greater than the cell dimensions |
||||
|
if ($x1 >= self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_start))) { |
||||
|
$x1 = 0; |
||||
|
} |
||||
|
if ($y1 >= self::sizeRow($worksheet, $row_start + 1)) { |
||||
|
$y1 = 0; |
||||
|
} |
||||
|
|
||||
|
$width = $width + $x1 - 1; |
||||
|
$height = $height + $y1 - 1; |
||||
|
|
||||
|
// Subtract the underlying cell widths to find the end cell of the image |
||||
|
while ($width >= self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end))) { |
||||
|
$width -= self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end)); |
||||
|
++$col_end; |
||||
|
} |
||||
|
|
||||
|
// Subtract the underlying cell heights to find the end cell of the image |
||||
|
while ($height >= self::sizeRow($worksheet, $row_end + 1)) { |
||||
|
$height -= self::sizeRow($worksheet, $row_end + 1); |
||||
|
++$row_end; |
||||
|
} |
||||
|
|
||||
|
// Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell |
||||
|
// with zero height or width. |
||||
|
if (self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_start)) == 0) { |
||||
|
return null; |
||||
|
} |
||||
|
if (self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end)) == 0) { |
||||
|
return null; |
||||
|
} |
||||
|
if (self::sizeRow($worksheet, $row_start + 1) == 0) { |
||||
|
return null; |
||||
|
} |
||||
|
if (self::sizeRow($worksheet, $row_end + 1) == 0) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// Convert the pixel values to the percentage value expected by Excel |
||||
|
$x1 = $x1 / self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_start)) * 1024; |
||||
|
$y1 = $y1 / self::sizeRow($worksheet, $row_start + 1) * 256; |
||||
|
$x2 = ($width + 1) / self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end)) * 1024; // Distance to right side of object |
||||
|
$y2 = ($height + 1) / self::sizeRow($worksheet, $row_end + 1) * 256; // Distance to bottom of object |
||||
|
|
||||
|
$startCoordinates = Coordinate::stringFromColumnIndex($col_start) . ($row_start + 1); |
||||
|
$endCoordinates = Coordinate::stringFromColumnIndex($col_end) . ($row_end + 1); |
||||
|
|
||||
|
return [ |
||||
|
'startCoordinates' => $startCoordinates, |
||||
|
'startOffsetX' => $x1, |
||||
|
'startOffsetY' => $y1, |
||||
|
'endCoordinates' => $endCoordinates, |
||||
|
'endOffsetX' => $x2, |
||||
|
'endOffsetY' => $y2, |
||||
|
]; |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue