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