package and depencies
This commit is contained in:
@@ -8,6 +8,7 @@ class CodePage
|
||||
{
|
||||
public const DEFAULT_CODE_PAGE = 'CP1252';
|
||||
|
||||
/** @var array */
|
||||
private static $pageArray = [
|
||||
0 => 'CP1252', // CodePage is not always correctly set when the xls file was saved by Apple's Numbers program
|
||||
367 => 'ASCII', // ASCII
|
||||
@@ -56,7 +57,7 @@ class CodePage
|
||||
10010 => 'MACROMANIA', // Macintosh Romania
|
||||
10017 => 'MACUKRAINE', // Macintosh Ukraine
|
||||
10021 => 'MACTHAI', // Macintosh Thai
|
||||
10029 => 'MACCENTRALEUROPE', // Macintosh Central Europe
|
||||
10029 => ['MACCENTRALEUROPE', 'MAC-CENTRALEUROPE'], // Macintosh Central Europe
|
||||
10079 => 'MACICELAND', // Macintosh Icelandic
|
||||
10081 => 'MACTURKISH', // Macintosh Turkish
|
||||
10082 => 'MACCROATIAN', // Macintosh Croatian
|
||||
@@ -65,6 +66,7 @@ class CodePage
|
||||
//32769 => 'unsupported', // ANSI Latin I (BIFF2-BIFF3)
|
||||
65000 => 'UTF-7', // Unicode (UTF-7)
|
||||
65001 => 'UTF-8', // Unicode (UTF-8)
|
||||
99999 => ['unsupported'], // Unicode (UTF-8)
|
||||
];
|
||||
|
||||
public static function validate(string $codePage): bool
|
||||
@@ -83,7 +85,20 @@ class CodePage
|
||||
public static function numberToName(int $codePage): string
|
||||
{
|
||||
if (array_key_exists($codePage, self::$pageArray)) {
|
||||
return self::$pageArray[$codePage];
|
||||
$value = self::$pageArray[$codePage];
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $encoding) {
|
||||
if (@iconv('UTF-8', $encoding, ' ') !== false) {
|
||||
self::$pageArray[$codePage] = $encoding;
|
||||
|
||||
return $encoding;
|
||||
}
|
||||
}
|
||||
|
||||
throw new PhpSpreadsheetException("Code page $codePage not implemented on this system.");
|
||||
} else {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
if ($codePage == 720 || $codePage == 32769) {
|
||||
throw new PhpSpreadsheetException("Code page $codePage not supported."); // OEM Arabic
|
||||
|
@@ -2,12 +2,15 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\DateTime;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
|
||||
class Date
|
||||
@@ -65,17 +68,17 @@ class Date
|
||||
/**
|
||||
* Set the Excel calendar (Windows 1900 or Mac 1904).
|
||||
*
|
||||
* @param int $baseDate Excel base date (1900 or 1904)
|
||||
* @param int $baseYear Excel base date (1900 or 1904)
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
public static function setExcelCalendar($baseDate)
|
||||
public static function setExcelCalendar($baseYear)
|
||||
{
|
||||
if (
|
||||
($baseDate == self::CALENDAR_WINDOWS_1900) ||
|
||||
($baseDate == self::CALENDAR_MAC_1904)
|
||||
($baseYear == self::CALENDAR_WINDOWS_1900) ||
|
||||
($baseYear == self::CALENDAR_MAC_1904)
|
||||
) {
|
||||
self::$excelCalendar = $baseDate;
|
||||
self::$excelCalendar = $baseYear;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -96,7 +99,7 @@ class Date
|
||||
/**
|
||||
* Set the Default timezone to use for dates.
|
||||
*
|
||||
* @param DateTimeZone|string $timeZone The timezone to set for all Excel datetimestamp to PHP DateTime Object conversions
|
||||
* @param null|DateTimeZone|string $timeZone The timezone to set for all Excel datetimestamp to PHP DateTime Object conversions
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
@@ -114,29 +117,39 @@ class Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Default timezone being used for dates.
|
||||
*
|
||||
* @return DateTimeZone The timezone being used as default for Excel timestamp to PHP DateTime object
|
||||
* Return the Default timezone, or UTC if default not set.
|
||||
*/
|
||||
public static function getDefaultTimezone()
|
||||
public static function getDefaultTimezone(): DateTimeZone
|
||||
{
|
||||
if (self::$defaultTimeZone === null) {
|
||||
self::$defaultTimeZone = new DateTimeZone('UTC');
|
||||
}
|
||||
return self::$defaultTimeZone ?? new DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Default timezone, or local timezone if default is not set.
|
||||
*/
|
||||
public static function getDefaultOrLocalTimezone(): DateTimeZone
|
||||
{
|
||||
return self::$defaultTimeZone ?? new DateTimeZone(date_default_timezone_get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Default timezone even if null.
|
||||
*/
|
||||
public static function getDefaultTimezoneOrNull(): ?DateTimeZone
|
||||
{
|
||||
return self::$defaultTimeZone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a timezone.
|
||||
*
|
||||
* @param DateTimeZone|string $timeZone The timezone to validate, either as a timezone string or object
|
||||
* @param null|DateTimeZone|string $timeZone The timezone to validate, either as a timezone string or object
|
||||
*
|
||||
* @return DateTimeZone The timezone as a timezone object
|
||||
* @return ?DateTimeZone The timezone as a timezone object
|
||||
*/
|
||||
private static function validateTimeZone($timeZone)
|
||||
{
|
||||
if ($timeZone instanceof DateTimeZone) {
|
||||
if ($timeZone instanceof DateTimeZone || $timeZone === null) {
|
||||
return $timeZone;
|
||||
}
|
||||
if (in_array($timeZone, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC))) {
|
||||
@@ -146,34 +159,64 @@ class Date
|
||||
throw new PhpSpreadsheetException('Invalid timezone');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return float|int
|
||||
*/
|
||||
public static function convertIsoDate($value)
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
throw new Exception('Non-string value supplied for Iso Date conversion');
|
||||
}
|
||||
|
||||
$date = new DateTime($value);
|
||||
$dateErrors = DateTime::getLastErrors();
|
||||
|
||||
if (is_array($dateErrors) && ($dateErrors['warning_count'] > 0 || $dateErrors['error_count'] > 0)) {
|
||||
throw new Exception("Invalid string $value supplied for datatype Date");
|
||||
}
|
||||
|
||||
$newValue = SharedDate::PHPToExcel($date);
|
||||
if ($newValue === false) {
|
||||
throw new Exception("Invalid string $value supplied for datatype Date");
|
||||
}
|
||||
|
||||
if (preg_match('/^\\d\\d:\\d\\d:\\d\\d/', $value) == 1) {
|
||||
$newValue = fmod($newValue, 1.0);
|
||||
}
|
||||
|
||||
return $newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a MS serialized datetime value from Excel to a PHP Date/Time object.
|
||||
*
|
||||
* @param float|int $excelTimestamp MS Excel serialized date/time value
|
||||
* @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp,
|
||||
* if you don't want to treat it as a UTC value
|
||||
* Use the default (UST) unless you absolutely need a conversion
|
||||
* Use the default (UTC) unless you absolutely need a conversion
|
||||
*
|
||||
* @return \DateTime PHP date/time object
|
||||
* @return DateTime PHP date/time object
|
||||
*/
|
||||
public static function excelToDateTimeObject($excelTimestamp, $timeZone = null)
|
||||
{
|
||||
$timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone);
|
||||
if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) {
|
||||
if ($excelTimestamp < 1.0) {
|
||||
if ($excelTimestamp < 1 && self::$excelCalendar === self::CALENDAR_WINDOWS_1900) {
|
||||
// Unix timestamp base date
|
||||
$baseDate = new \DateTime('1970-01-01', $timeZone);
|
||||
$baseDate = new DateTime('1970-01-01', $timeZone);
|
||||
} else {
|
||||
// MS Excel calendar base dates
|
||||
if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
|
||||
// Allow adjustment for 1900 Leap Year in MS Excel
|
||||
$baseDate = ($excelTimestamp < 60) ? new \DateTime('1899-12-31', $timeZone) : new \DateTime('1899-12-30', $timeZone);
|
||||
$baseDate = ($excelTimestamp < 60) ? new DateTime('1899-12-31', $timeZone) : new DateTime('1899-12-30', $timeZone);
|
||||
} else {
|
||||
$baseDate = new \DateTime('1904-01-01', $timeZone);
|
||||
$baseDate = new DateTime('1904-01-01', $timeZone);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$baseDate = new \DateTime('1899-12-30', $timeZone);
|
||||
$baseDate = new DateTime('1899-12-30', $timeZone);
|
||||
}
|
||||
|
||||
$days = floor($excelTimestamp);
|
||||
@@ -195,11 +238,13 @@ class Date
|
||||
|
||||
/**
|
||||
* Convert a MS serialized datetime value from Excel to a unix timestamp.
|
||||
* The use of Unix timestamps, and therefore this function, is discouraged.
|
||||
* They are not Y2038-safe on a 32-bit system, and have no timezone info.
|
||||
*
|
||||
* @param float|int $excelTimestamp MS Excel serialized date/time value
|
||||
* @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp,
|
||||
* if you don't want to treat it as a UTC value
|
||||
* Use the default (UST) unless you absolutely need a conversion
|
||||
* Use the default (UTC) unless you absolutely need a conversion
|
||||
*
|
||||
* @return int Unix timetamp for this date/time
|
||||
*/
|
||||
@@ -212,9 +257,10 @@ class Date
|
||||
/**
|
||||
* Convert a date from PHP to an MS Excel serialized date/time value.
|
||||
*
|
||||
* @param mixed $dateValue Unix Timestamp or PHP DateTime object or a string
|
||||
* @param mixed $dateValue PHP DateTime object or a string - Unix timestamp is also permitted, but discouraged;
|
||||
* not Y2038-safe on a 32-bit system, and no timezone info
|
||||
*
|
||||
* @return bool|float Excel date/time value
|
||||
* @return false|float Excel date/time value
|
||||
* or boolean FALSE on failure
|
||||
*/
|
||||
public static function PHPToExcel($dateValue)
|
||||
@@ -251,18 +297,20 @@ class Date
|
||||
|
||||
/**
|
||||
* Convert a Unix timestamp to an MS Excel serialized date/time value.
|
||||
* The use of Unix timestamps, and therefore this function, is discouraged.
|
||||
* They are not Y2038-safe on a 32-bit system, and have no timezone info.
|
||||
*
|
||||
* @param int $dateValue Unix Timestamp
|
||||
* @param float|int|string $unixTimestamp Unix Timestamp
|
||||
*
|
||||
* @return float MS Excel serialized date/time value
|
||||
* @return false|float MS Excel serialized date/time value
|
||||
*/
|
||||
public static function timestampToExcel($dateValue)
|
||||
public static function timestampToExcel($unixTimestamp)
|
||||
{
|
||||
if (!is_numeric($dateValue)) {
|
||||
if (!is_numeric($unixTimestamp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::dateTimeToExcel(new \DateTime('@' . $dateValue));
|
||||
return self::dateTimeToExcel(new DateTime('@' . $unixTimestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,8 +351,8 @@ class Date
|
||||
}
|
||||
|
||||
// Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0)
|
||||
$century = substr($year, 0, 2);
|
||||
$decade = substr($year, 2, 2);
|
||||
$century = (int) substr((string) $year, 0, 2);
|
||||
$decade = (int) substr((string) $year, 2, 2);
|
||||
$excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myexcelBaseDate + $excel1900isLeapYear;
|
||||
|
||||
$excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400;
|
||||
@@ -315,16 +363,30 @@ class Date
|
||||
/**
|
||||
* Is a given cell a date/time?
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isDateTime(Cell $pCell)
|
||||
public static function isDateTime(Cell $cell, $value = null, bool $dateWithoutTimeOkay = true)
|
||||
{
|
||||
return is_numeric($pCell->getCalculatedValue()) &&
|
||||
self::isDateTimeFormat(
|
||||
$pCell->getWorksheet()->getStyle(
|
||||
$pCell->getCoordinate()
|
||||
)->getNumberFormat()
|
||||
);
|
||||
$result = false;
|
||||
$worksheet = $cell->getWorksheetOrNull();
|
||||
$spreadsheet = ($worksheet === null) ? null : $worksheet->getParent();
|
||||
if ($worksheet !== null && $spreadsheet !== null) {
|
||||
$index = $spreadsheet->getActiveSheetIndex();
|
||||
$selected = $worksheet->getSelectedCells();
|
||||
$result = is_numeric($value ?? $cell->getCalculatedValue()) &&
|
||||
self::isDateTimeFormat(
|
||||
$worksheet->getStyle(
|
||||
$cell->getCoordinate()
|
||||
)->getNumberFormat(),
|
||||
$dateWithoutTimeOkay
|
||||
);
|
||||
$worksheet->setSelectedCells($selected);
|
||||
$spreadsheet->setActiveSheetIndex($index);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,79 +394,59 @@ class Date
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isDateTimeFormat(NumberFormat $pFormat)
|
||||
public static function isDateTimeFormat(NumberFormat $excelFormatCode, bool $dateWithoutTimeOkay = true)
|
||||
{
|
||||
return self::isDateTimeFormatCode($pFormat->getFormatCode());
|
||||
return self::isDateTimeFormatCode((string) $excelFormatCode->getFormatCode(), $dateWithoutTimeOkay);
|
||||
}
|
||||
|
||||
private static $possibleDateFormatCharacters = 'eymdHs';
|
||||
private const POSSIBLE_DATETIME_FORMAT_CHARACTERS = 'eymdHs';
|
||||
private const POSSIBLE_TIME_FORMAT_CHARACTERS = 'Hs'; // note - no 'm' due to ambiguity
|
||||
|
||||
/**
|
||||
* Is a given number format code a date/time?
|
||||
*
|
||||
* @param string $pFormatCode
|
||||
* @param string $excelFormatCode
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isDateTimeFormatCode($pFormatCode)
|
||||
public static function isDateTimeFormatCode($excelFormatCode, bool $dateWithoutTimeOkay = true)
|
||||
{
|
||||
if (strtolower($pFormatCode) === strtolower(NumberFormat::FORMAT_GENERAL)) {
|
||||
if (strtolower($excelFormatCode) === strtolower(NumberFormat::FORMAT_GENERAL)) {
|
||||
// "General" contains an epoch letter 'e', so we trap for it explicitly here (case-insensitive check)
|
||||
return false;
|
||||
}
|
||||
if (preg_match('/[0#]E[+-]0/i', $pFormatCode)) {
|
||||
if (preg_match('/[0#]E[+-]0/i', $excelFormatCode)) {
|
||||
// Scientific format
|
||||
return false;
|
||||
}
|
||||
|
||||
// Switch on formatcode
|
||||
switch ($pFormatCode) {
|
||||
// Explicitly defined date formats
|
||||
case NumberFormat::FORMAT_DATE_YYYYMMDD:
|
||||
case NumberFormat::FORMAT_DATE_YYYYMMDD2:
|
||||
case NumberFormat::FORMAT_DATE_DDMMYYYY:
|
||||
case NumberFormat::FORMAT_DATE_DMYSLASH:
|
||||
case NumberFormat::FORMAT_DATE_DMYMINUS:
|
||||
case NumberFormat::FORMAT_DATE_DMMINUS:
|
||||
case NumberFormat::FORMAT_DATE_MYMINUS:
|
||||
case NumberFormat::FORMAT_DATE_DATETIME:
|
||||
case NumberFormat::FORMAT_DATE_TIME1:
|
||||
case NumberFormat::FORMAT_DATE_TIME2:
|
||||
case NumberFormat::FORMAT_DATE_TIME3:
|
||||
case NumberFormat::FORMAT_DATE_TIME4:
|
||||
case NumberFormat::FORMAT_DATE_TIME5:
|
||||
case NumberFormat::FORMAT_DATE_TIME6:
|
||||
case NumberFormat::FORMAT_DATE_TIME7:
|
||||
case NumberFormat::FORMAT_DATE_TIME8:
|
||||
case NumberFormat::FORMAT_DATE_YYYYMMDDSLASH:
|
||||
case NumberFormat::FORMAT_DATE_XLSX14:
|
||||
case NumberFormat::FORMAT_DATE_XLSX15:
|
||||
case NumberFormat::FORMAT_DATE_XLSX16:
|
||||
case NumberFormat::FORMAT_DATE_XLSX17:
|
||||
case NumberFormat::FORMAT_DATE_XLSX22:
|
||||
return true;
|
||||
if (in_array($excelFormatCode, NumberFormat::DATE_TIME_OR_DATETIME_ARRAY, true)) {
|
||||
return $dateWithoutTimeOkay || in_array($excelFormatCode, NumberFormat::TIME_OR_DATETIME_ARRAY);
|
||||
}
|
||||
|
||||
// Typically number, currency or accounting (or occasionally fraction) formats
|
||||
if ((substr($pFormatCode, 0, 1) == '_') || (substr($pFormatCode, 0, 2) == '0 ')) {
|
||||
if ((substr($excelFormatCode, 0, 1) == '_') || (substr($excelFormatCode, 0, 2) == '0 ')) {
|
||||
return false;
|
||||
}
|
||||
// Some "special formats" provided in German Excel versions were detected as date time value,
|
||||
// so filter them out here - "\C\H\-00000" (Switzerland) and "\D-00000" (Germany).
|
||||
if (\strpos($pFormatCode, '-00000') !== false) {
|
||||
if (\strpos($excelFormatCode, '-00000') !== false) {
|
||||
return false;
|
||||
}
|
||||
$possibleFormatCharacters = $dateWithoutTimeOkay ? self::POSSIBLE_DATETIME_FORMAT_CHARACTERS : self::POSSIBLE_TIME_FORMAT_CHARACTERS;
|
||||
// Try checking for any of the date formatting characters that don't appear within square braces
|
||||
if (preg_match('/(^|\])[^\[]*[' . self::$possibleDateFormatCharacters . ']/i', $pFormatCode)) {
|
||||
if (preg_match('/(^|\])[^\[]*[' . $possibleFormatCharacters . ']/i', $excelFormatCode)) {
|
||||
// We might also have a format mask containing quoted strings...
|
||||
// we don't want to test for any of our characters within the quoted blocks
|
||||
if (strpos($pFormatCode, '"') !== false) {
|
||||
if (strpos($excelFormatCode, '"') !== false) {
|
||||
$segMatcher = false;
|
||||
foreach (explode('"', $pFormatCode) as $subVal) {
|
||||
foreach (explode('"', $excelFormatCode) as $subVal) {
|
||||
// Only test in alternate array entries (the non-quoted blocks)
|
||||
$segMatcher = $segMatcher === false;
|
||||
if (
|
||||
($segMatcher = !$segMatcher) &&
|
||||
(preg_match('/(^|\])[^\[]*[' . self::$possibleDateFormatCharacters . ']/i', $subVal))
|
||||
$segMatcher &&
|
||||
(preg_match('/(^|\])[^\[]*[' . $possibleFormatCharacters . ']/i', $subVal))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -436,15 +478,15 @@ class Date
|
||||
return false;
|
||||
}
|
||||
|
||||
$dateValueNew = DateTime::DATEVALUE($dateValue);
|
||||
$dateValueNew = DateTimeExcel\DateValue::fromString($dateValue);
|
||||
|
||||
if ($dateValueNew === Functions::VALUE()) {
|
||||
if (!is_float($dateValueNew)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strpos($dateValue, ':') !== false) {
|
||||
$timeValue = DateTime::TIMEVALUE($dateValue);
|
||||
if ($timeValue === Functions::VALUE()) {
|
||||
$timeValue = DateTimeExcel\TimeValue::fromString($dateValue);
|
||||
if (!is_float($timeValue)) {
|
||||
return false;
|
||||
}
|
||||
$dateValueNew += $timeValue;
|
||||
@@ -456,21 +498,21 @@ class Date
|
||||
/**
|
||||
* Converts a month name (either a long or a short name) to a month number.
|
||||
*
|
||||
* @param string $month Month name or abbreviation
|
||||
* @param string $monthName Month name or abbreviation
|
||||
*
|
||||
* @return int|string Month number (1 - 12), or the original string argument if it isn't a valid month name
|
||||
*/
|
||||
public static function monthStringToNumber($month)
|
||||
public static function monthStringToNumber($monthName)
|
||||
{
|
||||
$monthIndex = 1;
|
||||
foreach (self::$monthNames as $shortMonthName => $longMonthName) {
|
||||
if (($month === $longMonthName) || ($month === $shortMonthName)) {
|
||||
if (($monthName === $longMonthName) || ($monthName === $shortMonthName)) {
|
||||
return $monthIndex;
|
||||
}
|
||||
++$monthIndex;
|
||||
}
|
||||
|
||||
return $month;
|
||||
return $monthName;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,4 +531,19 @@ class Date
|
||||
|
||||
return $day;
|
||||
}
|
||||
|
||||
public static function dateTimeFromTimestamp(string $date, ?DateTimeZone $timeZone = null): DateTime
|
||||
{
|
||||
$dtobj = DateTime::createFromFormat('U', $date) ?: new DateTime();
|
||||
$dtobj->setTimeZone($timeZone ?? self::getDefaultOrLocalTimezone());
|
||||
|
||||
return $dtobj;
|
||||
}
|
||||
|
||||
public static function formattedDateTimeFromTimestamp(string $date, string $format, ?DateTimeZone $timeZone = null): string
|
||||
{
|
||||
$dtobj = self::dateTimeFromTimestamp($date, $timeZone);
|
||||
|
||||
return $dtobj->format($format);
|
||||
}
|
||||
}
|
||||
|
@@ -3,32 +3,35 @@
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use GdImage;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
|
||||
use SimpleXMLElement;
|
||||
|
||||
class Drawing
|
||||
{
|
||||
/**
|
||||
* Convert pixels to EMU.
|
||||
*
|
||||
* @param int $pValue Value in pixels
|
||||
* @param int $pixelValue Value in pixels
|
||||
*
|
||||
* @return int Value in EMU
|
||||
*/
|
||||
public static function pixelsToEMU($pValue)
|
||||
public static function pixelsToEMU($pixelValue)
|
||||
{
|
||||
return round($pValue * 9525);
|
||||
return $pixelValue * 9525;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert EMU to pixels.
|
||||
*
|
||||
* @param int $pValue Value in EMU
|
||||
* @param int|SimpleXMLElement $emuValue Value in EMU
|
||||
*
|
||||
* @return int Value in pixels
|
||||
*/
|
||||
public static function EMUToPixels($pValue)
|
||||
public static function EMUToPixels($emuValue)
|
||||
{
|
||||
if ($pValue != 0) {
|
||||
return round($pValue / 9525);
|
||||
$emuValue = (int) $emuValue;
|
||||
if ($emuValue != 0) {
|
||||
return (int) round($emuValue / 9525);
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -39,50 +42,51 @@ class Drawing
|
||||
* By inspection of a real Excel file using Calibri 11, one finds 1000px ~ 142.85546875
|
||||
* This gives a conversion factor of 7. Also, we assume that pixels and font size are proportional.
|
||||
*
|
||||
* @param int $pValue Value in pixels
|
||||
* @param \PhpOffice\PhpSpreadsheet\Style\Font $pDefaultFont Default font of the workbook
|
||||
* @param int $pixelValue Value in pixels
|
||||
*
|
||||
* @return int Value in cell dimension
|
||||
* @return float|int Value in cell dimension
|
||||
*/
|
||||
public static function pixelsToCellDimension($pValue, \PhpOffice\PhpSpreadsheet\Style\Font $pDefaultFont)
|
||||
public static function pixelsToCellDimension($pixelValue, \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont)
|
||||
{
|
||||
// Font name and size
|
||||
$name = $pDefaultFont->getName();
|
||||
$size = $pDefaultFont->getSize();
|
||||
$name = $defaultFont->getName();
|
||||
$size = $defaultFont->getSize();
|
||||
|
||||
if (isset(Font::$defaultColumnWidths[$name][$size])) {
|
||||
// Exact width can be determined
|
||||
$colWidth = $pValue * Font::$defaultColumnWidths[$name][$size]['width'] / Font::$defaultColumnWidths[$name][$size]['px'];
|
||||
} else {
|
||||
// We don't have data for this particular font and size, use approximation by
|
||||
// extrapolating from Calibri 11
|
||||
$colWidth = $pValue * 11 * Font::$defaultColumnWidths['Calibri'][11]['width'] / Font::$defaultColumnWidths['Calibri'][11]['px'] / $size;
|
||||
return $pixelValue * Font::$defaultColumnWidths[$name][$size]['width']
|
||||
/ Font::$defaultColumnWidths[$name][$size]['px'];
|
||||
}
|
||||
|
||||
return $colWidth;
|
||||
// We don't have data for this particular font and size, use approximation by
|
||||
// extrapolating from Calibri 11
|
||||
return $pixelValue * 11 * Font::$defaultColumnWidths['Calibri'][11]['width']
|
||||
/ Font::$defaultColumnWidths['Calibri'][11]['px'] / $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert column width from (intrinsic) Excel units to pixels.
|
||||
*
|
||||
* @param float $pValue Value in cell dimension
|
||||
* @param \PhpOffice\PhpSpreadsheet\Style\Font $pDefaultFont Default font of the workbook
|
||||
* @param float $cellWidth Value in cell dimension
|
||||
* @param \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont Default font of the workbook
|
||||
*
|
||||
* @return int Value in pixels
|
||||
*/
|
||||
public static function cellDimensionToPixels($pValue, \PhpOffice\PhpSpreadsheet\Style\Font $pDefaultFont)
|
||||
public static function cellDimensionToPixels($cellWidth, \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont)
|
||||
{
|
||||
// Font name and size
|
||||
$name = $pDefaultFont->getName();
|
||||
$size = $pDefaultFont->getSize();
|
||||
$name = $defaultFont->getName();
|
||||
$size = $defaultFont->getSize();
|
||||
|
||||
if (isset(Font::$defaultColumnWidths[$name][$size])) {
|
||||
// Exact width can be determined
|
||||
$colWidth = $pValue * Font::$defaultColumnWidths[$name][$size]['px'] / Font::$defaultColumnWidths[$name][$size]['width'];
|
||||
$colWidth = $cellWidth * Font::$defaultColumnWidths[$name][$size]['px']
|
||||
/ Font::$defaultColumnWidths[$name][$size]['width'];
|
||||
} else {
|
||||
// We don't have data for this particular font and size, use approximation by
|
||||
// extrapolating from Calibri 11
|
||||
$colWidth = $pValue * $size * Font::$defaultColumnWidths['Calibri'][11]['px'] / Font::$defaultColumnWidths['Calibri'][11]['width'] / 11;
|
||||
$colWidth = $cellWidth * $size * Font::$defaultColumnWidths['Calibri'][11]['px']
|
||||
/ Font::$defaultColumnWidths['Calibri'][11]['width'] / 11;
|
||||
}
|
||||
|
||||
// Round pixels to closest integer
|
||||
@@ -94,26 +98,26 @@ class Drawing
|
||||
/**
|
||||
* Convert pixels to points.
|
||||
*
|
||||
* @param int $pValue Value in pixels
|
||||
* @param int $pixelValue Value in pixels
|
||||
*
|
||||
* @return float Value in points
|
||||
*/
|
||||
public static function pixelsToPoints($pValue)
|
||||
public static function pixelsToPoints($pixelValue)
|
||||
{
|
||||
return $pValue * 0.75;
|
||||
return $pixelValue * 0.75;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert points to pixels.
|
||||
*
|
||||
* @param int $pValue Value in points
|
||||
* @param int $pointValue Value in points
|
||||
*
|
||||
* @return int Value in pixels
|
||||
*/
|
||||
public static function pointsToPixels($pValue)
|
||||
public static function pointsToPixels($pointValue)
|
||||
{
|
||||
if ($pValue != 0) {
|
||||
return (int) ceil($pValue / 0.75);
|
||||
if ($pointValue != 0) {
|
||||
return (int) ceil($pointValue / 0.75);
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -122,26 +126,27 @@ class Drawing
|
||||
/**
|
||||
* Convert degrees to angle.
|
||||
*
|
||||
* @param int $pValue Degrees
|
||||
* @param int $degrees Degrees
|
||||
*
|
||||
* @return int Angle
|
||||
*/
|
||||
public static function degreesToAngle($pValue)
|
||||
public static function degreesToAngle($degrees)
|
||||
{
|
||||
return (int) round($pValue * 60000);
|
||||
return (int) round($degrees * 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert angle to degrees.
|
||||
*
|
||||
* @param int $pValue Angle
|
||||
* @param int|SimpleXMLElement $angle Angle
|
||||
*
|
||||
* @return int Degrees
|
||||
*/
|
||||
public static function angleToDegrees($pValue)
|
||||
public static function angleToDegrees($angle)
|
||||
{
|
||||
if ($pValue != 0) {
|
||||
return round($pValue / 60000);
|
||||
$angle = (int) $angle;
|
||||
if ($angle != 0) {
|
||||
return (int) round($angle / 60000);
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -152,100 +157,21 @@ class Drawing
|
||||
*
|
||||
* @see http://www.php.net/manual/en/function.imagecreatefromwbmp.php#86214
|
||||
*
|
||||
* @param string $p_sFile Path to Windows DIB (BMP) image
|
||||
* @param string $bmpFilename Path to Windows DIB (BMP) image
|
||||
*
|
||||
* @return GdImage|resource
|
||||
*
|
||||
* @deprecated 1.26 use Php function imagecreatefrombmp instead
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function imagecreatefrombmp($p_sFile)
|
||||
public static function imagecreatefrombmp($bmpFilename)
|
||||
{
|
||||
// Load the image into a string
|
||||
$file = fopen($p_sFile, 'rb');
|
||||
$read = fread($file, 10);
|
||||
while (!feof($file) && ($read != '')) {
|
||||
$read .= fread($file, 1024);
|
||||
$retVal = @imagecreatefrombmp($bmpFilename);
|
||||
if ($retVal === false) {
|
||||
throw new ReaderException("Unable to create image from $bmpFilename");
|
||||
}
|
||||
|
||||
$temp = unpack('H*', $read);
|
||||
$hex = $temp[1];
|
||||
$header = substr($hex, 0, 108);
|
||||
|
||||
// Process the header
|
||||
// Structure: http://www.fastgraph.com/help/bmp_header_format.html
|
||||
if (substr($header, 0, 4) == '424d') {
|
||||
// Cut it in parts of 2 bytes
|
||||
$header_parts = str_split($header, 2);
|
||||
|
||||
// Get the width 4 bytes
|
||||
$width = hexdec($header_parts[19] . $header_parts[18]);
|
||||
|
||||
// Get the height 4 bytes
|
||||
$height = hexdec($header_parts[23] . $header_parts[22]);
|
||||
|
||||
// Unset the header params
|
||||
unset($header_parts);
|
||||
}
|
||||
|
||||
// Define starting X and Y
|
||||
$x = 0;
|
||||
$y = 1;
|
||||
|
||||
// Create newimage
|
||||
$image = imagecreatetruecolor($width, $height);
|
||||
|
||||
// Grab the body from the image
|
||||
$body = substr($hex, 108);
|
||||
|
||||
// Calculate if padding at the end-line is needed
|
||||
// Divided by two to keep overview.
|
||||
// 1 byte = 2 HEX-chars
|
||||
$body_size = (strlen($body) / 2);
|
||||
$header_size = ($width * $height);
|
||||
|
||||
// Use end-line padding? Only when needed
|
||||
$usePadding = ($body_size > ($header_size * 3) + 4);
|
||||
|
||||
// Using a for-loop with index-calculation instaid of str_split to avoid large memory consumption
|
||||
// Calculate the next DWORD-position in the body
|
||||
for ($i = 0; $i < $body_size; $i += 3) {
|
||||
// Calculate line-ending and padding
|
||||
if ($x >= $width) {
|
||||
// If padding needed, ignore image-padding
|
||||
// Shift i to the ending of the current 32-bit-block
|
||||
if ($usePadding) {
|
||||
$i += $width % 4;
|
||||
}
|
||||
|
||||
// Reset horizontal position
|
||||
$x = 0;
|
||||
|
||||
// Raise the height-position (bottom-up)
|
||||
++$y;
|
||||
|
||||
// Reached the image-height? Break the for-loop
|
||||
if ($y > $height) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculation of the RGB-pixel (defined as BGR in image-data)
|
||||
// Define $i_pos as absolute position in the body
|
||||
$i_pos = $i * 2;
|
||||
$r = hexdec($body[$i_pos + 4] . $body[$i_pos + 5]);
|
||||
$g = hexdec($body[$i_pos + 2] . $body[$i_pos + 3]);
|
||||
$b = hexdec($body[$i_pos] . $body[$i_pos + 1]);
|
||||
|
||||
// Calculate and draw the pixel
|
||||
$color = imagecolorallocate($image, $r, $g, $b);
|
||||
imagesetpixel($image, $x, $height - $y, $color);
|
||||
|
||||
// Raise the horizontal position
|
||||
++$x;
|
||||
}
|
||||
|
||||
// Unset the body / free the memory
|
||||
unset($body);
|
||||
|
||||
// Return image-object
|
||||
return $image;
|
||||
return $retVal;
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ class SpgrContainer
|
||||
/**
|
||||
* Parent Shape Group Container.
|
||||
*
|
||||
* @var \PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer
|
||||
* @var null|SpgrContainer
|
||||
*/
|
||||
private $parent;
|
||||
|
||||
@@ -20,20 +20,16 @@ class SpgrContainer
|
||||
|
||||
/**
|
||||
* Set parent Shape Group Container.
|
||||
*
|
||||
* @param \PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer $parent
|
||||
*/
|
||||
public function setParent($parent): void
|
||||
public function setParent(?self $parent): void
|
||||
{
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent Shape Group Container if any.
|
||||
*
|
||||
* @return null|\PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer
|
||||
*/
|
||||
public function getParent()
|
||||
public function getParent(): ?self
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
@@ -166,10 +166,10 @@ class DggContainer
|
||||
/**
|
||||
* Set identifier clusters. [<drawingId> => <max shape id>, ...].
|
||||
*
|
||||
* @param array $pValue
|
||||
* @param array $IDCLs
|
||||
*/
|
||||
public function setIDCLs($pValue): void
|
||||
public function setIDCLs($IDCLs): void
|
||||
{
|
||||
$this->IDCLs = $pValue;
|
||||
$this->IDCLs = $IDCLs;
|
||||
}
|
||||
}
|
||||
|
@@ -7,16 +7,14 @@ class BstoreContainer
|
||||
/**
|
||||
* BLIP Store Entries. Each of them holds one BLIP (Big Large Image or Picture).
|
||||
*
|
||||
* @var array
|
||||
* @var BstoreContainer\BSE[]
|
||||
*/
|
||||
private $BSECollection = [];
|
||||
|
||||
/**
|
||||
* Add a BLIP Store Entry.
|
||||
*
|
||||
* @param BstoreContainer\BSE $BSE
|
||||
*/
|
||||
public function addBSE($BSE): void
|
||||
public function addBSE(BstoreContainer\BSE $BSE): void
|
||||
{
|
||||
$this->BSECollection[] = $BSE;
|
||||
$BSE->setParent($this);
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
|
||||
|
||||
class BSE
|
||||
{
|
||||
const BLIPTYPE_ERROR = 0x00;
|
||||
@@ -18,7 +20,7 @@ class BSE
|
||||
/**
|
||||
* The parent BLIP Store Entry Container.
|
||||
*
|
||||
* @var \PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer
|
||||
* @var BstoreContainer
|
||||
*/
|
||||
private $parent;
|
||||
|
||||
@@ -38,10 +40,8 @@ class BSE
|
||||
|
||||
/**
|
||||
* Set parent BLIP Store Entry Container.
|
||||
*
|
||||
* @param \PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer $parent
|
||||
*/
|
||||
public function setParent($parent): void
|
||||
public function setParent(BstoreContainer $parent): void
|
||||
{
|
||||
$this->parent = $parent;
|
||||
}
|
||||
@@ -58,10 +58,8 @@ class BSE
|
||||
|
||||
/**
|
||||
* Set the BLIP.
|
||||
*
|
||||
* @param BSE\Blip $blip
|
||||
*/
|
||||
public function setBlip($blip): void
|
||||
public function setBlip(BSE\Blip $blip): void
|
||||
{
|
||||
$this->blip = $blip;
|
||||
$blip->setParent($this);
|
||||
|
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
|
||||
|
||||
class Blip
|
||||
{
|
||||
/**
|
||||
* The parent BSE.
|
||||
*
|
||||
* @var \PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE
|
||||
* @var BSE
|
||||
*/
|
||||
private $parent;
|
||||
|
||||
@@ -40,20 +42,16 @@ class Blip
|
||||
|
||||
/**
|
||||
* Set parent BSE.
|
||||
*
|
||||
* @param \PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE $parent
|
||||
*/
|
||||
public function setParent($parent): void
|
||||
public function setParent(BSE $parent): void
|
||||
{
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parent BSE.
|
||||
*
|
||||
* @return \PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE $parent
|
||||
*/
|
||||
public function getParent()
|
||||
public function getParent(): BSE
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
@@ -2,7 +2,8 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use PhpOffice\PhpSpreadsheet\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
|
||||
use ZipArchive;
|
||||
|
||||
class File
|
||||
@@ -16,75 +17,81 @@ class File
|
||||
|
||||
/**
|
||||
* Set the flag indicating whether the File Upload Temp directory should be used for temporary files.
|
||||
*
|
||||
* @param bool $useUploadTempDir Use File Upload Temporary directory (true or false)
|
||||
*/
|
||||
public static function setUseUploadTempDirectory($useUploadTempDir): void
|
||||
public static function setUseUploadTempDirectory(bool $useUploadTempDir): void
|
||||
{
|
||||
self::$useUploadTempDirectory = (bool) $useUploadTempDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the flag indicating whether the File Upload Temp directory should be used for temporary files.
|
||||
*
|
||||
* @return bool Use File Upload Temporary directory (true or false)
|
||||
*/
|
||||
public static function getUseUploadTempDirectory()
|
||||
public static function getUseUploadTempDirectory(): bool
|
||||
{
|
||||
return self::$useUploadTempDirectory;
|
||||
}
|
||||
|
||||
// https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
||||
// Section 4.3.7
|
||||
// Looks like there might be endian-ness considerations
|
||||
private const ZIP_FIRST_4 = [
|
||||
"\x50\x4b\x03\x04", // what it looks like on my system
|
||||
"\x04\x03\x4b\x50", // what it says in documentation
|
||||
];
|
||||
|
||||
private static function validateZipFirst4(string $zipFile): bool
|
||||
{
|
||||
$contents = @file_get_contents($zipFile, false, null, 0, 4);
|
||||
|
||||
return in_array($contents, self::ZIP_FIRST_4, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a file exists.
|
||||
*
|
||||
* @param string $pFilename Filename
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function fileExists($pFilename)
|
||||
public static function fileExists(string $filename): bool
|
||||
{
|
||||
// Sick construction, but it seems that
|
||||
// file_exists returns strange values when
|
||||
// doing the original file_exists on ZIP archives...
|
||||
if (strtolower(substr($pFilename, 0, 3)) == 'zip') {
|
||||
if (strtolower(substr($filename, 0, 6)) == 'zip://') {
|
||||
// Open ZIP file and verify if the file exists
|
||||
$zipFile = substr($pFilename, 6, strpos($pFilename, '#') - 6);
|
||||
$archiveFile = substr($pFilename, strpos($pFilename, '#') + 1);
|
||||
$zipFile = substr($filename, 6, strrpos($filename, '#') - 6);
|
||||
$archiveFile = substr($filename, strrpos($filename, '#') + 1);
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipFile) === true) {
|
||||
$returnValue = ($zip->getFromName($archiveFile) !== false);
|
||||
$zip->close();
|
||||
if (self::validateZipFirst4($zipFile)) {
|
||||
$zip = new ZipArchive();
|
||||
$res = $zip->open($zipFile);
|
||||
if ($res === true) {
|
||||
$returnValue = ($zip->getFromName($archiveFile) !== false);
|
||||
$zip->close();
|
||||
|
||||
return $returnValue;
|
||||
return $returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return file_exists($pFilename);
|
||||
return file_exists($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns canonicalized absolute pathname, also for ZIP archives.
|
||||
*
|
||||
* @param string $pFilename
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function realpath($pFilename)
|
||||
public static function realpath(string $filename): string
|
||||
{
|
||||
// Returnvalue
|
||||
$returnValue = '';
|
||||
|
||||
// Try using realpath()
|
||||
if (file_exists($pFilename)) {
|
||||
$returnValue = realpath($pFilename);
|
||||
if (file_exists($filename)) {
|
||||
$returnValue = realpath($filename) ?: '';
|
||||
}
|
||||
|
||||
// Found something?
|
||||
if ($returnValue == '' || ($returnValue === null)) {
|
||||
$pathArray = explode('/', $pFilename);
|
||||
if ($returnValue === '') {
|
||||
$pathArray = explode('/', $filename);
|
||||
while (in_array('..', $pathArray) && $pathArray[0] != '..') {
|
||||
$iMax = count($pathArray);
|
||||
for ($i = 0; $i < $iMax; ++$i) {
|
||||
@@ -104,39 +111,75 @@ class File
|
||||
|
||||
/**
|
||||
* Get the systems temporary directory.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sysGetTempDir()
|
||||
public static function sysGetTempDir(): string
|
||||
{
|
||||
$path = sys_get_temp_dir();
|
||||
if (self::$useUploadTempDirectory) {
|
||||
// use upload-directory when defined to allow running on environments having very restricted
|
||||
// open_basedir configs
|
||||
if (ini_get('upload_tmp_dir') !== false) {
|
||||
if ($temp = ini_get('upload_tmp_dir')) {
|
||||
if (file_exists($temp)) {
|
||||
return realpath($temp);
|
||||
$path = $temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return realpath(sys_get_temp_dir());
|
||||
return realpath($path) ?: '';
|
||||
}
|
||||
|
||||
public static function temporaryFilename(): string
|
||||
{
|
||||
$filename = tempnam(self::sysGetTempDir(), 'phpspreadsheet');
|
||||
if ($filename === false) {
|
||||
throw new Exception('Could not create temporary file');
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that given path is an existing file and is readable, otherwise throw exception.
|
||||
*
|
||||
* @param string $filename
|
||||
*/
|
||||
public static function assertFile($filename): void
|
||||
public static function assertFile(string $filename, string $zipMember = ''): void
|
||||
{
|
||||
if (!is_file($filename)) {
|
||||
throw new InvalidArgumentException('File "' . $filename . '" does not exist.');
|
||||
throw new ReaderException('File "' . $filename . '" does not exist.');
|
||||
}
|
||||
|
||||
if (!is_readable($filename)) {
|
||||
throw new InvalidArgumentException('Could not open "' . $filename . '" for reading.');
|
||||
throw new ReaderException('Could not open "' . $filename . '" for reading.');
|
||||
}
|
||||
|
||||
if ($zipMember !== '') {
|
||||
$zipfile = "zip://$filename#$zipMember";
|
||||
if (!self::fileExists($zipfile)) {
|
||||
throw new ReaderException("Could not find zip member $zipfile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as assertFile, except return true/false and don't throw Exception.
|
||||
*/
|
||||
public static function testFileNoThrow(string $filename, ?string $zipMember = null): bool
|
||||
{
|
||||
if (!is_file($filename)) {
|
||||
return false;
|
||||
}
|
||||
if (!is_readable($filename)) {
|
||||
return false;
|
||||
}
|
||||
if ($zipMember === null) {
|
||||
return true;
|
||||
}
|
||||
// validate zip, but don't check specific member
|
||||
if ($zipMember === '') {
|
||||
return self::validateZipFirst4($filename);
|
||||
}
|
||||
|
||||
return self::fileExists("zip://$filename#$zipMember");
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
|
||||
use PhpOffice\PhpSpreadsheet\RichText\RichText;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Font as FontStyle;
|
||||
|
||||
class Font
|
||||
{
|
||||
@@ -11,7 +13,7 @@ class Font
|
||||
const AUTOSIZE_METHOD_APPROX = 'approx';
|
||||
const AUTOSIZE_METHOD_EXACT = 'exact';
|
||||
|
||||
private static $autoSizeMethods = [
|
||||
private const AUTOSIZE_METHODS = [
|
||||
self::AUTOSIZE_METHOD_APPROX,
|
||||
self::AUTOSIZE_METHOD_EXACT,
|
||||
];
|
||||
@@ -44,10 +46,10 @@ class Font
|
||||
const ARIAL_ITALIC = 'ariali.ttf';
|
||||
const ARIAL_BOLD_ITALIC = 'arialbi.ttf';
|
||||
|
||||
const CALIBRI = 'CALIBRI.TTF';
|
||||
const CALIBRI_BOLD = 'CALIBRIB.TTF';
|
||||
const CALIBRI_ITALIC = 'CALIBRII.TTF';
|
||||
const CALIBRI_BOLD_ITALIC = 'CALIBRIZ.TTF';
|
||||
const CALIBRI = 'calibri.ttf';
|
||||
const CALIBRI_BOLD = 'calibrib.ttf';
|
||||
const CALIBRI_ITALIC = 'calibrii.ttf';
|
||||
const CALIBRI_BOLD_ITALIC = 'calibriz.ttf';
|
||||
|
||||
const COMIC_SANS_MS = 'comic.ttf';
|
||||
const COMIC_SANS_MS_BOLD = 'comicbd.ttf';
|
||||
@@ -99,6 +101,105 @@ class Font
|
||||
const VERDANA_ITALIC = 'verdanai.ttf';
|
||||
const VERDANA_BOLD_ITALIC = 'verdanaz.ttf';
|
||||
|
||||
const FONT_FILE_NAMES = [
|
||||
'Arial' => [
|
||||
'x' => self::ARIAL,
|
||||
'xb' => self::ARIAL_BOLD,
|
||||
'xi' => self::ARIAL_ITALIC,
|
||||
'xbi' => self::ARIAL_BOLD_ITALIC,
|
||||
],
|
||||
'Calibri' => [
|
||||
'x' => self::CALIBRI,
|
||||
'xb' => self::CALIBRI_BOLD,
|
||||
'xi' => self::CALIBRI_ITALIC,
|
||||
'xbi' => self::CALIBRI_BOLD_ITALIC,
|
||||
],
|
||||
'Comic Sans MS' => [
|
||||
'x' => self::COMIC_SANS_MS,
|
||||
'xb' => self::COMIC_SANS_MS_BOLD,
|
||||
'xi' => self::COMIC_SANS_MS,
|
||||
'xbi' => self::COMIC_SANS_MS_BOLD,
|
||||
],
|
||||
'Courier New' => [
|
||||
'x' => self::COURIER_NEW,
|
||||
'xb' => self::COURIER_NEW_BOLD,
|
||||
'xi' => self::COURIER_NEW_ITALIC,
|
||||
'xbi' => self::COURIER_NEW_BOLD_ITALIC,
|
||||
],
|
||||
'Georgia' => [
|
||||
'x' => self::GEORGIA,
|
||||
'xb' => self::GEORGIA_BOLD,
|
||||
'xi' => self::GEORGIA_ITALIC,
|
||||
'xbi' => self::GEORGIA_BOLD_ITALIC,
|
||||
],
|
||||
'Impact' => [
|
||||
'x' => self::IMPACT,
|
||||
'xb' => self::IMPACT,
|
||||
'xi' => self::IMPACT,
|
||||
'xbi' => self::IMPACT,
|
||||
],
|
||||
'Liberation Sans' => [
|
||||
'x' => self::LIBERATION_SANS,
|
||||
'xb' => self::LIBERATION_SANS_BOLD,
|
||||
'xi' => self::LIBERATION_SANS_ITALIC,
|
||||
'xbi' => self::LIBERATION_SANS_BOLD_ITALIC,
|
||||
],
|
||||
'Lucida Console' => [
|
||||
'x' => self::LUCIDA_CONSOLE,
|
||||
'xb' => self::LUCIDA_CONSOLE,
|
||||
'xi' => self::LUCIDA_CONSOLE,
|
||||
'xbi' => self::LUCIDA_CONSOLE,
|
||||
],
|
||||
'Lucida Sans Unicode' => [
|
||||
'x' => self::LUCIDA_SANS_UNICODE,
|
||||
'xb' => self::LUCIDA_SANS_UNICODE,
|
||||
'xi' => self::LUCIDA_SANS_UNICODE,
|
||||
'xbi' => self::LUCIDA_SANS_UNICODE,
|
||||
],
|
||||
'Microsoft Sans Serif' => [
|
||||
'x' => self::MICROSOFT_SANS_SERIF,
|
||||
'xb' => self::MICROSOFT_SANS_SERIF,
|
||||
'xi' => self::MICROSOFT_SANS_SERIF,
|
||||
'xbi' => self::MICROSOFT_SANS_SERIF,
|
||||
],
|
||||
'Palatino Linotype' => [
|
||||
'x' => self::PALATINO_LINOTYPE,
|
||||
'xb' => self::PALATINO_LINOTYPE_BOLD,
|
||||
'xi' => self::PALATINO_LINOTYPE_ITALIC,
|
||||
'xbi' => self::PALATINO_LINOTYPE_BOLD_ITALIC,
|
||||
],
|
||||
'Symbol' => [
|
||||
'x' => self::SYMBOL,
|
||||
'xb' => self::SYMBOL,
|
||||
'xi' => self::SYMBOL,
|
||||
'xbi' => self::SYMBOL,
|
||||
],
|
||||
'Tahoma' => [
|
||||
'x' => self::TAHOMA,
|
||||
'xb' => self::TAHOMA_BOLD,
|
||||
'xi' => self::TAHOMA,
|
||||
'xbi' => self::TAHOMA_BOLD,
|
||||
],
|
||||
'Times New Roman' => [
|
||||
'x' => self::TIMES_NEW_ROMAN,
|
||||
'xb' => self::TIMES_NEW_ROMAN_BOLD,
|
||||
'xi' => self::TIMES_NEW_ROMAN_ITALIC,
|
||||
'xbi' => self::TIMES_NEW_ROMAN_BOLD_ITALIC,
|
||||
],
|
||||
'Trebuchet MS' => [
|
||||
'x' => self::TREBUCHET_MS,
|
||||
'xb' => self::TREBUCHET_MS_BOLD,
|
||||
'xi' => self::TREBUCHET_MS_ITALIC,
|
||||
'xbi' => self::TREBUCHET_MS_BOLD_ITALIC,
|
||||
],
|
||||
'Verdana' => [
|
||||
'x' => self::VERDANA,
|
||||
'xb' => self::VERDANA_BOLD,
|
||||
'xi' => self::VERDANA_ITALIC,
|
||||
'xbi' => self::VERDANA_BOLD_ITALIC,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* AutoSize method.
|
||||
*
|
||||
@@ -111,68 +212,79 @@ class Font
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $trueTypeFontPath = null;
|
||||
private static $trueTypeFontPath = '';
|
||||
|
||||
/**
|
||||
* How wide is a default column for a given default font and size?
|
||||
* Empirical data found by inspecting real Excel files and reading off the pixel width
|
||||
* in Microsoft Office Excel 2007.
|
||||
*
|
||||
* @var array
|
||||
* Added height in points.
|
||||
*/
|
||||
public static $defaultColumnWidths = [
|
||||
public const DEFAULT_COLUMN_WIDTHS = [
|
||||
'Arial' => [
|
||||
1 => ['px' => 24, 'width' => 12.00000000],
|
||||
2 => ['px' => 24, 'width' => 12.00000000],
|
||||
3 => ['px' => 32, 'width' => 10.66406250],
|
||||
4 => ['px' => 32, 'width' => 10.66406250],
|
||||
5 => ['px' => 40, 'width' => 10.00000000],
|
||||
6 => ['px' => 48, 'width' => 9.59765625],
|
||||
7 => ['px' => 48, 'width' => 9.59765625],
|
||||
8 => ['px' => 56, 'width' => 9.33203125],
|
||||
9 => ['px' => 64, 'width' => 9.14062500],
|
||||
10 => ['px' => 64, 'width' => 9.14062500],
|
||||
1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25],
|
||||
2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25],
|
||||
3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0],
|
||||
|
||||
4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75],
|
||||
5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25],
|
||||
6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25],
|
||||
7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0],
|
||||
8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25],
|
||||
9 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.0],
|
||||
10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75],
|
||||
],
|
||||
'Calibri' => [
|
||||
1 => ['px' => 24, 'width' => 12.00000000],
|
||||
2 => ['px' => 24, 'width' => 12.00000000],
|
||||
3 => ['px' => 32, 'width' => 10.66406250],
|
||||
4 => ['px' => 32, 'width' => 10.66406250],
|
||||
5 => ['px' => 40, 'width' => 10.00000000],
|
||||
6 => ['px' => 48, 'width' => 9.59765625],
|
||||
7 => ['px' => 48, 'width' => 9.59765625],
|
||||
8 => ['px' => 56, 'width' => 9.33203125],
|
||||
9 => ['px' => 56, 'width' => 9.33203125],
|
||||
10 => ['px' => 64, 'width' => 9.14062500],
|
||||
11 => ['px' => 64, 'width' => 9.14062500],
|
||||
1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25],
|
||||
2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25],
|
||||
3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.00],
|
||||
4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75],
|
||||
5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25],
|
||||
6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25],
|
||||
7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0],
|
||||
8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25],
|
||||
9 => ['px' => 56, 'width' => 9.33203125, 'height' => 12.0],
|
||||
10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75],
|
||||
11 => ['px' => 64, 'width' => 9.14062500, 'height' => 15.0],
|
||||
],
|
||||
'Verdana' => [
|
||||
1 => ['px' => 24, 'width' => 12.00000000],
|
||||
2 => ['px' => 24, 'width' => 12.00000000],
|
||||
3 => ['px' => 32, 'width' => 10.66406250],
|
||||
4 => ['px' => 32, 'width' => 10.66406250],
|
||||
5 => ['px' => 40, 'width' => 10.00000000],
|
||||
6 => ['px' => 48, 'width' => 9.59765625],
|
||||
7 => ['px' => 48, 'width' => 9.59765625],
|
||||
8 => ['px' => 64, 'width' => 9.14062500],
|
||||
9 => ['px' => 72, 'width' => 9.00000000],
|
||||
10 => ['px' => 72, 'width' => 9.00000000],
|
||||
1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25],
|
||||
2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25],
|
||||
3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0],
|
||||
4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75],
|
||||
5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25],
|
||||
6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25],
|
||||
7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0],
|
||||
8 => ['px' => 64, 'width' => 9.14062500, 'height' => 10.5],
|
||||
9 => ['px' => 72, 'width' => 9.00000000, 'height' => 11.25],
|
||||
10 => ['px' => 72, 'width' => 9.00000000, 'height' => 12.75],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* List of column widths. Replaced by constant;
|
||||
* previously it was public and updateable, allowing
|
||||
* user to make inappropriate alterations.
|
||||
*
|
||||
* @deprecated 1.25.0 Use DEFAULT_COLUMN_WIDTHS constant instead.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $defaultColumnWidths = self::DEFAULT_COLUMN_WIDTHS;
|
||||
|
||||
/**
|
||||
* Set autoSize method.
|
||||
*
|
||||
* @param string $pValue see self::AUTOSIZE_METHOD_*
|
||||
* @param string $method see self::AUTOSIZE_METHOD_*
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
public static function setAutoSizeMethod($pValue)
|
||||
public static function setAutoSizeMethod($method)
|
||||
{
|
||||
if (!in_array($pValue, self::$autoSizeMethods)) {
|
||||
if (!in_array($method, self::AUTOSIZE_METHODS)) {
|
||||
return false;
|
||||
}
|
||||
self::$autoSizeMethod = $pValue;
|
||||
self::$autoSizeMethod = $method;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -196,11 +308,11 @@ class Font
|
||||
* <li>~/.fonts/</li>
|
||||
* </ul>.
|
||||
*
|
||||
* @param string $pValue
|
||||
* @param string $folderPath
|
||||
*/
|
||||
public static function setTrueTypeFontPath($pValue): void
|
||||
public static function setTrueTypeFontPath($folderPath): void
|
||||
{
|
||||
self::$trueTypeFontPath = $pValue;
|
||||
self::$trueTypeFontPath = $folderPath;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,35 +328,48 @@ class Font
|
||||
/**
|
||||
* Calculate an (approximate) OpenXML column width, based on font size and text contained.
|
||||
*
|
||||
* @param \PhpOffice\PhpSpreadsheet\Style\Font $font Font object
|
||||
* @param RichText|string $cellText Text to calculate width
|
||||
* @param FontStyle $font Font object
|
||||
* @param null|RichText|string $cellText Text to calculate width
|
||||
* @param int $rotation Rotation angle
|
||||
* @param null|\PhpOffice\PhpSpreadsheet\Style\Font $defaultFont Font object
|
||||
*
|
||||
* @return int Column width
|
||||
* @param null|FontStyle $defaultFont Font object
|
||||
* @param bool $filterAdjustment Add space for Autofilter or Table dropdown
|
||||
*/
|
||||
public static function calculateColumnWidth(\PhpOffice\PhpSpreadsheet\Style\Font $font, $cellText = '', $rotation = 0, ?\PhpOffice\PhpSpreadsheet\Style\Font $defaultFont = null)
|
||||
{
|
||||
public static function calculateColumnWidth(
|
||||
FontStyle $font,
|
||||
$cellText = '',
|
||||
$rotation = 0,
|
||||
?FontStyle $defaultFont = null,
|
||||
bool $filterAdjustment = false,
|
||||
int $indentAdjustment = 0
|
||||
): int {
|
||||
// If it is rich text, use plain text
|
||||
if ($cellText instanceof RichText) {
|
||||
$cellText = $cellText->getPlainText();
|
||||
}
|
||||
|
||||
// Special case if there are one or more newline characters ("\n")
|
||||
$cellText = (string) $cellText;
|
||||
if (strpos($cellText, "\n") !== false) {
|
||||
$lineTexts = explode("\n", $cellText);
|
||||
$lineWidths = [];
|
||||
foreach ($lineTexts as $lineText) {
|
||||
$lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont);
|
||||
$lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont, $filterAdjustment);
|
||||
}
|
||||
|
||||
return max($lineWidths); // width of longest line in cell
|
||||
}
|
||||
|
||||
// Try to get the exact text width in pixels
|
||||
$approximate = self::$autoSizeMethod == self::AUTOSIZE_METHOD_APPROX;
|
||||
$approximate = self::$autoSizeMethod === self::AUTOSIZE_METHOD_APPROX;
|
||||
$columnWidth = 0;
|
||||
if (!$approximate) {
|
||||
$columnWidthAdjust = ceil(self::getTextWidthPixelsExact('n', $font, 0) * 1.07);
|
||||
$columnWidthAdjust = ceil(
|
||||
self::getTextWidthPixelsExact(
|
||||
str_repeat('n', 1 * (($filterAdjustment ? 3 : 1) + ($indentAdjustment * 2))),
|
||||
$font,
|
||||
0
|
||||
) * 1.07
|
||||
);
|
||||
|
||||
try {
|
||||
// Width of text in pixels excl. padding
|
||||
@@ -256,29 +381,27 @@ class Font
|
||||
}
|
||||
|
||||
if ($approximate) {
|
||||
$columnWidthAdjust = self::getTextWidthPixelsApprox('n', $font, 0);
|
||||
$columnWidthAdjust = self::getTextWidthPixelsApprox(
|
||||
str_repeat('n', 1 * (($filterAdjustment ? 3 : 1) + ($indentAdjustment * 2))),
|
||||
$font,
|
||||
0
|
||||
);
|
||||
// Width of text in pixels excl. padding, approximation
|
||||
// and addition because Excel adds some padding, just use approx width of 'n' glyph
|
||||
$columnWidth = self::getTextWidthPixelsApprox($cellText, $font, $rotation) + $columnWidthAdjust;
|
||||
}
|
||||
|
||||
// Convert from pixel width to column width
|
||||
$columnWidth = Drawing::pixelsToCellDimension($columnWidth, $defaultFont);
|
||||
$columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont ?? new FontStyle());
|
||||
|
||||
// Return
|
||||
return round($columnWidth, 6);
|
||||
return (int) round($columnWidth, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GD text width in pixels for a string of text in a certain font at a certain rotation angle.
|
||||
*
|
||||
* @param string $text
|
||||
* @param \PhpOffice\PhpSpreadsheet\Style\Font
|
||||
* @param int $rotation
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getTextWidthPixelsExact($text, \PhpOffice\PhpSpreadsheet\Style\Font $font, $rotation = 0)
|
||||
public static function getTextWidthPixelsExact(string $text, FontStyle $font, int $rotation = 0): int
|
||||
{
|
||||
if (!function_exists('imagettfbbox')) {
|
||||
throw new PhpSpreadsheetException('GD library needs to be enabled');
|
||||
@@ -287,7 +410,12 @@ class Font
|
||||
// font size should really be supplied in pixels in GD2,
|
||||
// but since GD2 seems to assume 72dpi, pixels and points are the same
|
||||
$fontFile = self::getTrueTypeFontFileFromFont($font);
|
||||
$textBox = imagettfbbox($font->getSize(), $rotation, $fontFile, $text);
|
||||
$textBox = imagettfbbox($font->getSize() ?? 10.0, $rotation, $fontFile, $text);
|
||||
if ($textBox === false) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new PhpSpreadsheetException('imagettfbbox failed');
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
// Get corners positions
|
||||
$lowerLeftCornerX = $textBox[0];
|
||||
@@ -307,7 +435,7 @@ class Font
|
||||
*
|
||||
* @return int Text width in pixels (no padding added)
|
||||
*/
|
||||
public static function getTextWidthPixelsApprox($columnText, \PhpOffice\PhpSpreadsheet\Style\Font $font, $rotation = 0)
|
||||
public static function getTextWidthPixelsApprox($columnText, FontStyle $font, $rotation = 0)
|
||||
{
|
||||
$fontName = $font->getName();
|
||||
$fontSize = $font->getSize();
|
||||
@@ -342,7 +470,7 @@ class Font
|
||||
|
||||
// Calculate approximate rotated column width
|
||||
if ($rotation !== 0) {
|
||||
if ($rotation == -165) {
|
||||
if ($rotation == Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) {
|
||||
// stacked text
|
||||
$columnWidth = 4; // approximation
|
||||
} else {
|
||||
@@ -395,183 +523,88 @@ class Font
|
||||
/**
|
||||
* Returns the font path given the font.
|
||||
*
|
||||
* @param \PhpOffice\PhpSpreadsheet\Style\Font $font
|
||||
*
|
||||
* @return string Path to TrueType font file
|
||||
*/
|
||||
public static function getTrueTypeFontFileFromFont($font)
|
||||
public static function getTrueTypeFontFileFromFont(FontStyle $font, bool $checkPath = true)
|
||||
{
|
||||
if (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath)) {
|
||||
if ($checkPath && (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath))) {
|
||||
throw new PhpSpreadsheetException('Valid directory to TrueType Font files not specified');
|
||||
}
|
||||
|
||||
$name = $font->getName();
|
||||
if (!isset(self::FONT_FILE_NAMES[$name])) {
|
||||
throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file');
|
||||
}
|
||||
$bold = $font->getBold();
|
||||
$italic = $font->getItalic();
|
||||
|
||||
// Check if we can map font to true type font file
|
||||
switch ($name) {
|
||||
case 'Arial':
|
||||
$fontFile = (
|
||||
$bold ? ($italic ? self::ARIAL_BOLD_ITALIC : self::ARIAL_BOLD)
|
||||
: ($italic ? self::ARIAL_ITALIC : self::ARIAL)
|
||||
);
|
||||
|
||||
break;
|
||||
case 'Calibri':
|
||||
$fontFile = (
|
||||
$bold ? ($italic ? self::CALIBRI_BOLD_ITALIC : self::CALIBRI_BOLD)
|
||||
: ($italic ? self::CALIBRI_ITALIC : self::CALIBRI)
|
||||
);
|
||||
|
||||
break;
|
||||
case 'Courier New':
|
||||
$fontFile = (
|
||||
$bold ? ($italic ? self::COURIER_NEW_BOLD_ITALIC : self::COURIER_NEW_BOLD)
|
||||
: ($italic ? self::COURIER_NEW_ITALIC : self::COURIER_NEW)
|
||||
);
|
||||
|
||||
break;
|
||||
case 'Comic Sans MS':
|
||||
$fontFile = (
|
||||
$bold ? self::COMIC_SANS_MS_BOLD : self::COMIC_SANS_MS
|
||||
);
|
||||
|
||||
break;
|
||||
case 'Georgia':
|
||||
$fontFile = (
|
||||
$bold ? ($italic ? self::GEORGIA_BOLD_ITALIC : self::GEORGIA_BOLD)
|
||||
: ($italic ? self::GEORGIA_ITALIC : self::GEORGIA)
|
||||
);
|
||||
|
||||
break;
|
||||
case 'Impact':
|
||||
$fontFile = self::IMPACT;
|
||||
|
||||
break;
|
||||
case 'Liberation Sans':
|
||||
$fontFile = (
|
||||
$bold ? ($italic ? self::LIBERATION_SANS_BOLD_ITALIC : self::LIBERATION_SANS_BOLD)
|
||||
: ($italic ? self::LIBERATION_SANS_ITALIC : self::LIBERATION_SANS)
|
||||
);
|
||||
|
||||
break;
|
||||
case 'Lucida Console':
|
||||
$fontFile = self::LUCIDA_CONSOLE;
|
||||
|
||||
break;
|
||||
case 'Lucida Sans Unicode':
|
||||
$fontFile = self::LUCIDA_SANS_UNICODE;
|
||||
|
||||
break;
|
||||
case 'Microsoft Sans Serif':
|
||||
$fontFile = self::MICROSOFT_SANS_SERIF;
|
||||
|
||||
break;
|
||||
case 'Palatino Linotype':
|
||||
$fontFile = (
|
||||
$bold ? ($italic ? self::PALATINO_LINOTYPE_BOLD_ITALIC : self::PALATINO_LINOTYPE_BOLD)
|
||||
: ($italic ? self::PALATINO_LINOTYPE_ITALIC : self::PALATINO_LINOTYPE)
|
||||
);
|
||||
|
||||
break;
|
||||
case 'Symbol':
|
||||
$fontFile = self::SYMBOL;
|
||||
|
||||
break;
|
||||
case 'Tahoma':
|
||||
$fontFile = (
|
||||
$bold ? self::TAHOMA_BOLD : self::TAHOMA
|
||||
);
|
||||
|
||||
break;
|
||||
case 'Times New Roman':
|
||||
$fontFile = (
|
||||
$bold ? ($italic ? self::TIMES_NEW_ROMAN_BOLD_ITALIC : self::TIMES_NEW_ROMAN_BOLD)
|
||||
: ($italic ? self::TIMES_NEW_ROMAN_ITALIC : self::TIMES_NEW_ROMAN)
|
||||
);
|
||||
|
||||
break;
|
||||
case 'Trebuchet MS':
|
||||
$fontFile = (
|
||||
$bold ? ($italic ? self::TREBUCHET_MS_BOLD_ITALIC : self::TREBUCHET_MS_BOLD)
|
||||
: ($italic ? self::TREBUCHET_MS_ITALIC : self::TREBUCHET_MS)
|
||||
);
|
||||
|
||||
break;
|
||||
case 'Verdana':
|
||||
$fontFile = (
|
||||
$bold ? ($italic ? self::VERDANA_BOLD_ITALIC : self::VERDANA_BOLD)
|
||||
: ($italic ? self::VERDANA_ITALIC : self::VERDANA)
|
||||
);
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file');
|
||||
|
||||
break;
|
||||
$index = 'x';
|
||||
if ($bold) {
|
||||
$index .= 'b';
|
||||
}
|
||||
if ($italic) {
|
||||
$index .= 'i';
|
||||
}
|
||||
$fontFile = self::FONT_FILE_NAMES[$name][$index];
|
||||
|
||||
$fontFile = self::$trueTypeFontPath . $fontFile;
|
||||
$separator = '';
|
||||
if (mb_strlen(self::$trueTypeFontPath) > 1 && mb_substr(self::$trueTypeFontPath, -1) !== '/' && mb_substr(self::$trueTypeFontPath, -1) !== '\\') {
|
||||
$separator = DIRECTORY_SEPARATOR;
|
||||
}
|
||||
$fontFile = self::$trueTypeFontPath . $separator . $fontFile;
|
||||
|
||||
// Check if file actually exists
|
||||
if (!file_exists($fontFile)) {
|
||||
if ($checkPath && !file_exists($fontFile)) {
|
||||
throw new PhpSpreadsheetException('TrueType Font file not found');
|
||||
}
|
||||
|
||||
return $fontFile;
|
||||
}
|
||||
|
||||
public const CHARSET_FROM_FONT_NAME = [
|
||||
'EucrosiaUPC' => self::CHARSET_ANSI_THAI,
|
||||
'Wingdings' => self::CHARSET_SYMBOL,
|
||||
'Wingdings 2' => self::CHARSET_SYMBOL,
|
||||
'Wingdings 3' => self::CHARSET_SYMBOL,
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the associated charset for the font name.
|
||||
*
|
||||
* @param string $name Font name
|
||||
* @param string $fontName Font name
|
||||
*
|
||||
* @return int Character set code
|
||||
*/
|
||||
public static function getCharsetFromFontName($name)
|
||||
public static function getCharsetFromFontName($fontName)
|
||||
{
|
||||
switch ($name) {
|
||||
// Add more cases. Check FONT records in real Excel files.
|
||||
case 'EucrosiaUPC':
|
||||
return self::CHARSET_ANSI_THAI;
|
||||
case 'Wingdings':
|
||||
return self::CHARSET_SYMBOL;
|
||||
case 'Wingdings 2':
|
||||
return self::CHARSET_SYMBOL;
|
||||
case 'Wingdings 3':
|
||||
return self::CHARSET_SYMBOL;
|
||||
default:
|
||||
return self::CHARSET_ANSI_LATIN;
|
||||
}
|
||||
return self::CHARSET_FROM_FONT_NAME[$fontName] ?? self::CHARSET_ANSI_LATIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective column width for columns without a column dimension or column with width -1
|
||||
* For example, for Calibri 11 this is 9.140625 (64 px).
|
||||
*
|
||||
* @param \PhpOffice\PhpSpreadsheet\Style\Font $font The workbooks default font
|
||||
* @param bool $pPixels true = return column width in pixels, false = return in OOXML units
|
||||
* @param FontStyle $font The workbooks default font
|
||||
* @param bool $returnAsPixels true = return column width in pixels, false = return in OOXML units
|
||||
*
|
||||
* @return mixed Column width
|
||||
*/
|
||||
public static function getDefaultColumnWidthByFont(\PhpOffice\PhpSpreadsheet\Style\Font $font, $pPixels = false)
|
||||
public static function getDefaultColumnWidthByFont(FontStyle $font, $returnAsPixels = false)
|
||||
{
|
||||
if (isset(self::$defaultColumnWidths[$font->getName()][$font->getSize()])) {
|
||||
if (isset(self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()])) {
|
||||
// Exact width can be determined
|
||||
$columnWidth = $pPixels ?
|
||||
self::$defaultColumnWidths[$font->getName()][$font->getSize()]['px']
|
||||
: self::$defaultColumnWidths[$font->getName()][$font->getSize()]['width'];
|
||||
$columnWidth = $returnAsPixels ?
|
||||
self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()]['px']
|
||||
: self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()]['width'];
|
||||
} else {
|
||||
// We don't have data for this particular font and size, use approximation by
|
||||
// extrapolating from Calibri 11
|
||||
$columnWidth = $pPixels ?
|
||||
self::$defaultColumnWidths['Calibri'][11]['px']
|
||||
: self::$defaultColumnWidths['Calibri'][11]['width'];
|
||||
$columnWidth = $returnAsPixels ?
|
||||
self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['px']
|
||||
: self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['width'];
|
||||
$columnWidth = $columnWidth * $font->getSize() / 11;
|
||||
|
||||
// Round pixels to closest integer
|
||||
if ($pPixels) {
|
||||
if ($returnAsPixels) {
|
||||
$columnWidth = (int) round($columnWidth);
|
||||
}
|
||||
}
|
||||
@@ -583,179 +616,20 @@ class Font
|
||||
* Get the effective row height for rows without a row dimension or rows with height -1
|
||||
* For example, for Calibri 11 this is 15 points.
|
||||
*
|
||||
* @param \PhpOffice\PhpSpreadsheet\Style\Font $font The workbooks default font
|
||||
* @param FontStyle $font The workbooks default font
|
||||
*
|
||||
* @return float Row height in points
|
||||
*/
|
||||
public static function getDefaultRowHeightByFont(\PhpOffice\PhpSpreadsheet\Style\Font $font)
|
||||
public static function getDefaultRowHeightByFont(FontStyle $font)
|
||||
{
|
||||
switch ($font->getName()) {
|
||||
case 'Arial':
|
||||
switch ($font->getSize()) {
|
||||
case 10:
|
||||
// inspection of Arial 10 workbook says 12.75pt ~17px
|
||||
$rowHeight = 12.75;
|
||||
|
||||
break;
|
||||
case 9:
|
||||
// inspection of Arial 9 workbook says 12.00pt ~16px
|
||||
$rowHeight = 12;
|
||||
|
||||
break;
|
||||
case 8:
|
||||
// inspection of Arial 8 workbook says 11.25pt ~15px
|
||||
$rowHeight = 11.25;
|
||||
|
||||
break;
|
||||
case 7:
|
||||
// inspection of Arial 7 workbook says 9.00pt ~12px
|
||||
$rowHeight = 9;
|
||||
|
||||
break;
|
||||
case 6:
|
||||
case 5:
|
||||
// inspection of Arial 5,6 workbook says 8.25pt ~11px
|
||||
$rowHeight = 8.25;
|
||||
|
||||
break;
|
||||
case 4:
|
||||
// inspection of Arial 4 workbook says 6.75pt ~9px
|
||||
$rowHeight = 6.75;
|
||||
|
||||
break;
|
||||
case 3:
|
||||
// inspection of Arial 3 workbook says 6.00pt ~8px
|
||||
$rowHeight = 6;
|
||||
|
||||
break;
|
||||
case 2:
|
||||
case 1:
|
||||
// inspection of Arial 1,2 workbook says 5.25pt ~7px
|
||||
$rowHeight = 5.25;
|
||||
|
||||
break;
|
||||
default:
|
||||
// use Arial 10 workbook as an approximation, extrapolation
|
||||
$rowHeight = 12.75 * $font->getSize() / 10;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'Calibri':
|
||||
switch ($font->getSize()) {
|
||||
case 11:
|
||||
// inspection of Calibri 11 workbook says 15.00pt ~20px
|
||||
$rowHeight = 15;
|
||||
|
||||
break;
|
||||
case 10:
|
||||
// inspection of Calibri 10 workbook says 12.75pt ~17px
|
||||
$rowHeight = 12.75;
|
||||
|
||||
break;
|
||||
case 9:
|
||||
// inspection of Calibri 9 workbook says 12.00pt ~16px
|
||||
$rowHeight = 12;
|
||||
|
||||
break;
|
||||
case 8:
|
||||
// inspection of Calibri 8 workbook says 11.25pt ~15px
|
||||
$rowHeight = 11.25;
|
||||
|
||||
break;
|
||||
case 7:
|
||||
// inspection of Calibri 7 workbook says 9.00pt ~12px
|
||||
$rowHeight = 9;
|
||||
|
||||
break;
|
||||
case 6:
|
||||
case 5:
|
||||
// inspection of Calibri 5,6 workbook says 8.25pt ~11px
|
||||
$rowHeight = 8.25;
|
||||
|
||||
break;
|
||||
case 4:
|
||||
// inspection of Calibri 4 workbook says 6.75pt ~9px
|
||||
$rowHeight = 6.75;
|
||||
|
||||
break;
|
||||
case 3:
|
||||
// inspection of Calibri 3 workbook says 6.00pt ~8px
|
||||
$rowHeight = 6.00;
|
||||
|
||||
break;
|
||||
case 2:
|
||||
case 1:
|
||||
// inspection of Calibri 1,2 workbook says 5.25pt ~7px
|
||||
$rowHeight = 5.25;
|
||||
|
||||
break;
|
||||
default:
|
||||
// use Calibri 11 workbook as an approximation, extrapolation
|
||||
$rowHeight = 15 * $font->getSize() / 11;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'Verdana':
|
||||
switch ($font->getSize()) {
|
||||
case 10:
|
||||
// inspection of Verdana 10 workbook says 12.75pt ~17px
|
||||
$rowHeight = 12.75;
|
||||
|
||||
break;
|
||||
case 9:
|
||||
// inspection of Verdana 9 workbook says 11.25pt ~15px
|
||||
$rowHeight = 11.25;
|
||||
|
||||
break;
|
||||
case 8:
|
||||
// inspection of Verdana 8 workbook says 10.50pt ~14px
|
||||
$rowHeight = 10.50;
|
||||
|
||||
break;
|
||||
case 7:
|
||||
// inspection of Verdana 7 workbook says 9.00pt ~12px
|
||||
$rowHeight = 9.00;
|
||||
|
||||
break;
|
||||
case 6:
|
||||
case 5:
|
||||
// inspection of Verdana 5,6 workbook says 8.25pt ~11px
|
||||
$rowHeight = 8.25;
|
||||
|
||||
break;
|
||||
case 4:
|
||||
// inspection of Verdana 4 workbook says 6.75pt ~9px
|
||||
$rowHeight = 6.75;
|
||||
|
||||
break;
|
||||
case 3:
|
||||
// inspection of Verdana 3 workbook says 6.00pt ~8px
|
||||
$rowHeight = 6;
|
||||
|
||||
break;
|
||||
case 2:
|
||||
case 1:
|
||||
// inspection of Verdana 1,2 workbook says 5.25pt ~7px
|
||||
$rowHeight = 5.25;
|
||||
|
||||
break;
|
||||
default:
|
||||
// use Verdana 10 workbook as an approximation, extrapolation
|
||||
$rowHeight = 12.75 * $font->getSize() / 10;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
// just use Calibri as an approximation
|
||||
$rowHeight = 15 * $font->getSize() / 11;
|
||||
|
||||
break;
|
||||
$name = $font->getName();
|
||||
$size = $font->getSize();
|
||||
if (isset(self::DEFAULT_COLUMN_WIDTHS[$name][$size])) {
|
||||
$rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][$size]['height'];
|
||||
} elseif ($name === 'Arial' || $name === 'Verdana') {
|
||||
$rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][10]['height'] * $size / 10.0;
|
||||
} else {
|
||||
$rowHeight = self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['height'] * $size / 11.0;
|
||||
}
|
||||
|
||||
return $rowHeight;
|
||||
|
21
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/IntOrFloat.php
vendored
Normal file
21
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/IntOrFloat.php
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
class IntOrFloat
|
||||
{
|
||||
/**
|
||||
* Help some functions with large results operate correctly on 32-bit,
|
||||
* by returning result as int when possible, float otherwise.
|
||||
*
|
||||
* @param float|int $value
|
||||
*
|
||||
* @return float|int
|
||||
*/
|
||||
public static function evaluate($value)
|
||||
{
|
||||
$iValue = (int) $value;
|
||||
|
||||
return ($value == $iValue) ? $iValue : $value;
|
||||
}
|
||||
}
|
@@ -1,147 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\JAMA;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
|
||||
|
||||
/**
|
||||
* Cholesky decomposition class.
|
||||
*
|
||||
* For a symmetric, positive definite matrix A, the Cholesky decomposition
|
||||
* is an lower triangular matrix L so that A = L*L'.
|
||||
*
|
||||
* If the matrix is not symmetric or positive definite, the constructor
|
||||
* returns a partial decomposition and sets an internal flag that may
|
||||
* be queried by the isSPD() method.
|
||||
*
|
||||
* @author Paul Meagher
|
||||
* @author Michael Bommarito
|
||||
*
|
||||
* @version 1.2
|
||||
*/
|
||||
class CholeskyDecomposition
|
||||
{
|
||||
/**
|
||||
* Decomposition storage.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $L = [];
|
||||
|
||||
/**
|
||||
* Matrix row and column dimension.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $m;
|
||||
|
||||
/**
|
||||
* Symmetric positive definite flag.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $isspd = true;
|
||||
|
||||
/**
|
||||
* CholeskyDecomposition.
|
||||
*
|
||||
* Class constructor - decomposes symmetric positive definite matrix
|
||||
*
|
||||
* @param Matrix $A Matrix square symmetric positive definite matrix
|
||||
*/
|
||||
public function __construct(Matrix $A)
|
||||
{
|
||||
$this->L = $A->getArray();
|
||||
$this->m = $A->getRowDimension();
|
||||
|
||||
for ($i = 0; $i < $this->m; ++$i) {
|
||||
for ($j = $i; $j < $this->m; ++$j) {
|
||||
for ($sum = $this->L[$i][$j], $k = $i - 1; $k >= 0; --$k) {
|
||||
$sum -= $this->L[$i][$k] * $this->L[$j][$k];
|
||||
}
|
||||
if ($i == $j) {
|
||||
if ($sum >= 0) {
|
||||
$this->L[$i][$i] = sqrt($sum);
|
||||
} else {
|
||||
$this->isspd = false;
|
||||
}
|
||||
} else {
|
||||
if ($this->L[$i][$i] != 0) {
|
||||
$this->L[$j][$i] = $sum / $this->L[$i][$i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ($k = $i + 1; $k < $this->m; ++$k) {
|
||||
$this->L[$i][$k] = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the matrix symmetric and positive definite?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSPD()
|
||||
{
|
||||
return $this->isspd;
|
||||
}
|
||||
|
||||
/**
|
||||
* getL.
|
||||
*
|
||||
* Return triangular factor.
|
||||
*
|
||||
* @return Matrix Lower triangular matrix
|
||||
*/
|
||||
public function getL()
|
||||
{
|
||||
return new Matrix($this->L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Solve A*X = B.
|
||||
*
|
||||
* @param $B Row-equal matrix
|
||||
*
|
||||
* @return Matrix L * L' * X = B
|
||||
*/
|
||||
public function solve(Matrix $B)
|
||||
{
|
||||
if ($B->getRowDimension() == $this->m) {
|
||||
if ($this->isspd) {
|
||||
$X = $B->getArrayCopy();
|
||||
$nx = $B->getColumnDimension();
|
||||
|
||||
for ($k = 0; $k < $this->m; ++$k) {
|
||||
for ($i = $k + 1; $i < $this->m; ++$i) {
|
||||
for ($j = 0; $j < $nx; ++$j) {
|
||||
$X[$i][$j] -= $X[$k][$j] * $this->L[$i][$k];
|
||||
}
|
||||
}
|
||||
for ($j = 0; $j < $nx; ++$j) {
|
||||
$X[$k][$j] /= $this->L[$k][$k];
|
||||
}
|
||||
}
|
||||
|
||||
for ($k = $this->m - 1; $k >= 0; --$k) {
|
||||
for ($j = 0; $j < $nx; ++$j) {
|
||||
$X[$k][$j] /= $this->L[$k][$k];
|
||||
}
|
||||
for ($i = 0; $i < $k; ++$i) {
|
||||
for ($j = 0; $j < $nx; ++$j) {
|
||||
$X[$i][$j] -= $X[$k][$j] * $this->L[$k][$i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Matrix($X, $this->m, $nx);
|
||||
}
|
||||
|
||||
throw new CalculationException(Matrix::MATRIX_SPD_EXCEPTION);
|
||||
}
|
||||
|
||||
throw new CalculationException(Matrix::MATRIX_DIMENSION_EXCEPTION);
|
||||
}
|
||||
}
|
@@ -1,863 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\JAMA;
|
||||
|
||||
/**
|
||||
* Class to obtain eigenvalues and eigenvectors of a real matrix.
|
||||
*
|
||||
* If A is symmetric, then A = V*D*V' where the eigenvalue matrix D
|
||||
* is diagonal and the eigenvector matrix V is orthogonal (i.e.
|
||||
* A = V.times(D.times(V.transpose())) and V.times(V.transpose())
|
||||
* equals the identity matrix).
|
||||
*
|
||||
* If A is not symmetric, then the eigenvalue matrix D is block diagonal
|
||||
* with the real eigenvalues in 1-by-1 blocks and any complex eigenvalues,
|
||||
* lambda + i*mu, in 2-by-2 blocks, [lambda, mu; -mu, lambda]. The
|
||||
* columns of V represent the eigenvectors in the sense that A*V = V*D,
|
||||
* i.e. A.times(V) equals V.times(D). The matrix V may be badly
|
||||
* conditioned, or even singular, so the validity of the equation
|
||||
* A = V*D*inverse(V) depends upon V.cond().
|
||||
*
|
||||
* @author Paul Meagher
|
||||
*
|
||||
* @version 1.1
|
||||
*/
|
||||
class EigenvalueDecomposition
|
||||
{
|
||||
/**
|
||||
* Row and column dimension (square matrix).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $n;
|
||||
|
||||
/**
|
||||
* Arrays for internal storage of eigenvalues.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $d = [];
|
||||
|
||||
private $e = [];
|
||||
|
||||
/**
|
||||
* Array for internal storage of eigenvectors.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $V = [];
|
||||
|
||||
/**
|
||||
* Array for internal storage of nonsymmetric Hessenberg form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $H = [];
|
||||
|
||||
/**
|
||||
* Working storage for nonsymmetric algorithm.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $ort;
|
||||
|
||||
/**
|
||||
* Used for complex scalar division.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
private $cdivr;
|
||||
|
||||
private $cdivi;
|
||||
|
||||
/**
|
||||
* Symmetric Householder reduction to tridiagonal form.
|
||||
*/
|
||||
private function tred2(): void
|
||||
{
|
||||
// This is derived from the Algol procedures tred2 by
|
||||
// Bowdler, Martin, Reinsch, and Wilkinson, Handbook for
|
||||
// Auto. Comp., Vol.ii-Linear Algebra, and the corresponding
|
||||
// Fortran subroutine in EISPACK.
|
||||
$this->d = $this->V[$this->n - 1];
|
||||
// Householder reduction to tridiagonal form.
|
||||
for ($i = $this->n - 1; $i > 0; --$i) {
|
||||
$i_ = $i - 1;
|
||||
// Scale to avoid under/overflow.
|
||||
$h = $scale = 0.0;
|
||||
$scale += array_sum(array_map('abs', $this->d));
|
||||
if ($scale == 0.0) {
|
||||
$this->e[$i] = $this->d[$i_];
|
||||
$this->d = array_slice($this->V[$i_], 0, $i_);
|
||||
for ($j = 0; $j < $i; ++$j) {
|
||||
$this->V[$j][$i] = $this->V[$i][$j] = 0.0;
|
||||
}
|
||||
} else {
|
||||
// Generate Householder vector.
|
||||
for ($k = 0; $k < $i; ++$k) {
|
||||
$this->d[$k] /= $scale;
|
||||
$h += $this->d[$k] ** 2;
|
||||
}
|
||||
$f = $this->d[$i_];
|
||||
$g = sqrt($h);
|
||||
if ($f > 0) {
|
||||
$g = -$g;
|
||||
}
|
||||
$this->e[$i] = $scale * $g;
|
||||
$h = $h - $f * $g;
|
||||
$this->d[$i_] = $f - $g;
|
||||
for ($j = 0; $j < $i; ++$j) {
|
||||
$this->e[$j] = 0.0;
|
||||
}
|
||||
// Apply similarity transformation to remaining columns.
|
||||
for ($j = 0; $j < $i; ++$j) {
|
||||
$f = $this->d[$j];
|
||||
$this->V[$j][$i] = $f;
|
||||
$g = $this->e[$j] + $this->V[$j][$j] * $f;
|
||||
for ($k = $j + 1; $k <= $i_; ++$k) {
|
||||
$g += $this->V[$k][$j] * $this->d[$k];
|
||||
$this->e[$k] += $this->V[$k][$j] * $f;
|
||||
}
|
||||
$this->e[$j] = $g;
|
||||
}
|
||||
$f = 0.0;
|
||||
for ($j = 0; $j < $i; ++$j) {
|
||||
$this->e[$j] /= $h;
|
||||
$f += $this->e[$j] * $this->d[$j];
|
||||
}
|
||||
$hh = $f / (2 * $h);
|
||||
for ($j = 0; $j < $i; ++$j) {
|
||||
$this->e[$j] -= $hh * $this->d[$j];
|
||||
}
|
||||
for ($j = 0; $j < $i; ++$j) {
|
||||
$f = $this->d[$j];
|
||||
$g = $this->e[$j];
|
||||
for ($k = $j; $k <= $i_; ++$k) {
|
||||
$this->V[$k][$j] -= ($f * $this->e[$k] + $g * $this->d[$k]);
|
||||
}
|
||||
$this->d[$j] = $this->V[$i - 1][$j];
|
||||
$this->V[$i][$j] = 0.0;
|
||||
}
|
||||
}
|
||||
$this->d[$i] = $h;
|
||||
}
|
||||
|
||||
// Accumulate transformations.
|
||||
for ($i = 0; $i < $this->n - 1; ++$i) {
|
||||
$this->V[$this->n - 1][$i] = $this->V[$i][$i];
|
||||
$this->V[$i][$i] = 1.0;
|
||||
$h = $this->d[$i + 1];
|
||||
if ($h != 0.0) {
|
||||
for ($k = 0; $k <= $i; ++$k) {
|
||||
$this->d[$k] = $this->V[$k][$i + 1] / $h;
|
||||
}
|
||||
for ($j = 0; $j <= $i; ++$j) {
|
||||
$g = 0.0;
|
||||
for ($k = 0; $k <= $i; ++$k) {
|
||||
$g += $this->V[$k][$i + 1] * $this->V[$k][$j];
|
||||
}
|
||||
for ($k = 0; $k <= $i; ++$k) {
|
||||
$this->V[$k][$j] -= $g * $this->d[$k];
|
||||
}
|
||||
}
|
||||
}
|
||||
for ($k = 0; $k <= $i; ++$k) {
|
||||
$this->V[$k][$i + 1] = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
$this->d = $this->V[$this->n - 1];
|
||||
$this->V[$this->n - 1] = array_fill(0, $j, 0.0);
|
||||
$this->V[$this->n - 1][$this->n - 1] = 1.0;
|
||||
$this->e[0] = 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Symmetric tridiagonal QL algorithm.
|
||||
*
|
||||
* This is derived from the Algol procedures tql2, by
|
||||
* Bowdler, Martin, Reinsch, and Wilkinson, Handbook for
|
||||
* Auto. Comp., Vol.ii-Linear Algebra, and the corresponding
|
||||
* Fortran subroutine in EISPACK.
|
||||
*/
|
||||
private function tql2(): void
|
||||
{
|
||||
for ($i = 1; $i < $this->n; ++$i) {
|
||||
$this->e[$i - 1] = $this->e[$i];
|
||||
}
|
||||
$this->e[$this->n - 1] = 0.0;
|
||||
$f = 0.0;
|
||||
$tst1 = 0.0;
|
||||
$eps = 2.0 ** (-52.0);
|
||||
|
||||
for ($l = 0; $l < $this->n; ++$l) {
|
||||
// Find small subdiagonal element
|
||||
$tst1 = max($tst1, abs($this->d[$l]) + abs($this->e[$l]));
|
||||
$m = $l;
|
||||
while ($m < $this->n) {
|
||||
if (abs($this->e[$m]) <= $eps * $tst1) {
|
||||
break;
|
||||
}
|
||||
++$m;
|
||||
}
|
||||
// If m == l, $this->d[l] is an eigenvalue,
|
||||
// otherwise, iterate.
|
||||
if ($m > $l) {
|
||||
$iter = 0;
|
||||
do {
|
||||
// Could check iteration count here.
|
||||
++$iter;
|
||||
// Compute implicit shift
|
||||
$g = $this->d[$l];
|
||||
$p = ($this->d[$l + 1] - $g) / (2.0 * $this->e[$l]);
|
||||
$r = hypo($p, 1.0);
|
||||
if ($p < 0) {
|
||||
$r *= -1;
|
||||
}
|
||||
$this->d[$l] = $this->e[$l] / ($p + $r);
|
||||
$this->d[$l + 1] = $this->e[$l] * ($p + $r);
|
||||
$dl1 = $this->d[$l + 1];
|
||||
$h = $g - $this->d[$l];
|
||||
for ($i = $l + 2; $i < $this->n; ++$i) {
|
||||
$this->d[$i] -= $h;
|
||||
}
|
||||
$f += $h;
|
||||
// Implicit QL transformation.
|
||||
$p = $this->d[$m];
|
||||
$c = 1.0;
|
||||
$c2 = $c3 = $c;
|
||||
$el1 = $this->e[$l + 1];
|
||||
$s = $s2 = 0.0;
|
||||
for ($i = $m - 1; $i >= $l; --$i) {
|
||||
$c3 = $c2;
|
||||
$c2 = $c;
|
||||
$s2 = $s;
|
||||
$g = $c * $this->e[$i];
|
||||
$h = $c * $p;
|
||||
$r = hypo($p, $this->e[$i]);
|
||||
$this->e[$i + 1] = $s * $r;
|
||||
$s = $this->e[$i] / $r;
|
||||
$c = $p / $r;
|
||||
$p = $c * $this->d[$i] - $s * $g;
|
||||
$this->d[$i + 1] = $h + $s * ($c * $g + $s * $this->d[$i]);
|
||||
// Accumulate transformation.
|
||||
for ($k = 0; $k < $this->n; ++$k) {
|
||||
$h = $this->V[$k][$i + 1];
|
||||
$this->V[$k][$i + 1] = $s * $this->V[$k][$i] + $c * $h;
|
||||
$this->V[$k][$i] = $c * $this->V[$k][$i] - $s * $h;
|
||||
}
|
||||
}
|
||||
$p = -$s * $s2 * $c3 * $el1 * $this->e[$l] / $dl1;
|
||||
$this->e[$l] = $s * $p;
|
||||
$this->d[$l] = $c * $p;
|
||||
// Check for convergence.
|
||||
} while (abs($this->e[$l]) > $eps * $tst1);
|
||||
}
|
||||
$this->d[$l] = $this->d[$l] + $f;
|
||||
$this->e[$l] = 0.0;
|
||||
}
|
||||
|
||||
// Sort eigenvalues and corresponding vectors.
|
||||
for ($i = 0; $i < $this->n - 1; ++$i) {
|
||||
$k = $i;
|
||||
$p = $this->d[$i];
|
||||
for ($j = $i + 1; $j < $this->n; ++$j) {
|
||||
if ($this->d[$j] < $p) {
|
||||
$k = $j;
|
||||
$p = $this->d[$j];
|
||||
}
|
||||
}
|
||||
if ($k != $i) {
|
||||
$this->d[$k] = $this->d[$i];
|
||||
$this->d[$i] = $p;
|
||||
for ($j = 0; $j < $this->n; ++$j) {
|
||||
$p = $this->V[$j][$i];
|
||||
$this->V[$j][$i] = $this->V[$j][$k];
|
||||
$this->V[$j][$k] = $p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nonsymmetric reduction to Hessenberg form.
|
||||
*
|
||||
* This is derived from the Algol procedures orthes and ortran,
|
||||
* by Martin and Wilkinson, Handbook for Auto. Comp.,
|
||||
* Vol.ii-Linear Algebra, and the corresponding
|
||||
* Fortran subroutines in EISPACK.
|
||||
*/
|
||||
private function orthes(): void
|
||||
{
|
||||
$low = 0;
|
||||
$high = $this->n - 1;
|
||||
|
||||
for ($m = $low + 1; $m <= $high - 1; ++$m) {
|
||||
// Scale column.
|
||||
$scale = 0.0;
|
||||
for ($i = $m; $i <= $high; ++$i) {
|
||||
$scale = $scale + abs($this->H[$i][$m - 1]);
|
||||
}
|
||||
if ($scale != 0.0) {
|
||||
// Compute Householder transformation.
|
||||
$h = 0.0;
|
||||
for ($i = $high; $i >= $m; --$i) {
|
||||
$this->ort[$i] = $this->H[$i][$m - 1] / $scale;
|
||||
$h += $this->ort[$i] * $this->ort[$i];
|
||||
}
|
||||
$g = sqrt($h);
|
||||
if ($this->ort[$m] > 0) {
|
||||
$g *= -1;
|
||||
}
|
||||
$h -= $this->ort[$m] * $g;
|
||||
$this->ort[$m] -= $g;
|
||||
// Apply Householder similarity transformation
|
||||
// H = (I -u * u' / h) * H * (I -u * u') / h)
|
||||
for ($j = $m; $j < $this->n; ++$j) {
|
||||
$f = 0.0;
|
||||
for ($i = $high; $i >= $m; --$i) {
|
||||
$f += $this->ort[$i] * $this->H[$i][$j];
|
||||
}
|
||||
$f /= $h;
|
||||
for ($i = $m; $i <= $high; ++$i) {
|
||||
$this->H[$i][$j] -= $f * $this->ort[$i];
|
||||
}
|
||||
}
|
||||
for ($i = 0; $i <= $high; ++$i) {
|
||||
$f = 0.0;
|
||||
for ($j = $high; $j >= $m; --$j) {
|
||||
$f += $this->ort[$j] * $this->H[$i][$j];
|
||||
}
|
||||
$f = $f / $h;
|
||||
for ($j = $m; $j <= $high; ++$j) {
|
||||
$this->H[$i][$j] -= $f * $this->ort[$j];
|
||||
}
|
||||
}
|
||||
$this->ort[$m] = $scale * $this->ort[$m];
|
||||
$this->H[$m][$m - 1] = $scale * $g;
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate transformations (Algol's ortran).
|
||||
for ($i = 0; $i < $this->n; ++$i) {
|
||||
for ($j = 0; $j < $this->n; ++$j) {
|
||||
$this->V[$i][$j] = ($i == $j ? 1.0 : 0.0);
|
||||
}
|
||||
}
|
||||
for ($m = $high - 1; $m >= $low + 1; --$m) {
|
||||
if ($this->H[$m][$m - 1] != 0.0) {
|
||||
for ($i = $m + 1; $i <= $high; ++$i) {
|
||||
$this->ort[$i] = $this->H[$i][$m - 1];
|
||||
}
|
||||
for ($j = $m; $j <= $high; ++$j) {
|
||||
$g = 0.0;
|
||||
for ($i = $m; $i <= $high; ++$i) {
|
||||
$g += $this->ort[$i] * $this->V[$i][$j];
|
||||
}
|
||||
// Double division avoids possible underflow
|
||||
$g = ($g / $this->ort[$m]) / $this->H[$m][$m - 1];
|
||||
for ($i = $m; $i <= $high; ++$i) {
|
||||
$this->V[$i][$j] += $g * $this->ort[$i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs complex division.
|
||||
*
|
||||
* @param mixed $xr
|
||||
* @param mixed $xi
|
||||
* @param mixed $yr
|
||||
* @param mixed $yi
|
||||
*/
|
||||
private function cdiv($xr, $xi, $yr, $yi): void
|
||||
{
|
||||
if (abs($yr) > abs($yi)) {
|
||||
$r = $yi / $yr;
|
||||
$d = $yr + $r * $yi;
|
||||
$this->cdivr = ($xr + $r * $xi) / $d;
|
||||
$this->cdivi = ($xi - $r * $xr) / $d;
|
||||
} else {
|
||||
$r = $yr / $yi;
|
||||
$d = $yi + $r * $yr;
|
||||
$this->cdivr = ($r * $xr + $xi) / $d;
|
||||
$this->cdivi = ($r * $xi - $xr) / $d;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nonsymmetric reduction from Hessenberg to real Schur form.
|
||||
*
|
||||
* Code is derived from the Algol procedure hqr2,
|
||||
* by Martin and Wilkinson, Handbook for Auto. Comp.,
|
||||
* Vol.ii-Linear Algebra, and the corresponding
|
||||
* Fortran subroutine in EISPACK.
|
||||
*/
|
||||
private function hqr2(): void
|
||||
{
|
||||
// Initialize
|
||||
$nn = $this->n;
|
||||
$n = $nn - 1;
|
||||
$low = 0;
|
||||
$high = $nn - 1;
|
||||
$eps = 2.0 ** (-52.0);
|
||||
$exshift = 0.0;
|
||||
$p = $q = $r = $s = $z = 0;
|
||||
// Store roots isolated by balanc and compute matrix norm
|
||||
$norm = 0.0;
|
||||
|
||||
for ($i = 0; $i < $nn; ++$i) {
|
||||
if (($i < $low) || ($i > $high)) {
|
||||
$this->d[$i] = $this->H[$i][$i];
|
||||
$this->e[$i] = 0.0;
|
||||
}
|
||||
for ($j = max($i - 1, 0); $j < $nn; ++$j) {
|
||||
$norm = $norm + abs($this->H[$i][$j]);
|
||||
}
|
||||
}
|
||||
|
||||
// Outer loop over eigenvalue index
|
||||
$iter = 0;
|
||||
while ($n >= $low) {
|
||||
// Look for single small sub-diagonal element
|
||||
$l = $n;
|
||||
while ($l > $low) {
|
||||
$s = abs($this->H[$l - 1][$l - 1]) + abs($this->H[$l][$l]);
|
||||
if ($s == 0.0) {
|
||||
$s = $norm;
|
||||
}
|
||||
if (abs($this->H[$l][$l - 1]) < $eps * $s) {
|
||||
break;
|
||||
}
|
||||
--$l;
|
||||
}
|
||||
// Check for convergence
|
||||
// One root found
|
||||
if ($l == $n) {
|
||||
$this->H[$n][$n] = $this->H[$n][$n] + $exshift;
|
||||
$this->d[$n] = $this->H[$n][$n];
|
||||
$this->e[$n] = 0.0;
|
||||
--$n;
|
||||
$iter = 0;
|
||||
// Two roots found
|
||||
} elseif ($l == $n - 1) {
|
||||
$w = $this->H[$n][$n - 1] * $this->H[$n - 1][$n];
|
||||
$p = ($this->H[$n - 1][$n - 1] - $this->H[$n][$n]) / 2.0;
|
||||
$q = $p * $p + $w;
|
||||
$z = sqrt(abs($q));
|
||||
$this->H[$n][$n] = $this->H[$n][$n] + $exshift;
|
||||
$this->H[$n - 1][$n - 1] = $this->H[$n - 1][$n - 1] + $exshift;
|
||||
$x = $this->H[$n][$n];
|
||||
// Real pair
|
||||
if ($q >= 0) {
|
||||
if ($p >= 0) {
|
||||
$z = $p + $z;
|
||||
} else {
|
||||
$z = $p - $z;
|
||||
}
|
||||
$this->d[$n - 1] = $x + $z;
|
||||
$this->d[$n] = $this->d[$n - 1];
|
||||
if ($z != 0.0) {
|
||||
$this->d[$n] = $x - $w / $z;
|
||||
}
|
||||
$this->e[$n - 1] = 0.0;
|
||||
$this->e[$n] = 0.0;
|
||||
$x = $this->H[$n][$n - 1];
|
||||
$s = abs($x) + abs($z);
|
||||
$p = $x / $s;
|
||||
$q = $z / $s;
|
||||
$r = sqrt($p * $p + $q * $q);
|
||||
$p = $p / $r;
|
||||
$q = $q / $r;
|
||||
// Row modification
|
||||
for ($j = $n - 1; $j < $nn; ++$j) {
|
||||
$z = $this->H[$n - 1][$j];
|
||||
$this->H[$n - 1][$j] = $q * $z + $p * $this->H[$n][$j];
|
||||
$this->H[$n][$j] = $q * $this->H[$n][$j] - $p * $z;
|
||||
}
|
||||
// Column modification
|
||||
for ($i = 0; $i <= $n; ++$i) {
|
||||
$z = $this->H[$i][$n - 1];
|
||||
$this->H[$i][$n - 1] = $q * $z + $p * $this->H[$i][$n];
|
||||
$this->H[$i][$n] = $q * $this->H[$i][$n] - $p * $z;
|
||||
}
|
||||
// Accumulate transformations
|
||||
for ($i = $low; $i <= $high; ++$i) {
|
||||
$z = $this->V[$i][$n - 1];
|
||||
$this->V[$i][$n - 1] = $q * $z + $p * $this->V[$i][$n];
|
||||
$this->V[$i][$n] = $q * $this->V[$i][$n] - $p * $z;
|
||||
}
|
||||
// Complex pair
|
||||
} else {
|
||||
$this->d[$n - 1] = $x + $p;
|
||||
$this->d[$n] = $x + $p;
|
||||
$this->e[$n - 1] = $z;
|
||||
$this->e[$n] = -$z;
|
||||
}
|
||||
$n = $n - 2;
|
||||
$iter = 0;
|
||||
// No convergence yet
|
||||
} else {
|
||||
// Form shift
|
||||
$x = $this->H[$n][$n];
|
||||
$y = 0.0;
|
||||
$w = 0.0;
|
||||
if ($l < $n) {
|
||||
$y = $this->H[$n - 1][$n - 1];
|
||||
$w = $this->H[$n][$n - 1] * $this->H[$n - 1][$n];
|
||||
}
|
||||
// Wilkinson's original ad hoc shift
|
||||
if ($iter == 10) {
|
||||
$exshift += $x;
|
||||
for ($i = $low; $i <= $n; ++$i) {
|
||||
$this->H[$i][$i] -= $x;
|
||||
}
|
||||
$s = abs($this->H[$n][$n - 1]) + abs($this->H[$n - 1][$n - 2]);
|
||||
$x = $y = 0.75 * $s;
|
||||
$w = -0.4375 * $s * $s;
|
||||
}
|
||||
// MATLAB's new ad hoc shift
|
||||
if ($iter == 30) {
|
||||
$s = ($y - $x) / 2.0;
|
||||
$s = $s * $s + $w;
|
||||
if ($s > 0) {
|
||||
$s = sqrt($s);
|
||||
if ($y < $x) {
|
||||
$s = -$s;
|
||||
}
|
||||
$s = $x - $w / (($y - $x) / 2.0 + $s);
|
||||
for ($i = $low; $i <= $n; ++$i) {
|
||||
$this->H[$i][$i] -= $s;
|
||||
}
|
||||
$exshift += $s;
|
||||
$x = $y = $w = 0.964;
|
||||
}
|
||||
}
|
||||
// Could check iteration count here.
|
||||
$iter = $iter + 1;
|
||||
// Look for two consecutive small sub-diagonal elements
|
||||
$m = $n - 2;
|
||||
while ($m >= $l) {
|
||||
$z = $this->H[$m][$m];
|
||||
$r = $x - $z;
|
||||
$s = $y - $z;
|
||||
$p = ($r * $s - $w) / $this->H[$m + 1][$m] + $this->H[$m][$m + 1];
|
||||
$q = $this->H[$m + 1][$m + 1] - $z - $r - $s;
|
||||
$r = $this->H[$m + 2][$m + 1];
|
||||
$s = abs($p) + abs($q) + abs($r);
|
||||
$p = $p / $s;
|
||||
$q = $q / $s;
|
||||
$r = $r / $s;
|
||||
if ($m == $l) {
|
||||
break;
|
||||
}
|
||||
if (
|
||||
abs($this->H[$m][$m - 1]) * (abs($q) + abs($r)) <
|
||||
$eps * (abs($p) * (abs($this->H[$m - 1][$m - 1]) + abs($z) + abs($this->H[$m + 1][$m + 1])))
|
||||
) {
|
||||
break;
|
||||
}
|
||||
--$m;
|
||||
}
|
||||
for ($i = $m + 2; $i <= $n; ++$i) {
|
||||
$this->H[$i][$i - 2] = 0.0;
|
||||
if ($i > $m + 2) {
|
||||
$this->H[$i][$i - 3] = 0.0;
|
||||
}
|
||||
}
|
||||
// Double QR step involving rows l:n and columns m:n
|
||||
for ($k = $m; $k <= $n - 1; ++$k) {
|
||||
$notlast = ($k != $n - 1);
|
||||
if ($k != $m) {
|
||||
$p = $this->H[$k][$k - 1];
|
||||
$q = $this->H[$k + 1][$k - 1];
|
||||
$r = ($notlast ? $this->H[$k + 2][$k - 1] : 0.0);
|
||||
$x = abs($p) + abs($q) + abs($r);
|
||||
if ($x != 0.0) {
|
||||
$p = $p / $x;
|
||||
$q = $q / $x;
|
||||
$r = $r / $x;
|
||||
}
|
||||
}
|
||||
if ($x == 0.0) {
|
||||
break;
|
||||
}
|
||||
$s = sqrt($p * $p + $q * $q + $r * $r);
|
||||
if ($p < 0) {
|
||||
$s = -$s;
|
||||
}
|
||||
if ($s != 0) {
|
||||
if ($k != $m) {
|
||||
$this->H[$k][$k - 1] = -$s * $x;
|
||||
} elseif ($l != $m) {
|
||||
$this->H[$k][$k - 1] = -$this->H[$k][$k - 1];
|
||||
}
|
||||
$p = $p + $s;
|
||||
$x = $p / $s;
|
||||
$y = $q / $s;
|
||||
$z = $r / $s;
|
||||
$q = $q / $p;
|
||||
$r = $r / $p;
|
||||
// Row modification
|
||||
for ($j = $k; $j < $nn; ++$j) {
|
||||
$p = $this->H[$k][$j] + $q * $this->H[$k + 1][$j];
|
||||
if ($notlast) {
|
||||
$p = $p + $r * $this->H[$k + 2][$j];
|
||||
$this->H[$k + 2][$j] = $this->H[$k + 2][$j] - $p * $z;
|
||||
}
|
||||
$this->H[$k][$j] = $this->H[$k][$j] - $p * $x;
|
||||
$this->H[$k + 1][$j] = $this->H[$k + 1][$j] - $p * $y;
|
||||
}
|
||||
// Column modification
|
||||
$iMax = min($n, $k + 3);
|
||||
for ($i = 0; $i <= $iMax; ++$i) {
|
||||
$p = $x * $this->H[$i][$k] + $y * $this->H[$i][$k + 1];
|
||||
if ($notlast) {
|
||||
$p = $p + $z * $this->H[$i][$k + 2];
|
||||
$this->H[$i][$k + 2] = $this->H[$i][$k + 2] - $p * $r;
|
||||
}
|
||||
$this->H[$i][$k] = $this->H[$i][$k] - $p;
|
||||
$this->H[$i][$k + 1] = $this->H[$i][$k + 1] - $p * $q;
|
||||
}
|
||||
// Accumulate transformations
|
||||
for ($i = $low; $i <= $high; ++$i) {
|
||||
$p = $x * $this->V[$i][$k] + $y * $this->V[$i][$k + 1];
|
||||
if ($notlast) {
|
||||
$p = $p + $z * $this->V[$i][$k + 2];
|
||||
$this->V[$i][$k + 2] = $this->V[$i][$k + 2] - $p * $r;
|
||||
}
|
||||
$this->V[$i][$k] = $this->V[$i][$k] - $p;
|
||||
$this->V[$i][$k + 1] = $this->V[$i][$k + 1] - $p * $q;
|
||||
}
|
||||
} // ($s != 0)
|
||||
} // k loop
|
||||
} // check convergence
|
||||
} // while ($n >= $low)
|
||||
|
||||
// Backsubstitute to find vectors of upper triangular form
|
||||
if ($norm == 0.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for ($n = $nn - 1; $n >= 0; --$n) {
|
||||
$p = $this->d[$n];
|
||||
$q = $this->e[$n];
|
||||
// Real vector
|
||||
if ($q == 0) {
|
||||
$l = $n;
|
||||
$this->H[$n][$n] = 1.0;
|
||||
for ($i = $n - 1; $i >= 0; --$i) {
|
||||
$w = $this->H[$i][$i] - $p;
|
||||
$r = 0.0;
|
||||
for ($j = $l; $j <= $n; ++$j) {
|
||||
$r = $r + $this->H[$i][$j] * $this->H[$j][$n];
|
||||
}
|
||||
if ($this->e[$i] < 0.0) {
|
||||
$z = $w;
|
||||
$s = $r;
|
||||
} else {
|
||||
$l = $i;
|
||||
if ($this->e[$i] == 0.0) {
|
||||
if ($w != 0.0) {
|
||||
$this->H[$i][$n] = -$r / $w;
|
||||
} else {
|
||||
$this->H[$i][$n] = -$r / ($eps * $norm);
|
||||
}
|
||||
// Solve real equations
|
||||
} else {
|
||||
$x = $this->H[$i][$i + 1];
|
||||
$y = $this->H[$i + 1][$i];
|
||||
$q = ($this->d[$i] - $p) * ($this->d[$i] - $p) + $this->e[$i] * $this->e[$i];
|
||||
$t = ($x * $s - $z * $r) / $q;
|
||||
$this->H[$i][$n] = $t;
|
||||
if (abs($x) > abs($z)) {
|
||||
$this->H[$i + 1][$n] = (-$r - $w * $t) / $x;
|
||||
} else {
|
||||
$this->H[$i + 1][$n] = (-$s - $y * $t) / $z;
|
||||
}
|
||||
}
|
||||
// Overflow control
|
||||
$t = abs($this->H[$i][$n]);
|
||||
if (($eps * $t) * $t > 1) {
|
||||
for ($j = $i; $j <= $n; ++$j) {
|
||||
$this->H[$j][$n] = $this->H[$j][$n] / $t;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Complex vector
|
||||
} elseif ($q < 0) {
|
||||
$l = $n - 1;
|
||||
// Last vector component imaginary so matrix is triangular
|
||||
if (abs($this->H[$n][$n - 1]) > abs($this->H[$n - 1][$n])) {
|
||||
$this->H[$n - 1][$n - 1] = $q / $this->H[$n][$n - 1];
|
||||
$this->H[$n - 1][$n] = -($this->H[$n][$n] - $p) / $this->H[$n][$n - 1];
|
||||
} else {
|
||||
$this->cdiv(0.0, -$this->H[$n - 1][$n], $this->H[$n - 1][$n - 1] - $p, $q);
|
||||
$this->H[$n - 1][$n - 1] = $this->cdivr;
|
||||
$this->H[$n - 1][$n] = $this->cdivi;
|
||||
}
|
||||
$this->H[$n][$n - 1] = 0.0;
|
||||
$this->H[$n][$n] = 1.0;
|
||||
for ($i = $n - 2; $i >= 0; --$i) {
|
||||
// double ra,sa,vr,vi;
|
||||
$ra = 0.0;
|
||||
$sa = 0.0;
|
||||
for ($j = $l; $j <= $n; ++$j) {
|
||||
$ra = $ra + $this->H[$i][$j] * $this->H[$j][$n - 1];
|
||||
$sa = $sa + $this->H[$i][$j] * $this->H[$j][$n];
|
||||
}
|
||||
$w = $this->H[$i][$i] - $p;
|
||||
if ($this->e[$i] < 0.0) {
|
||||
$z = $w;
|
||||
$r = $ra;
|
||||
$s = $sa;
|
||||
} else {
|
||||
$l = $i;
|
||||
if ($this->e[$i] == 0) {
|
||||
$this->cdiv(-$ra, -$sa, $w, $q);
|
||||
$this->H[$i][$n - 1] = $this->cdivr;
|
||||
$this->H[$i][$n] = $this->cdivi;
|
||||
} else {
|
||||
// Solve complex equations
|
||||
$x = $this->H[$i][$i + 1];
|
||||
$y = $this->H[$i + 1][$i];
|
||||
$vr = ($this->d[$i] - $p) * ($this->d[$i] - $p) + $this->e[$i] * $this->e[$i] - $q * $q;
|
||||
$vi = ($this->d[$i] - $p) * 2.0 * $q;
|
||||
if ($vr == 0.0 & $vi == 0.0) {
|
||||
$vr = $eps * $norm * (abs($w) + abs($q) + abs($x) + abs($y) + abs($z));
|
||||
}
|
||||
$this->cdiv($x * $r - $z * $ra + $q * $sa, $x * $s - $z * $sa - $q * $ra, $vr, $vi);
|
||||
$this->H[$i][$n - 1] = $this->cdivr;
|
||||
$this->H[$i][$n] = $this->cdivi;
|
||||
if (abs($x) > (abs($z) + abs($q))) {
|
||||
$this->H[$i + 1][$n - 1] = (-$ra - $w * $this->H[$i][$n - 1] + $q * $this->H[$i][$n]) / $x;
|
||||
$this->H[$i + 1][$n] = (-$sa - $w * $this->H[$i][$n] - $q * $this->H[$i][$n - 1]) / $x;
|
||||
} else {
|
||||
$this->cdiv(-$r - $y * $this->H[$i][$n - 1], -$s - $y * $this->H[$i][$n], $z, $q);
|
||||
$this->H[$i + 1][$n - 1] = $this->cdivr;
|
||||
$this->H[$i + 1][$n] = $this->cdivi;
|
||||
}
|
||||
}
|
||||
// Overflow control
|
||||
$t = max(abs($this->H[$i][$n - 1]), abs($this->H[$i][$n]));
|
||||
if (($eps * $t) * $t > 1) {
|
||||
for ($j = $i; $j <= $n; ++$j) {
|
||||
$this->H[$j][$n - 1] = $this->H[$j][$n - 1] / $t;
|
||||
$this->H[$j][$n] = $this->H[$j][$n] / $t;
|
||||
}
|
||||
}
|
||||
} // end else
|
||||
} // end for
|
||||
} // end else for complex case
|
||||
} // end for
|
||||
|
||||
// Vectors of isolated roots
|
||||
for ($i = 0; $i < $nn; ++$i) {
|
||||
if ($i < $low | $i > $high) {
|
||||
for ($j = $i; $j < $nn; ++$j) {
|
||||
$this->V[$i][$j] = $this->H[$i][$j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Back transformation to get eigenvectors of original matrix
|
||||
for ($j = $nn - 1; $j >= $low; --$j) {
|
||||
for ($i = $low; $i <= $high; ++$i) {
|
||||
$z = 0.0;
|
||||
$kMax = min($j, $high);
|
||||
for ($k = $low; $k <= $kMax; ++$k) {
|
||||
$z = $z + $this->V[$i][$k] * $this->H[$k][$j];
|
||||
}
|
||||
$this->V[$i][$j] = $z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// end hqr2
|
||||
|
||||
/**
|
||||
* Constructor: Check for symmetry, then construct the eigenvalue decomposition.
|
||||
*
|
||||
* @param mixed $Arg A Square matrix
|
||||
*/
|
||||
public function __construct($Arg)
|
||||
{
|
||||
$this->A = $Arg->getArray();
|
||||
$this->n = $Arg->getColumnDimension();
|
||||
|
||||
$issymmetric = true;
|
||||
for ($j = 0; ($j < $this->n) & $issymmetric; ++$j) {
|
||||
for ($i = 0; ($i < $this->n) & $issymmetric; ++$i) {
|
||||
$issymmetric = ($this->A[$i][$j] == $this->A[$j][$i]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($issymmetric) {
|
||||
$this->V = $this->A;
|
||||
// Tridiagonalize.
|
||||
$this->tred2();
|
||||
// Diagonalize.
|
||||
$this->tql2();
|
||||
} else {
|
||||
$this->H = $this->A;
|
||||
$this->ort = [];
|
||||
// Reduce to Hessenberg form.
|
||||
$this->orthes();
|
||||
// Reduce Hessenberg to real Schur form.
|
||||
$this->hqr2();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the eigenvector matrix.
|
||||
*
|
||||
* @return Matrix V
|
||||
*/
|
||||
public function getV()
|
||||
{
|
||||
return new Matrix($this->V, $this->n, $this->n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the real parts of the eigenvalues.
|
||||
*
|
||||
* @return array real(diag(D))
|
||||
*/
|
||||
public function getRealEigenvalues()
|
||||
{
|
||||
return $this->d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the imaginary parts of the eigenvalues.
|
||||
*
|
||||
* @return array imag(diag(D))
|
||||
*/
|
||||
public function getImagEigenvalues()
|
||||
{
|
||||
return $this->e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the block diagonal eigenvalue matrix.
|
||||
*
|
||||
* @return Matrix D
|
||||
*/
|
||||
public function getD()
|
||||
{
|
||||
for ($i = 0; $i < $this->n; ++$i) {
|
||||
$D[$i] = array_fill(0, $this->n, 0.0);
|
||||
$D[$i][$i] = $this->d[$i];
|
||||
if ($this->e[$i] == 0) {
|
||||
continue;
|
||||
}
|
||||
$o = ($this->e[$i] > 0) ? $i + 1 : $i - 1;
|
||||
$D[$i][$o] = $this->e[$i];
|
||||
}
|
||||
|
||||
return new Matrix($D);
|
||||
}
|
||||
}
|
@@ -64,7 +64,7 @@ class LUDecomposition
|
||||
/**
|
||||
* LU Decomposition constructor.
|
||||
*
|
||||
* @param Matrix $A Rectangular matrix
|
||||
* @param ?Matrix $A Rectangular matrix
|
||||
*/
|
||||
public function __construct($A)
|
||||
{
|
||||
@@ -77,7 +77,7 @@ class LUDecomposition
|
||||
$this->piv[$i] = $i;
|
||||
}
|
||||
$this->pivsign = 1;
|
||||
$LUrowi = $LUcolj = [];
|
||||
$LUcolj = [];
|
||||
|
||||
// Outer loop.
|
||||
for ($j = 0; $j < $this->n; ++$j) {
|
||||
@@ -135,6 +135,7 @@ class LUDecomposition
|
||||
*/
|
||||
public function getL()
|
||||
{
|
||||
$L = [];
|
||||
for ($i = 0; $i < $this->m; ++$i) {
|
||||
for ($j = 0; $j < $this->n; ++$j) {
|
||||
if ($i > $j) {
|
||||
@@ -159,6 +160,7 @@ class LUDecomposition
|
||||
*/
|
||||
public function getU()
|
||||
{
|
||||
$U = [];
|
||||
for ($i = 0; $i < $this->n; ++$i) {
|
||||
for ($j = 0; $j < $this->n; ++$j) {
|
||||
if ($i <= $j) {
|
||||
@@ -189,7 +191,9 @@ class LUDecomposition
|
||||
/**
|
||||
* Alias for getPivot.
|
||||
*
|
||||
* @see getPivot
|
||||
* @see getPivot
|
||||
*
|
||||
* @return array Pivot vector
|
||||
*/
|
||||
public function getDoublePivot()
|
||||
{
|
||||
@@ -219,7 +223,7 @@ class LUDecomposition
|
||||
/**
|
||||
* Count determinants.
|
||||
*
|
||||
* @return array d matrix deterninat
|
||||
* @return float
|
||||
*/
|
||||
public function det()
|
||||
{
|
||||
@@ -240,11 +244,11 @@ class LUDecomposition
|
||||
/**
|
||||
* Solve A*X = B.
|
||||
*
|
||||
* @param mixed $B a Matrix with as many rows as A and any number of columns
|
||||
* @param Matrix $B a Matrix with as many rows as A and any number of columns
|
||||
*
|
||||
* @return Matrix X so that L*U*X = B(piv,:)
|
||||
*/
|
||||
public function solve($B)
|
||||
public function solve(Matrix $B)
|
||||
{
|
||||
if ($B->getRowDimension() == $this->m) {
|
||||
if ($this->isNonsingular()) {
|
||||
|
@@ -2,9 +2,10 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\JAMA;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\FormattedNumber;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
|
||||
/**
|
||||
* Matrix class.
|
||||
@@ -66,21 +67,21 @@ class Matrix
|
||||
$this->A = $args[0];
|
||||
|
||||
break;
|
||||
//Square matrix - n x n
|
||||
//Square matrix - n x n
|
||||
case 'integer':
|
||||
$this->m = $args[0];
|
||||
$this->n = $args[0];
|
||||
$this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0));
|
||||
|
||||
break;
|
||||
//Rectangular matrix - m x n
|
||||
//Rectangular matrix - m x n
|
||||
case 'integer,integer':
|
||||
$this->m = $args[0];
|
||||
$this->n = $args[1];
|
||||
$this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0));
|
||||
|
||||
break;
|
||||
//Rectangular matrix - m x n initialized from packed array
|
||||
//Rectangular matrix - m x n initialized from packed array
|
||||
case 'array,integer':
|
||||
$this->m = $args[1];
|
||||
if ($this->m != 0) {
|
||||
@@ -147,7 +148,7 @@ class Matrix
|
||||
* @param int $i Row position
|
||||
* @param int $j Column position
|
||||
*
|
||||
* @return mixed Element (int/float/double)
|
||||
* @return float|int
|
||||
*/
|
||||
public function get($i = null, $j = null)
|
||||
{
|
||||
@@ -188,9 +189,7 @@ class Matrix
|
||||
}
|
||||
|
||||
return $R;
|
||||
|
||||
break;
|
||||
//A($i0...$iF; $j0...$jF)
|
||||
//A($i0...$iF; $j0...$jF)
|
||||
case 'integer,integer,integer,integer':
|
||||
[$i0, $iF, $j0, $jF] = $args;
|
||||
if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) {
|
||||
@@ -211,9 +210,7 @@ class Matrix
|
||||
}
|
||||
|
||||
return $R;
|
||||
|
||||
break;
|
||||
//$R = array of row indices; $C = array of column indices
|
||||
//$R = array of row indices; $C = array of column indices
|
||||
case 'array,array':
|
||||
[$RL, $CL] = $args;
|
||||
if (count($RL) > 0) {
|
||||
@@ -234,9 +231,7 @@ class Matrix
|
||||
}
|
||||
|
||||
return $R;
|
||||
|
||||
break;
|
||||
//A($i0...$iF); $CL = array of column indices
|
||||
//A($i0...$iF); $CL = array of column indices
|
||||
case 'integer,integer,array':
|
||||
[$i0, $iF, $CL] = $args;
|
||||
if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) {
|
||||
@@ -257,9 +252,7 @@ class Matrix
|
||||
}
|
||||
|
||||
return $R;
|
||||
|
||||
break;
|
||||
//$RL = array of row indices
|
||||
//$RL = array of row indices
|
||||
case 'array,integer,integer':
|
||||
[$RL, $j0, $jF] = $args;
|
||||
if (count($RL) > 0) {
|
||||
@@ -280,8 +273,6 @@ class Matrix
|
||||
}
|
||||
|
||||
return $R;
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION);
|
||||
|
||||
@@ -323,11 +314,9 @@ class Matrix
|
||||
*
|
||||
* @param int $i Row position
|
||||
* @param int $j Column position
|
||||
* @param mixed $c Int/float/double value
|
||||
*
|
||||
* @return mixed Element (int/float/double)
|
||||
* @param float|int $c value
|
||||
*/
|
||||
public function set($i = null, $j = null, $c = null)
|
||||
public function set($i = null, $j = null, $c = null): void
|
||||
{
|
||||
// Optimized set version just has this
|
||||
$this->A[$i][$j] = $c;
|
||||
@@ -456,17 +445,6 @@ class Matrix
|
||||
return $s;
|
||||
}
|
||||
|
||||
/**
|
||||
* uminus.
|
||||
*
|
||||
* Unary minus matrix -A
|
||||
*
|
||||
* @return Matrix Unary minus matrix
|
||||
*/
|
||||
public function uminus()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* plus.
|
||||
*
|
||||
@@ -545,18 +523,12 @@ class Matrix
|
||||
for ($j = 0; $j < $this->n; ++$j) {
|
||||
$validValues = true;
|
||||
$value = $M->get($i, $j);
|
||||
if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) {
|
||||
$this->A[$i][$j] = trim($this->A[$i][$j], '"');
|
||||
$validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]);
|
||||
}
|
||||
if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) {
|
||||
$value = trim($value, '"');
|
||||
$validValues &= StringHelper::convertToNumberIfFraction($value);
|
||||
}
|
||||
[$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues);
|
||||
[$value, $validValues] = $this->validateExtractedValue($value, /** @scrutinizer ignore-type */ $validValues);
|
||||
if ($validValues) {
|
||||
$this->A[$i][$j] += $value;
|
||||
} else {
|
||||
$this->A[$i][$j] = Functions::NAN();
|
||||
$this->A[$i][$j] = ExcelError::NAN();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -645,18 +617,12 @@ class Matrix
|
||||
for ($j = 0; $j < $this->n; ++$j) {
|
||||
$validValues = true;
|
||||
$value = $M->get($i, $j);
|
||||
if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) {
|
||||
$this->A[$i][$j] = trim($this->A[$i][$j], '"');
|
||||
$validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]);
|
||||
}
|
||||
if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) {
|
||||
$value = trim($value, '"');
|
||||
$validValues &= StringHelper::convertToNumberIfFraction($value);
|
||||
}
|
||||
[$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues);
|
||||
[$value, $validValues] = $this->validateExtractedValue($value, /** @scrutinizer ignore-type */ $validValues);
|
||||
if ($validValues) {
|
||||
$this->A[$i][$j] -= $value;
|
||||
} else {
|
||||
$this->A[$i][$j] = Functions::NAN();
|
||||
$this->A[$i][$j] = ExcelError::NAN();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -747,18 +713,12 @@ class Matrix
|
||||
for ($j = 0; $j < $this->n; ++$j) {
|
||||
$validValues = true;
|
||||
$value = $M->get($i, $j);
|
||||
if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) {
|
||||
$this->A[$i][$j] = trim($this->A[$i][$j], '"');
|
||||
$validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]);
|
||||
}
|
||||
if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) {
|
||||
$value = trim($value, '"');
|
||||
$validValues &= StringHelper::convertToNumberIfFraction($value);
|
||||
}
|
||||
[$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues);
|
||||
[$value, $validValues] = $this->validateExtractedValue($value, /** @scrutinizer ignore-type */ $validValues);
|
||||
if ($validValues) {
|
||||
$this->A[$i][$j] *= $value;
|
||||
} else {
|
||||
$this->A[$i][$j] = Functions::NAN();
|
||||
$this->A[$i][$j] = ExcelError::NAN();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -805,23 +765,17 @@ class Matrix
|
||||
for ($j = 0; $j < $this->n; ++$j) {
|
||||
$validValues = true;
|
||||
$value = $M->get($i, $j);
|
||||
if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) {
|
||||
$this->A[$i][$j] = trim($this->A[$i][$j], '"');
|
||||
$validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]);
|
||||
}
|
||||
if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) {
|
||||
$value = trim($value, '"');
|
||||
$validValues &= StringHelper::convertToNumberIfFraction($value);
|
||||
}
|
||||
[$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues);
|
||||
[$value, $validValues] = $this->validateExtractedValue($value, /** @scrutinizer ignore-type */ $validValues);
|
||||
if ($validValues) {
|
||||
if ($value == 0) {
|
||||
// Trap for Divide by Zero error
|
||||
$M->set($i, $j, '#DIV/0!');
|
||||
$M->set($i, $j, /** @scrutinizer ignore-type */ '#DIV/0!');
|
||||
} else {
|
||||
$M->set($i, $j, $this->A[$i][$j] / $value);
|
||||
}
|
||||
} else {
|
||||
$M->set($i, $j, Functions::NAN());
|
||||
$M->set($i, $j, /** @scrutinizer ignore-type */ ExcelError::NAN());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1092,18 +1046,12 @@ class Matrix
|
||||
for ($j = 0; $j < $this->n; ++$j) {
|
||||
$validValues = true;
|
||||
$value = $M->get($i, $j);
|
||||
if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) {
|
||||
$this->A[$i][$j] = trim($this->A[$i][$j], '"');
|
||||
$validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]);
|
||||
}
|
||||
if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) {
|
||||
$value = trim($value, '"');
|
||||
$validValues &= StringHelper::convertToNumberIfFraction($value);
|
||||
}
|
||||
[$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues);
|
||||
[$value, $validValues] = $this->validateExtractedValue($value, /** @scrutinizer ignore-type */ $validValues);
|
||||
if ($validValues) {
|
||||
$this->A[$i][$j] = $this->A[$i][$j] ** $value;
|
||||
} else {
|
||||
$this->A[$i][$j] = Functions::NAN();
|
||||
$this->A[$i][$j] = ExcelError::NAN();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1147,6 +1095,7 @@ class Matrix
|
||||
$this->checkMatrixDimensions($M);
|
||||
for ($i = 0; $i < $this->m; ++$i) {
|
||||
for ($j = 0; $j < $this->n; ++$j) {
|
||||
// @phpstan-ignore-next-line
|
||||
$this->A[$i][$j] = trim($this->A[$i][$j], '"') . trim($M->get($i, $j), '"');
|
||||
}
|
||||
}
|
||||
@@ -1164,7 +1113,7 @@ class Matrix
|
||||
*
|
||||
* @return Matrix ... Solution if A is square, least squares solution otherwise
|
||||
*/
|
||||
public function solve($B)
|
||||
public function solve(self $B)
|
||||
{
|
||||
if ($this->m == $this->n) {
|
||||
$LU = new LUDecomposition($this);
|
||||
@@ -1199,4 +1148,20 @@ class Matrix
|
||||
|
||||
return $L->det();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function validateExtractedValue($value, bool $validValues): array
|
||||
{
|
||||
if (!is_numeric($value) && is_array($value)) {
|
||||
$value = Functions::flattenArray($value)[0];
|
||||
}
|
||||
if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) {
|
||||
$value = trim($value, '"');
|
||||
$validValues &= FormattedNumber::convertToNumberIfFormatted($value);
|
||||
}
|
||||
|
||||
return [$value, $validValues];
|
||||
}
|
||||
}
|
||||
|
@@ -15,9 +15,9 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
|
||||
* of simultaneous linear equations. This will fail if isFullRank()
|
||||
* returns false.
|
||||
*
|
||||
* @author Paul Meagher
|
||||
* @author Paul Meagher
|
||||
*
|
||||
* @version 1.1
|
||||
* @version 1.1
|
||||
*/
|
||||
class QRDecomposition
|
||||
{
|
||||
@@ -54,47 +54,43 @@ class QRDecomposition
|
||||
/**
|
||||
* QR Decomposition computed by Householder reflections.
|
||||
*
|
||||
* @param matrix $A Rectangular matrix
|
||||
* @param Matrix $A Rectangular matrix
|
||||
*/
|
||||
public function __construct($A)
|
||||
public function __construct(Matrix $A)
|
||||
{
|
||||
if ($A instanceof Matrix) {
|
||||
// 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;
|
||||
// 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]);
|
||||
}
|
||||
} else {
|
||||
throw new CalculationException(Matrix::ARGUMENT_TYPE_EXCEPTION);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,13 +201,13 @@ class QRDecomposition
|
||||
*
|
||||
* @return Matrix matrix that minimizes the two norm of Q*R*X-B
|
||||
*/
|
||||
public function solve($B)
|
||||
public function solve(Matrix $B)
|
||||
{
|
||||
if ($B->getRowDimension() == $this->m) {
|
||||
if ($this->isFullRank()) {
|
||||
// Copy right hand side
|
||||
$nx = $B->getColumnDimension();
|
||||
$X = $B->getArrayCopy();
|
||||
$X = $B->getArray();
|
||||
// Compute Y = transpose(Q)*B
|
||||
for ($k = 0; $k < $this->n; ++$k) {
|
||||
for ($j = 0; $j < $nx; ++$j) {
|
||||
|
@@ -1,528 +0,0 @@
|
||||
<?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->getArrayCopy();
|
||||
$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()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
@@ -21,6 +21,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
// +----------------------------------------------------------------------+
|
||||
//
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\OLE\ChainedBlockStream;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\Root;
|
||||
@@ -109,15 +110,15 @@ class OLE
|
||||
*
|
||||
* @acces public
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $filename
|
||||
*
|
||||
* @return bool true on success, PEAR_Error on failure
|
||||
*/
|
||||
public function read($file)
|
||||
public function read($filename)
|
||||
{
|
||||
$fh = fopen($file, 'rb');
|
||||
if (!$fh) {
|
||||
throw new ReaderException("Can't open file $file");
|
||||
$fh = fopen($filename, 'rb');
|
||||
if ($fh === false) {
|
||||
throw new ReaderException("Can't open file $filename");
|
||||
}
|
||||
$this->_file_handle = $fh;
|
||||
|
||||
@@ -227,7 +228,8 @@ class OLE
|
||||
// in OLE_ChainedBlockStream::stream_open().
|
||||
// Object is removed from self::$instances in OLE_Stream::close().
|
||||
$GLOBALS['_OLE_INSTANCES'][] = $this;
|
||||
$instanceId = end(array_keys($GLOBALS['_OLE_INSTANCES']));
|
||||
$keys = array_keys($GLOBALS['_OLE_INSTANCES']);
|
||||
$instanceId = end($keys);
|
||||
|
||||
$path = 'ole-chainedblockstream://oleInstanceId=' . $instanceId;
|
||||
if ($blockIdOrPps instanceof OLE\PPS) {
|
||||
@@ -243,13 +245,14 @@ class OLE
|
||||
/**
|
||||
* Reads a signed char.
|
||||
*
|
||||
* @param resource $fh file handle
|
||||
* @param resource $fileHandle file handle
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function readInt1($fh)
|
||||
private static function readInt1($fileHandle)
|
||||
{
|
||||
[, $tmp] = unpack('c', fread($fh, 1));
|
||||
// @phpstan-ignore-next-line
|
||||
[, $tmp] = unpack('c', fread($fileHandle, 1));
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
@@ -257,13 +260,14 @@ class OLE
|
||||
/**
|
||||
* Reads an unsigned short (2 octets).
|
||||
*
|
||||
* @param resource $fh file handle
|
||||
* @param resource $fileHandle file handle
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function readInt2($fh)
|
||||
private static function readInt2($fileHandle)
|
||||
{
|
||||
[, $tmp] = unpack('v', fread($fh, 2));
|
||||
// @phpstan-ignore-next-line
|
||||
[, $tmp] = unpack('v', fread($fileHandle, 2));
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
@@ -271,13 +275,14 @@ class OLE
|
||||
/**
|
||||
* Reads an unsigned long (4 octets).
|
||||
*
|
||||
* @param resource $fh file handle
|
||||
* @param resource $fileHandle file handle
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function readInt4($fh)
|
||||
private static function readInt4($fileHandle)
|
||||
{
|
||||
[, $tmp] = unpack('V', fread($fh, 4));
|
||||
// @phpstan-ignore-next-line
|
||||
[, $tmp] = unpack('V', fread($fileHandle, 4));
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
@@ -316,7 +321,7 @@ class OLE
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
throw new Exception('Unsupported PPS type');
|
||||
}
|
||||
fseek($fh, 1, SEEK_CUR);
|
||||
$pps->Type = $type;
|
||||
@@ -344,7 +349,7 @@ class OLE
|
||||
if ($pps->Type == self::OLE_PPS_TYPE_DIR || $pps->Type == self::OLE_PPS_TYPE_ROOT) {
|
||||
$nos = [$pps->DirPps];
|
||||
$pps->children = [];
|
||||
while ($nos) {
|
||||
while (!empty($nos)) {
|
||||
$no = array_pop($nos);
|
||||
if ($no != -1) {
|
||||
$childPps = $this->_list[$no];
|
||||
@@ -489,42 +494,33 @@ class OLE
|
||||
* Utility function
|
||||
* Returns a string for the OLE container with the date given.
|
||||
*
|
||||
* @param int $date A timestamp
|
||||
* @param float|int $date A timestamp
|
||||
*
|
||||
* @return string The string for the OLE container
|
||||
*/
|
||||
public static function localDateToOLE($date)
|
||||
{
|
||||
if (!isset($date)) {
|
||||
if (!$date) {
|
||||
return "\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
}
|
||||
|
||||
// factor used for separating numbers into 4 bytes parts
|
||||
$factor = 2 ** 32;
|
||||
$dateTime = Date::dateTimeFromTimestamp("$date");
|
||||
|
||||
// days from 1-1-1601 until the beggining of UNIX era
|
||||
$days = 134774;
|
||||
// calculate seconds
|
||||
$big_date = $days * 24 * 3600 + mktime((int) date('H', $date), (int) date('i', $date), (int) date('s', $date), (int) date('m', $date), (int) date('d', $date), (int) date('Y', $date));
|
||||
$big_date = $days * 24 * 3600 + (float) $dateTime->format('U');
|
||||
// multiply just to make MS happy
|
||||
$big_date *= 10000000;
|
||||
|
||||
$high_part = floor($big_date / $factor);
|
||||
// lower 4 bytes
|
||||
$low_part = floor((($big_date / $factor) - $high_part) * $factor);
|
||||
|
||||
// Make HEX string
|
||||
$res = '';
|
||||
|
||||
for ($i = 0; $i < 4; ++$i) {
|
||||
$hex = $low_part % 0x100;
|
||||
$res .= pack('c', $hex);
|
||||
$low_part /= 0x100;
|
||||
}
|
||||
for ($i = 0; $i < 4; ++$i) {
|
||||
$hex = $high_part % 0x100;
|
||||
$res .= pack('c', $hex);
|
||||
$high_part /= 0x100;
|
||||
$factor = 2 ** 56;
|
||||
while ($factor >= 1) {
|
||||
$hex = (int) floor($big_date / $factor);
|
||||
$res = pack('c', $hex) . $res;
|
||||
$big_date = fmod($big_date, $factor);
|
||||
$factor /= 256;
|
||||
}
|
||||
|
||||
return $res;
|
||||
@@ -535,7 +531,7 @@ class OLE
|
||||
*
|
||||
* @param string $oleTimestamp A binary string with the encoded date
|
||||
*
|
||||
* @return int The Unix timestamp corresponding to the string
|
||||
* @return float|int The Unix timestamp corresponding to the string
|
||||
*/
|
||||
public static function OLE2LocalDate($oleTimestamp)
|
||||
{
|
||||
@@ -558,9 +554,6 @@ class OLE
|
||||
// translate to seconds since 1970:
|
||||
$unixTimestamp = floor(65536.0 * 65536.0 * $timestampHigh + $timestampLow - $days * 24 * 3600 + 0.5);
|
||||
|
||||
$iTimestamp = (int) $unixTimestamp;
|
||||
|
||||
// Overflow conditions can't happen on 64-bit system
|
||||
return ($iTimestamp == $unixTimestamp) ? $iTimestamp : ($unixTimestamp >= 0.0 ? PHP_INT_MAX : PHP_INT_MIN);
|
||||
return IntOrFloat::evaluate($unixTimestamp);
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ class ChainedBlockStream
|
||||
/**
|
||||
* The OLE container of the file that is being read.
|
||||
*
|
||||
* @var OLE
|
||||
* @var null|OLE
|
||||
*/
|
||||
public $ole;
|
||||
|
||||
@@ -42,7 +42,7 @@ class ChainedBlockStream
|
||||
* ole-chainedblockstream://oleInstanceId=1
|
||||
* @param string $mode only "r" is supported
|
||||
* @param int $options mask of STREAM_REPORT_ERRORS and STREAM_USE_PATH
|
||||
* @param string &$openedPath absolute path of the opened stream (out parameter)
|
||||
* @param string $openedPath absolute path of the opened stream (out parameter)
|
||||
*
|
||||
* @return bool true on success
|
||||
*/
|
||||
@@ -112,7 +112,7 @@ class ChainedBlockStream
|
||||
*
|
||||
* @param int $count maximum number of bytes to read
|
||||
*
|
||||
* @return string
|
||||
* @return false|string
|
||||
*/
|
||||
public function stream_read($count) // @codingStandardsIgnoreLine
|
||||
{
|
||||
@@ -160,7 +160,8 @@ class ChainedBlockStream
|
||||
$this->pos = $offset;
|
||||
} elseif ($whence == SEEK_CUR && -$offset <= $this->pos) {
|
||||
$this->pos += $offset;
|
||||
} elseif ($whence == SEEK_END && -$offset <= count($this->data)) {
|
||||
// @phpstan-ignore-next-line
|
||||
} elseif ($whence == SEEK_END && -$offset <= count(/** @scrutinizer ignore-type */ $this->data)) {
|
||||
$this->pos = strlen($this->data) + $offset;
|
||||
} else {
|
||||
return false;
|
||||
|
@@ -74,14 +74,14 @@ class PPS
|
||||
/**
|
||||
* A timestamp.
|
||||
*
|
||||
* @var int
|
||||
* @var float|int
|
||||
*/
|
||||
public $Time1st;
|
||||
|
||||
/**
|
||||
* A timestamp.
|
||||
*
|
||||
* @var int
|
||||
* @var float|int
|
||||
*/
|
||||
public $Time2nd;
|
||||
|
||||
@@ -129,8 +129,8 @@ class PPS
|
||||
* @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 int $time_1st A timestamp
|
||||
* @param int $time_2nd A timestamp
|
||||
* @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
|
||||
*/
|
||||
@@ -142,8 +142,8 @@ class PPS
|
||||
$this->PrevPps = $prev;
|
||||
$this->NextPps = $next;
|
||||
$this->DirPps = $dir;
|
||||
$this->Time1st = $time_1st;
|
||||
$this->Time2nd = $time_2nd;
|
||||
$this->Time1st = $time_1st ?? 0;
|
||||
$this->Time2nd = $time_2nd ?? 0;
|
||||
$this->_data = $data;
|
||||
$this->children = $children;
|
||||
if ($data != '') {
|
||||
@@ -189,7 +189,7 @@ class PPS
|
||||
. "\x00\x00\x00\x00" // 100
|
||||
. OLE::localDateToOLE($this->Time1st) // 108
|
||||
. OLE::localDateToOLE($this->Time2nd) // 116
|
||||
. pack('V', isset($this->startBlock) ? $this->startBlock : 0) // 120
|
||||
. pack('V', $this->startBlock ?? 0) // 120
|
||||
. pack('V', $this->Size) // 124
|
||||
. pack('V', 0); // 128
|
||||
|
||||
@@ -200,7 +200,7 @@ class PPS
|
||||
* 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
|
||||
* @param array $raList Reference to the array of PPS's for the whole OLE
|
||||
* container
|
||||
* @param mixed $to_save
|
||||
* @param mixed $depth
|
||||
@@ -220,7 +220,7 @@ class PPS
|
||||
$raList[$cnt]->NextPps = 0xFFFFFFFF;
|
||||
$raList[$cnt]->DirPps = self::savePpsSetPnt($raList, @$raList[$cnt]->children, $depth++);
|
||||
} else {
|
||||
$iPos = floor(count($to_save) / 2);
|
||||
$iPos = (int) floor(count($to_save) / 2);
|
||||
$aPrev = array_slice($to_save, 0, $iPos);
|
||||
$aNext = array_slice($to_save, $iPos + 1);
|
||||
$cnt = count($raList);
|
||||
|
@@ -46,8 +46,8 @@ class Root extends PPS
|
||||
private $bigBlockSize;
|
||||
|
||||
/**
|
||||
* @param int $time_1st A timestamp
|
||||
* @param int $time_2nd A timestamp
|
||||
* @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)
|
||||
@@ -71,12 +71,12 @@ class Root extends PPS
|
||||
$this->fileHandle = $fileHandle;
|
||||
|
||||
// Initial Setting for saving
|
||||
$this->bigBlockSize = 2 ** (
|
||||
$this->bigBlockSize = (int) (2 ** (
|
||||
(isset($this->bigBlockSize)) ? self::adjust2($this->bigBlockSize) : 9
|
||||
);
|
||||
$this->smallBlockSize = 2 ** (
|
||||
));
|
||||
$this->smallBlockSize = (int) (2 ** (
|
||||
(isset($this->smallBlockSize)) ? self::adjust2($this->smallBlockSize) : 6
|
||||
);
|
||||
));
|
||||
|
||||
// Make an array of PPS's (for Save)
|
||||
$aList = [];
|
||||
@@ -84,17 +84,17 @@ class Root extends PPS
|
||||
// calculate values for header
|
||||
[$iSBDcnt, $iBBcnt, $iPPScnt] = $this->calcSize($aList); //, $rhInfo);
|
||||
// Save Header
|
||||
$this->saveHeader($iSBDcnt, $iBBcnt, $iPPScnt);
|
||||
$this->saveHeader((int) $iSBDcnt, (int) $iBBcnt, (int) $iPPScnt);
|
||||
|
||||
// Make Small Data string (write SBD)
|
||||
$this->_data = $this->makeSmallData($aList);
|
||||
|
||||
// Write BB
|
||||
$this->saveBigData($iSBDcnt, $aList);
|
||||
$this->saveBigData((int) $iSBDcnt, $aList);
|
||||
// Write PPS
|
||||
$this->savePps($aList);
|
||||
// Write Big Block Depot and BDList and Adding Header informations
|
||||
$this->saveBbd($iSBDcnt, $iBBcnt, $iPPScnt);
|
||||
$this->saveBbd((int) $iSBDcnt, (int) $iBBcnt, (int) $iPPScnt);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -110,7 +110,6 @@ class Root extends PPS
|
||||
{
|
||||
// Calculate Basic Setting
|
||||
[$iSBDcnt, $iBBcnt, $iPPScnt] = [0, 0, 0];
|
||||
$iSmallLen = 0;
|
||||
$iSBcnt = 0;
|
||||
$iCount = count($raList);
|
||||
for ($i = 0; $i < $iCount; ++$i) {
|
||||
@@ -237,7 +236,7 @@ class Root extends PPS
|
||||
* 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
|
||||
* @param array $raList Reference to array of PPS's
|
||||
*/
|
||||
private function saveBigData($iStBlk, &$raList): void
|
||||
{
|
||||
@@ -267,7 +266,7 @@ class Root extends PPS
|
||||
/**
|
||||
* 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
|
||||
* @param array $raList Reference to array of PPS's
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
@@ -92,25 +92,23 @@ class OLERead
|
||||
|
||||
/**
|
||||
* Read the file.
|
||||
*
|
||||
* @param $pFilename string Filename
|
||||
*/
|
||||
public function read($pFilename): void
|
||||
public function read(string $filename): void
|
||||
{
|
||||
File::assertFile($pFilename);
|
||||
File::assertFile($filename);
|
||||
|
||||
// Get the file identifier
|
||||
// Don't bother reading the whole file until we know it's a valid OLE file
|
||||
$this->data = file_get_contents($pFilename, false, null, 0, 8);
|
||||
$this->data = file_get_contents($filename, false, null, 0, 8);
|
||||
|
||||
// Check OLE identifier
|
||||
$identifierOle = pack('CCCCCCCC', 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1);
|
||||
if ($this->data != $identifierOle) {
|
||||
throw new ReaderException('The filename ' . $pFilename . ' is not recognised as an OLE file');
|
||||
throw new ReaderException('The filename ' . $filename . ' is not recognised as an OLE file');
|
||||
}
|
||||
|
||||
// Get the file data
|
||||
$this->data = file_get_contents($pFilename);
|
||||
$this->data = file_get_contents($filename);
|
||||
|
||||
// Total number of sectors used for the SAT
|
||||
$this->numBigBlockDepotBlocks = self::getInt4d($this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS);
|
||||
@@ -166,7 +164,6 @@ class OLERead
|
||||
$pos += 4 * $bbs;
|
||||
}
|
||||
|
||||
$pos = 0;
|
||||
$sbdBlock = $this->sbdStartBlock;
|
||||
$this->smallBlockChain = '';
|
||||
while ($sbdBlock != -2) {
|
||||
@@ -188,9 +185,9 @@ class OLERead
|
||||
/**
|
||||
* Extract binary stream data.
|
||||
*
|
||||
* @param int $stream
|
||||
* @param ?int $stream
|
||||
*
|
||||
* @return string
|
||||
* @return null|string
|
||||
*/
|
||||
public function getStream($stream)
|
||||
{
|
||||
@@ -237,13 +234,12 @@ class OLERead
|
||||
/**
|
||||
* Read a standard stream (by joining sectors using information from SAT).
|
||||
*
|
||||
* @param int $bl Sector ID where the stream starts
|
||||
* @param int $block Sector ID where the stream starts
|
||||
*
|
||||
* @return string Data for standard stream
|
||||
*/
|
||||
private function readData($bl)
|
||||
private function readData($block)
|
||||
{
|
||||
$block = $bl;
|
||||
$data = '';
|
||||
|
||||
while ($block != -2) {
|
||||
|
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as SpException;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Protection;
|
||||
|
||||
class PasswordHasher
|
||||
{
|
||||
const MAX_PASSWORD_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Get algorithm name for PHP.
|
||||
*/
|
||||
@@ -34,36 +36,40 @@ class PasswordHasher
|
||||
return $mapping[$algorithmName];
|
||||
}
|
||||
|
||||
throw new Exception('Unsupported password algorithm: ' . $algorithmName);
|
||||
throw new SpException('Unsupported password algorithm: ' . $algorithmName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a password hash from a given string.
|
||||
*
|
||||
* This method is based on the algorithm provided by
|
||||
* This method is based on the spec at:
|
||||
* https://interoperability.blob.core.windows.net/files/MS-OFFCRYPTO/[MS-OFFCRYPTO].pdf
|
||||
* 2.3.7.1 Binary Document Password Verifier Derivation Method 1
|
||||
*
|
||||
* It replaces a method based on the algorithm provided by
|
||||
* Daniel Rentz of OpenOffice and the PEAR package
|
||||
* Spreadsheet_Excel_Writer by Xavier Noguer <xnoguer@rezebra.com>.
|
||||
*
|
||||
* @param string $pPassword Password to hash
|
||||
* Scrutinizer will squawk at the use of bitwise operations here,
|
||||
* but it should ultimately pass.
|
||||
*
|
||||
* @param string $password Password to hash
|
||||
*/
|
||||
private static function defaultHashPassword(string $pPassword): string
|
||||
private static function defaultHashPassword(string $password): string
|
||||
{
|
||||
$password = 0x0000;
|
||||
$charPos = 1; // char position
|
||||
|
||||
// split the plain text password in its component characters
|
||||
$chars = preg_split('//', $pPassword, -1, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($chars as $char) {
|
||||
$value = ord($char) << $charPos++; // shifted ASCII value
|
||||
$rotated_bits = $value >> 15; // rotated bits beyond bit 15
|
||||
$value &= 0x7fff; // first 15 bits
|
||||
$password ^= ($value | $rotated_bits);
|
||||
$verifier = 0;
|
||||
$pwlen = strlen($password);
|
||||
$passwordArray = pack('c', $pwlen) . $password;
|
||||
for ($i = $pwlen; $i >= 0; --$i) {
|
||||
$intermediate1 = (($verifier & 0x4000) === 0) ? 0 : 1;
|
||||
$intermediate2 = 2 * $verifier;
|
||||
$intermediate2 = $intermediate2 & 0x7fff;
|
||||
$intermediate3 = $intermediate1 | $intermediate2;
|
||||
$verifier = $intermediate3 ^ ord($passwordArray[$i]);
|
||||
}
|
||||
$verifier ^= 0xCE4B;
|
||||
|
||||
$password ^= strlen($pPassword);
|
||||
$password ^= 0xCE4B;
|
||||
|
||||
return strtoupper(dechex($password));
|
||||
return strtoupper(dechex($verifier));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,6 +88,9 @@ class PasswordHasher
|
||||
*/
|
||||
public static function hashPassword(string $password, string $algorithm = '', string $salt = '', int $spinCount = 10000): string
|
||||
{
|
||||
if (strlen($password) > self::MAX_PASSWORD_LENGTH) {
|
||||
throw new SpException('Password exceeds ' . self::MAX_PASSWORD_LENGTH . ' characters');
|
||||
}
|
||||
$phpAlgorithm = self::getAlgorithm($algorithm);
|
||||
if (!$phpAlgorithm) {
|
||||
return self::defaultHashPassword($password);
|
||||
@@ -90,7 +99,7 @@ class PasswordHasher
|
||||
$saltValue = base64_decode($salt);
|
||||
$encodedPassword = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8');
|
||||
|
||||
$hashValue = hash($phpAlgorithm, $saltValue . $encodedPassword, true);
|
||||
$hashValue = hash($phpAlgorithm, $saltValue . /** @scrutinizer ignore-type */ $encodedPassword, true);
|
||||
for ($i = 0; $i < $spinCount; ++$i) {
|
||||
$hashValue = hash($phpAlgorithm, $hashValue . pack('L', $i), true);
|
||||
}
|
||||
|
@@ -2,15 +2,8 @@
|
||||
|
||||
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.
|
||||
*
|
||||
@@ -28,14 +21,14 @@ class StringHelper
|
||||
/**
|
||||
* Decimal separator.
|
||||
*
|
||||
* @var string
|
||||
* @var ?string
|
||||
*/
|
||||
private static $decimalSeparator;
|
||||
|
||||
/**
|
||||
* Thousands separator.
|
||||
*
|
||||
* @var string
|
||||
* @var ?string
|
||||
*/
|
||||
private static $thousandsSeparator;
|
||||
|
||||
@@ -49,7 +42,7 @@ class StringHelper
|
||||
/**
|
||||
* Is iconv extension avalable?
|
||||
*
|
||||
* @var bool
|
||||
* @var ?bool
|
||||
*/
|
||||
private static $isIconvEnabled;
|
||||
|
||||
@@ -294,15 +287,15 @@ class StringHelper
|
||||
* 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 $value Value to unescape
|
||||
* @param string $textValue Value to unescape
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function controlCharacterOOXML2PHP($value)
|
||||
public static function controlCharacterOOXML2PHP($textValue)
|
||||
{
|
||||
self::buildCharacterSets();
|
||||
|
||||
return str_replace(array_keys(self::$controlCharacters), array_values(self::$controlCharacters), $value);
|
||||
return str_replace(array_keys(self::$controlCharacters), array_values(self::$controlCharacters), $textValue);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,64 +309,63 @@ class StringHelper
|
||||
* 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 $value Value to escape
|
||||
* @param string $textValue Value to escape
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function controlCharacterPHP2OOXML($value)
|
||||
public static function controlCharacterPHP2OOXML($textValue)
|
||||
{
|
||||
self::buildCharacterSets();
|
||||
|
||||
return str_replace(array_values(self::$controlCharacters), array_keys(self::$controlCharacters), $value);
|
||||
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 $value
|
||||
*
|
||||
* @return string
|
||||
* Try to sanitize UTF8, replacing invalid sequences with Unicode substitution characters.
|
||||
*/
|
||||
public static function sanitizeUTF8($value)
|
||||
public static function sanitizeUTF8(string $textValue): string
|
||||
{
|
||||
if (self::getIsIconvEnabled()) {
|
||||
$value = @iconv('UTF-8', 'UTF-8', $value);
|
||||
$textValue = str_replace(["\xef\xbf\xbe", "\xef\xbf\xbf"], "\xef\xbf\xbd", $textValue);
|
||||
$subst = mb_substitute_character(); // default is question mark
|
||||
mb_substitute_character(65533); // Unicode substitution character
|
||||
// Phpstan does not think this can return false.
|
||||
$returnValue = mb_convert_encoding($textValue, 'UTF-8', 'UTF-8');
|
||||
mb_substitute_character(/** @scrutinizer ignore-type */ $subst);
|
||||
|
||||
return $value;
|
||||
}
|
||||
return self::returnString($returnValue);
|
||||
}
|
||||
|
||||
$value = mb_convert_encoding($value, 'UTF-8', 'UTF-8');
|
||||
|
||||
return $value;
|
||||
/**
|
||||
* Strictly to satisfy Scrutinizer.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static function returnString($value): string
|
||||
{
|
||||
return is_string($value) ? $value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string contains UTF8 data.
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isUTF8($value)
|
||||
public static function isUTF8(string $textValue): bool
|
||||
{
|
||||
return $value === '' || preg_match('/^./su', $value) === 1;
|
||||
return $textValue === self::sanitizeUTF8($textValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $value
|
||||
*
|
||||
* @return string
|
||||
* @param float|int|string $numericValue
|
||||
*/
|
||||
public static function formatNumber($value)
|
||||
public static function formatNumber($numericValue): string
|
||||
{
|
||||
if (is_float($value)) {
|
||||
return str_replace(',', '.', $value);
|
||||
if (is_float($numericValue)) {
|
||||
return str_replace(',', '.', (string) $numericValue);
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
return (string) $numericValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -383,25 +375,23 @@ class StringHelper
|
||||
* 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 $value UTF-8 encoded string
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
* @param mixed[] $arrcRuns Details of rich text runs in $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function UTF8toBIFF8UnicodeShort($value, $arrcRuns = [])
|
||||
public static function UTF8toBIFF8UnicodeShort(string $textValue, array $arrcRuns = []): string
|
||||
{
|
||||
// character count
|
||||
$ln = self::countCharacters($value, 'UTF-8');
|
||||
$ln = self::countCharacters($textValue, 'UTF-8');
|
||||
// option flags
|
||||
if (empty($arrcRuns)) {
|
||||
$data = pack('CC', $ln, 0x0001);
|
||||
// characters
|
||||
$data .= self::convertEncoding($value, 'UTF-16LE', 'UTF-8');
|
||||
$data .= self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8');
|
||||
} else {
|
||||
$data = pack('vC', $ln, 0x09);
|
||||
$data .= pack('v', count($arrcRuns));
|
||||
// characters
|
||||
$data .= self::convertEncoding($value, 'UTF-16LE', 'UTF-8');
|
||||
$data .= self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8');
|
||||
foreach ($arrcRuns as $cRun) {
|
||||
$data .= pack('v', $cRun['strlen']);
|
||||
$data .= pack('v', $cRun['fontidx']);
|
||||
@@ -418,17 +408,15 @@ class StringHelper
|
||||
* 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 $value UTF-8 encoded string
|
||||
*
|
||||
* @return string
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
*/
|
||||
public static function UTF8toBIFF8UnicodeLong($value)
|
||||
public static function UTF8toBIFF8UnicodeLong(string $textValue): string
|
||||
{
|
||||
// character count
|
||||
$ln = self::countCharacters($value, 'UTF-8');
|
||||
$ln = self::countCharacters($textValue, 'UTF-8');
|
||||
|
||||
// characters
|
||||
$chars = self::convertEncoding($value, 'UTF-16LE', 'UTF-8');
|
||||
$chars = self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8');
|
||||
|
||||
return pack('vC', $ln, 0x0001) . $chars;
|
||||
}
|
||||
@@ -436,111 +424,102 @@ class StringHelper
|
||||
/**
|
||||
* Convert string from one encoding to another.
|
||||
*
|
||||
* @param string $value
|
||||
* @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($value, $to, $from)
|
||||
public static function convertEncoding(string $textValue, string $to, string $from): string
|
||||
{
|
||||
if (self::getIsIconvEnabled()) {
|
||||
$result = iconv($from, $to . self::$iconvOptions, $value);
|
||||
$result = iconv($from, $to . self::$iconvOptions, $textValue);
|
||||
if (false !== $result) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return mb_convert_encoding($value, $to, $from);
|
||||
return self::returnString(mb_convert_encoding($textValue, $to, $from));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get character count.
|
||||
*
|
||||
* @param string $value
|
||||
* @param string $enc Encoding
|
||||
* @param string $encoding Encoding
|
||||
*
|
||||
* @return int Character count
|
||||
*/
|
||||
public static function countCharacters($value, $enc = 'UTF-8')
|
||||
public static function countCharacters(string $textValue, string $encoding = 'UTF-8'): int
|
||||
{
|
||||
return mb_strlen($value, $enc);
|
||||
return mb_strlen($textValue, $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a substring of a UTF-8 encoded string.
|
||||
*
|
||||
* @param string $pValue UTF-8 encoded string
|
||||
* @param int $pStart Start offset
|
||||
* @param int $pLength Maximum number of characters in substring
|
||||
*
|
||||
* @return string
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
* @param int $offset Start offset
|
||||
* @param ?int $length Maximum number of characters in substring
|
||||
*/
|
||||
public static function substring($pValue, $pStart, $pLength = 0)
|
||||
public static function substring(string $textValue, int $offset, ?int $length = 0): string
|
||||
{
|
||||
return mb_substr($pValue, $pStart, $pLength, 'UTF-8');
|
||||
return mb_substr($textValue, $offset, $length, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a UTF-8 encoded string to upper case.
|
||||
*
|
||||
* @param string $pValue UTF-8 encoded string
|
||||
*
|
||||
* @return string
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
*/
|
||||
public static function strToUpper($pValue)
|
||||
public static function strToUpper(string $textValue): string
|
||||
{
|
||||
return mb_convert_case($pValue, MB_CASE_UPPER, 'UTF-8');
|
||||
return mb_convert_case($textValue, MB_CASE_UPPER, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a UTF-8 encoded string to lower case.
|
||||
*
|
||||
* @param string $pValue UTF-8 encoded string
|
||||
*
|
||||
* @return string
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
*/
|
||||
public static function strToLower($pValue)
|
||||
public static function strToLower(string $textValue): string
|
||||
{
|
||||
return mb_convert_case($pValue, MB_CASE_LOWER, 'UTF-8');
|
||||
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 $pValue UTF-8 encoded string
|
||||
*
|
||||
* @return string
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
*/
|
||||
public static function strToTitle($pValue)
|
||||
public static function strToTitle(string $textValue): string
|
||||
{
|
||||
return mb_convert_case($pValue, MB_CASE_TITLE, 'UTF-8');
|
||||
return mb_convert_case($textValue, MB_CASE_TITLE, 'UTF-8');
|
||||
}
|
||||
|
||||
public static function mbIsUpper($char)
|
||||
public static function mbIsUpper(string $character): bool
|
||||
{
|
||||
return mb_strtolower($char, 'UTF-8') != $char;
|
||||
return mb_strtolower($character, 'UTF-8') !== $character;
|
||||
}
|
||||
|
||||
public static function mbStrSplit($string)
|
||||
/**
|
||||
* Splits a UTF-8 string into an array of individual characters.
|
||||
*/
|
||||
public static function mbStrSplit(string $string): array
|
||||
{
|
||||
// Split at all position not after the start: ^
|
||||
// and not before the end: $
|
||||
return preg_split('/(?<!^)(?!$)/u', $string);
|
||||
$split = preg_split('/(?<!^)(?!$)/u', $string);
|
||||
|
||||
return ($split === false) ? [] : $split;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the case of a string, so that all uppercase characters become lowercase
|
||||
* and all lowercase characters become uppercase.
|
||||
*
|
||||
* @param string $pValue UTF-8 encoded string
|
||||
*
|
||||
* @return string
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
*/
|
||||
public static function strCaseReverse($pValue)
|
||||
public static function strCaseReverse(string $textValue): string
|
||||
{
|
||||
$characters = self::mbStrSplit($pValue);
|
||||
$characters = self::mbStrSplit($textValue);
|
||||
foreach ($characters as &$character) {
|
||||
if (self::mbIsUpper($character)) {
|
||||
$character = mb_strtolower($character, 'UTF-8');
|
||||
@@ -552,36 +531,11 @@ class StringHelper
|
||||
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()
|
||||
public static function getDecimalSeparator(): string
|
||||
{
|
||||
if (!isset(self::$decimalSeparator)) {
|
||||
$localeconv = localeconv();
|
||||
@@ -601,20 +555,18 @@ class StringHelper
|
||||
* Set the decimal separator. Only used by NumberFormat::toFormattedString()
|
||||
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
|
||||
*
|
||||
* @param string $pValue Character for decimal separator
|
||||
* @param string $separator Character for decimal separator
|
||||
*/
|
||||
public static function setDecimalSeparator($pValue): void
|
||||
public static function setDecimalSeparator(string $separator): void
|
||||
{
|
||||
self::$decimalSeparator = $pValue;
|
||||
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()
|
||||
public static function getThousandsSeparator(): string
|
||||
{
|
||||
if (!isset(self::$thousandsSeparator)) {
|
||||
$localeconv = localeconv();
|
||||
@@ -634,20 +586,18 @@ class StringHelper
|
||||
* Set the thousands separator. Only used by NumberFormat::toFormattedString()
|
||||
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
|
||||
*
|
||||
* @param string $pValue Character for thousands separator
|
||||
* @param string $separator Character for thousands separator
|
||||
*/
|
||||
public static function setThousandsSeparator($pValue): void
|
||||
public static function setThousandsSeparator(string $separator): void
|
||||
{
|
||||
self::$thousandsSeparator = $pValue;
|
||||
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()
|
||||
public static function getCurrencyCode(): string
|
||||
{
|
||||
if (!empty(self::$currencyCode)) {
|
||||
return self::$currencyCode;
|
||||
@@ -672,51 +622,51 @@ class StringHelper
|
||||
* Set the currency code. Only used by NumberFormat::toFormattedString()
|
||||
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
|
||||
*
|
||||
* @param string $pValue Character for currency code
|
||||
* @param string $currencyCode Character for currency code
|
||||
*/
|
||||
public static function setCurrencyCode($pValue): void
|
||||
public static function setCurrencyCode(string $currencyCode): void
|
||||
{
|
||||
self::$currencyCode = $pValue;
|
||||
self::$currencyCode = $currencyCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert SYLK encoded string to UTF-8.
|
||||
*
|
||||
* @param string $pValue
|
||||
* @param string $textValue SYLK encoded string
|
||||
*
|
||||
* @return string UTF-8 encoded string
|
||||
*/
|
||||
public static function SYLKtoUTF8($pValue)
|
||||
public static function SYLKtoUTF8(string $textValue): string
|
||||
{
|
||||
self::buildCharacterSets();
|
||||
|
||||
// If there is no escape character in the string there is nothing to do
|
||||
if (strpos($pValue, '') === false) {
|
||||
return $pValue;
|
||||
if (strpos($textValue, '') === false) {
|
||||
return $textValue;
|
||||
}
|
||||
|
||||
foreach (self::$SYLKCharacters as $k => $v) {
|
||||
$pValue = str_replace($k, $v, $pValue);
|
||||
$textValue = str_replace($k, $v, $textValue);
|
||||
}
|
||||
|
||||
return $pValue;
|
||||
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 $value
|
||||
* @param string $textValue
|
||||
*
|
||||
* @return mixed string or only the leading numeric part of the string
|
||||
*/
|
||||
public static function testStringAsNumeric($value)
|
||||
public static function testStringAsNumeric($textValue)
|
||||
{
|
||||
if (is_numeric($value)) {
|
||||
return $value;
|
||||
if (is_numeric($textValue)) {
|
||||
return $textValue;
|
||||
}
|
||||
$v = (float) $value;
|
||||
$v = (float) $textValue;
|
||||
|
||||
return (is_numeric(substr($value, 0, strlen($v)))) ? $v : $value;
|
||||
return (is_numeric(substr($textValue, 0, strlen((string) $v)))) ? $v : $textValue;
|
||||
}
|
||||
}
|
||||
|
@@ -17,26 +17,26 @@ class TimeZone
|
||||
/**
|
||||
* Validate a Timezone name.
|
||||
*
|
||||
* @param string $timezone Time zone (e.g. 'Europe/London')
|
||||
* @param string $timezoneName Time zone (e.g. 'Europe/London')
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
private static function validateTimeZone($timezone)
|
||||
private static function validateTimeZone(string $timezoneName): bool
|
||||
{
|
||||
return in_array($timezone, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC));
|
||||
return in_array($timezoneName, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Default Timezone used for date/time conversions.
|
||||
*
|
||||
* @param string $timezone Time zone (e.g. 'Europe/London')
|
||||
* @param string $timezoneName Time zone (e.g. 'Europe/London')
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
public static function setTimeZone($timezone)
|
||||
public static function setTimeZone(string $timezoneName): bool
|
||||
{
|
||||
if (self::validateTimezone($timezone)) {
|
||||
self::$timezone = $timezone;
|
||||
if (self::validateTimeZone($timezoneName)) {
|
||||
self::$timezone = $timezoneName;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class TimeZone
|
||||
*
|
||||
* @return string Timezone (e.g. 'Europe/London')
|
||||
*/
|
||||
public static function getTimeZone()
|
||||
public static function getTimeZone(): string
|
||||
{
|
||||
return self::$timezone;
|
||||
}
|
||||
@@ -58,24 +58,20 @@ class 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 $timezone The timezone for finding the adjustment to UST
|
||||
* @param int $timestamp PHP date/time value
|
||||
* @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($timezone, $timestamp)
|
||||
public static function getTimeZoneAdjustment(?string $timezoneName, $timestamp): int
|
||||
{
|
||||
if ($timezone !== null) {
|
||||
if (!self::validateTimezone($timezone)) {
|
||||
throw new PhpSpreadsheetException('Invalid timezone ' . $timezone);
|
||||
}
|
||||
} else {
|
||||
$timezone = self::$timezone;
|
||||
$timezoneName = $timezoneName ?? self::$timezone;
|
||||
$dtobj = Date::dateTimeFromTimestamp("$timestamp");
|
||||
if (!self::validateTimeZone($timezoneName)) {
|
||||
throw new PhpSpreadsheetException("Invalid timezone $timezoneName");
|
||||
}
|
||||
$dtobj->setTimeZone(new DateTimeZone($timezoneName));
|
||||
|
||||
$objTimezone = new DateTimeZone($timezone);
|
||||
$transitions = $objTimezone->getTransitions($timestamp, $timestamp);
|
||||
|
||||
return (count($transitions) > 0) ? $transitions[0]['offset'] : 0;
|
||||
return $dtobj->getOffset();
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
|
||||
|
||||
class BestFit
|
||||
abstract class BestFit
|
||||
{
|
||||
/**
|
||||
* Indicator flag for a calculation error.
|
||||
@@ -96,24 +96,18 @@ class BestFit
|
||||
*
|
||||
* @param float $xValue X-Value
|
||||
*
|
||||
* @return bool Y-Value
|
||||
* @return float Y-Value
|
||||
*/
|
||||
public function getValueOfYForX($xValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
abstract public function getValueOfYForX($xValue);
|
||||
|
||||
/**
|
||||
* Return the X-Value for a specified value of Y.
|
||||
*
|
||||
* @param float $yValue Y-Value
|
||||
*
|
||||
* @return bool X-Value
|
||||
* @return float X-Value
|
||||
*/
|
||||
public function getValueOfXForY($yValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
abstract public function getValueOfXForY($yValue);
|
||||
|
||||
/**
|
||||
* Return the original set of X-Values.
|
||||
@@ -130,12 +124,9 @@ class BestFit
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return bool
|
||||
* @return string
|
||||
*/
|
||||
public function getEquation($dp = 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
abstract public function getEquation($dp = 0);
|
||||
|
||||
/**
|
||||
* Return the Slope of the line.
|
||||
@@ -341,20 +332,32 @@ class BestFit
|
||||
return $this->yBestFitValues;
|
||||
}
|
||||
|
||||
/** @var mixed */
|
||||
private static $scrutinizerZeroPointZero = 0.0;
|
||||
|
||||
/**
|
||||
* @param mixed $x
|
||||
* @param mixed $y
|
||||
*/
|
||||
private static function scrutinizerLooseCompare($x, $y): bool
|
||||
{
|
||||
return $x == $y;
|
||||
}
|
||||
|
||||
protected function calculateGoodnessOfFit($sumX, $sumY, $sumX2, $sumY2, $sumXY, $meanX, $meanY, $const): void
|
||||
{
|
||||
$SSres = $SScov = $SScor = $SStot = $SSsex = 0.0;
|
||||
$SSres = $SScov = $SStot = $SSsex = 0.0;
|
||||
foreach ($this->xValues as $xKey => $xValue) {
|
||||
$bestFitY = $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);
|
||||
|
||||
$SSres += ($this->yValues[$xKey] - $bestFitY) * ($this->yValues[$xKey] - $bestFitY);
|
||||
if ($const) {
|
||||
if ($const === true) {
|
||||
$SStot += ($this->yValues[$xKey] - $meanY) * ($this->yValues[$xKey] - $meanY);
|
||||
} else {
|
||||
$SStot += $this->yValues[$xKey] * $this->yValues[$xKey];
|
||||
}
|
||||
$SScov += ($this->xValues[$xKey] - $meanX) * ($this->yValues[$xKey] - $meanY);
|
||||
if ($const) {
|
||||
if ($const === true) {
|
||||
$SSsex += ($this->xValues[$xKey] - $meanX) * ($this->xValues[$xKey] - $meanX);
|
||||
} else {
|
||||
$SSsex += $this->xValues[$xKey] * $this->xValues[$xKey];
|
||||
@@ -362,14 +365,15 @@ class BestFit
|
||||
}
|
||||
|
||||
$this->SSResiduals = $SSres;
|
||||
$this->DFResiduals = $this->valueCount - 1 - $const;
|
||||
$this->DFResiduals = $this->valueCount - 1 - ($const === true ? 1 : 0);
|
||||
|
||||
if ($this->DFResiduals == 0.0) {
|
||||
$this->stdevOfResiduals = 0.0;
|
||||
} else {
|
||||
$this->stdevOfResiduals = sqrt($SSres / $this->DFResiduals);
|
||||
}
|
||||
if (($SStot == 0.0) || ($SSres == $SStot)) {
|
||||
// Scrutinizer thinks $SSres == $SStot is always true. It is wrong.
|
||||
if ($SStot == self::$scrutinizerZeroPointZero || self::scrutinizerLooseCompare($SSres, $SStot)) {
|
||||
$this->goodnessOfFit = 1;
|
||||
} else {
|
||||
$this->goodnessOfFit = 1 - ($SSres / $SStot);
|
||||
@@ -395,27 +399,39 @@ class BestFit
|
||||
}
|
||||
}
|
||||
|
||||
private function sumSquares(array $values)
|
||||
{
|
||||
return array_sum(
|
||||
array_map(
|
||||
function ($value) {
|
||||
return $value ** 2;
|
||||
},
|
||||
$values
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float[] $yValues
|
||||
* @param float[] $xValues
|
||||
* @param bool $const
|
||||
*/
|
||||
protected function leastSquareFit(array $yValues, array $xValues, $const): void
|
||||
protected function leastSquareFit(array $yValues, array $xValues, bool $const): void
|
||||
{
|
||||
// calculate sums
|
||||
$x_sum = array_sum($xValues);
|
||||
$y_sum = array_sum($yValues);
|
||||
$meanX = $x_sum / $this->valueCount;
|
||||
$meanY = $y_sum / $this->valueCount;
|
||||
$mBase = $mDivisor = $xx_sum = $xy_sum = $yy_sum = 0.0;
|
||||
$sumValuesX = array_sum($xValues);
|
||||
$sumValuesY = array_sum($yValues);
|
||||
$meanValueX = $sumValuesX / $this->valueCount;
|
||||
$meanValueY = $sumValuesY / $this->valueCount;
|
||||
$sumSquaresX = $this->sumSquares($xValues);
|
||||
$sumSquaresY = $this->sumSquares($yValues);
|
||||
$mBase = $mDivisor = 0.0;
|
||||
$xy_sum = 0.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];
|
||||
|
||||
if ($const) {
|
||||
$mBase += ($xValues[$i] - $meanX) * ($yValues[$i] - $meanY);
|
||||
$mDivisor += ($xValues[$i] - $meanX) * ($xValues[$i] - $meanX);
|
||||
if ($const === true) {
|
||||
$mBase += ($xValues[$i] - $meanValueX) * ($yValues[$i] - $meanValueY);
|
||||
$mDivisor += ($xValues[$i] - $meanValueX) * ($xValues[$i] - $meanValueX);
|
||||
} else {
|
||||
$mBase += $xValues[$i] * $yValues[$i];
|
||||
$mDivisor += $xValues[$i] * $xValues[$i];
|
||||
@@ -426,13 +442,9 @@ class BestFit
|
||||
$this->slope = $mBase / $mDivisor;
|
||||
|
||||
// calculate intersect
|
||||
if ($const) {
|
||||
$this->intersect = $meanY - ($this->slope * $meanX);
|
||||
} else {
|
||||
$this->intersect = 0;
|
||||
}
|
||||
$this->intersect = ($const === true) ? $meanValueY - ($this->slope * $meanValueX) : 0.0;
|
||||
|
||||
$this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, $meanX, $meanY, $const);
|
||||
$this->calculateGoodnessOfFit($sumValuesX, $sumValuesY, $sumSquaresX, $sumSquaresY, $xy_sum, $meanValueX, $meanValueY, $const);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -440,23 +452,22 @@ class BestFit
|
||||
*
|
||||
* @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)
|
||||
public function __construct($yValues, $xValues = [])
|
||||
{
|
||||
// Calculate number of points
|
||||
$nY = count($yValues);
|
||||
$nX = count($xValues);
|
||||
$yValueCount = count($yValues);
|
||||
$xValueCount = count($xValues);
|
||||
|
||||
// Define X Values if necessary
|
||||
if ($nX == 0) {
|
||||
$xValues = range(1, $nY);
|
||||
} elseif ($nY != $nX) {
|
||||
if ($xValueCount === 0) {
|
||||
$xValues = range(1, $yValueCount);
|
||||
} elseif ($yValueCount !== $xValueCount) {
|
||||
// Ensure both arrays of points are the same size
|
||||
$this->error = true;
|
||||
}
|
||||
|
||||
$this->valueCount = $nY;
|
||||
$this->valueCount = $yValueCount;
|
||||
$this->xValues = $xValues;
|
||||
$this->yValues = $yValues;
|
||||
}
|
||||
|
@@ -88,20 +88,17 @@ class ExponentialBestFit extends BestFit
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
private function exponentialRegression($yValues, $xValues, $const): void
|
||||
private function exponentialRegression(array $yValues, array $xValues, bool $const): void
|
||||
{
|
||||
foreach ($yValues as &$value) {
|
||||
if ($value < 0.0) {
|
||||
$value = 0 - log(abs($value));
|
||||
} elseif ($value > 0.0) {
|
||||
$value = log($value);
|
||||
}
|
||||
}
|
||||
unset($value);
|
||||
$adjustedYValues = array_map(
|
||||
function ($value) {
|
||||
return ($value < 0.0) ? 0 - log(abs($value)) : log($value);
|
||||
},
|
||||
$yValues
|
||||
);
|
||||
|
||||
$this->leastSquareFit($yValues, $xValues, $const);
|
||||
$this->leastSquareFit($adjustedYValues, $xValues, $const);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +113,7 @@ class ExponentialBestFit extends BestFit
|
||||
parent::__construct($yValues, $xValues);
|
||||
|
||||
if (!$this->error) {
|
||||
$this->exponentialRegression($yValues, $xValues, $const);
|
||||
$this->exponentialRegression($yValues, $xValues, (bool) $const);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -56,9 +56,8 @@ class LinearBestFit extends BestFit
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
private function linearRegression($yValues, $xValues, $const): void
|
||||
private function linearRegression(array $yValues, array $xValues, bool $const): void
|
||||
{
|
||||
$this->leastSquareFit($yValues, $xValues, $const);
|
||||
}
|
||||
@@ -75,7 +74,7 @@ class LinearBestFit extends BestFit
|
||||
parent::__construct($yValues, $xValues);
|
||||
|
||||
if (!$this->error) {
|
||||
$this->linearRegression($yValues, $xValues, $const);
|
||||
$this->linearRegression($yValues, $xValues, (bool) $const);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -48,7 +48,7 @@ class LogarithmicBestFit extends BestFit
|
||||
$slope = $this->getSlope($dp);
|
||||
$intersect = $this->getIntersect($dp);
|
||||
|
||||
return 'Y = ' . $intersect . ' + ' . $slope . ' * log(X)';
|
||||
return 'Y = ' . $slope . ' * log(' . $intersect . ' * X)';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,20 +56,17 @@ class LogarithmicBestFit extends BestFit
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
private function logarithmicRegression($yValues, $xValues, $const): void
|
||||
private function logarithmicRegression(array $yValues, array $xValues, bool $const): void
|
||||
{
|
||||
foreach ($xValues as &$value) {
|
||||
if ($value < 0.0) {
|
||||
$value = 0 - log(abs($value));
|
||||
} elseif ($value > 0.0) {
|
||||
$value = log($value);
|
||||
}
|
||||
}
|
||||
unset($value);
|
||||
$adjustedYValues = array_map(
|
||||
function ($value) {
|
||||
return ($value < 0.0) ? 0 - log(abs($value)) : log($value);
|
||||
},
|
||||
$yValues
|
||||
);
|
||||
|
||||
$this->leastSquareFit($yValues, $xValues, $const);
|
||||
$this->leastSquareFit($adjustedYValues, $xValues, $const);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +81,7 @@ class LogarithmicBestFit extends BestFit
|
||||
parent::__construct($yValues, $xValues);
|
||||
|
||||
if (!$this->error) {
|
||||
$this->logarithmicRegression($yValues, $xValues, $const);
|
||||
$this->logarithmicRegression($yValues, $xValues, (bool) $const);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,10 @@ namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\JAMA\Matrix;
|
||||
|
||||
// Phpstan and Scrutinizer seem to have legitimate complaints.
|
||||
// $this->slope is specified where an array is expected in several places.
|
||||
// But it seems that it should always be float.
|
||||
// This code is probably not exercised at all in unit tests.
|
||||
class PolynomialBestFit extends BestFit
|
||||
{
|
||||
/**
|
||||
@@ -42,6 +46,8 @@ class PolynomialBestFit extends BestFit
|
||||
{
|
||||
$retVal = $this->getIntersect();
|
||||
$slope = $this->getSlope();
|
||||
// Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
|
||||
// @phpstan-ignore-next-line
|
||||
foreach ($slope as $key => $value) {
|
||||
if ($value != 0.0) {
|
||||
$retVal += $value * $xValue ** ($key + 1);
|
||||
@@ -76,6 +82,8 @@ class PolynomialBestFit extends BestFit
|
||||
$intersect = $this->getIntersect($dp);
|
||||
|
||||
$equation = 'Y = ' . $intersect;
|
||||
// Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
|
||||
// @phpstan-ignore-next-line
|
||||
foreach ($slope as $key => $value) {
|
||||
if ($value != 0.0) {
|
||||
$equation .= ' + ' . $value . ' * X';
|
||||
@@ -93,16 +101,18 @@ class PolynomialBestFit extends BestFit
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return string
|
||||
* @return float
|
||||
*/
|
||||
public function getSlope($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
$coefficients = [];
|
||||
// Scrutinizer is correct - $this->slope is float, not array.
|
||||
foreach ($this->slope as $coefficient) {
|
||||
$coefficients[] = round($coefficient, $dp);
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
return $coefficients;
|
||||
}
|
||||
|
||||
@@ -111,6 +121,8 @@ class PolynomialBestFit extends BestFit
|
||||
|
||||
public function getCoefficients($dp = 0)
|
||||
{
|
||||
// Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
|
||||
// @phpstan-ignore-next-line
|
||||
return array_merge([$this->getIntersect($dp)], $this->getSlope($dp));
|
||||
}
|
||||
|
||||
@@ -178,9 +190,8 @@ class PolynomialBestFit extends BestFit
|
||||
* @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
|
||||
* @param bool $const
|
||||
*/
|
||||
public function __construct($order, $yValues, $xValues = [], $const = true)
|
||||
public function __construct($order, $yValues, $xValues = [])
|
||||
{
|
||||
parent::__construct($yValues, $xValues);
|
||||
|
||||
|
@@ -72,28 +72,23 @@ class PowerBestFit extends BestFit
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
private function powerRegression($yValues, $xValues, $const): void
|
||||
private function powerRegression(array $yValues, array $xValues, bool $const): void
|
||||
{
|
||||
foreach ($xValues as &$value) {
|
||||
if ($value < 0.0) {
|
||||
$value = 0 - log(abs($value));
|
||||
} elseif ($value > 0.0) {
|
||||
$value = log($value);
|
||||
}
|
||||
}
|
||||
unset($value);
|
||||
foreach ($yValues as &$value) {
|
||||
if ($value < 0.0) {
|
||||
$value = 0 - log(abs($value));
|
||||
} elseif ($value > 0.0) {
|
||||
$value = log($value);
|
||||
}
|
||||
}
|
||||
unset($value);
|
||||
$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($yValues, $xValues, $const);
|
||||
$this->leastSquareFit($adjustedYValues, $adjustedXValues, $const);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,7 +103,7 @@ class PowerBestFit extends BestFit
|
||||
parent::__construct($yValues, $xValues);
|
||||
|
||||
if (!$this->error) {
|
||||
$this->powerRegression($yValues, $xValues, $const);
|
||||
$this->powerRegression($yValues, $xValues, (bool) $const);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -44,7 +44,7 @@ class Trend
|
||||
/**
|
||||
* Cached results for each method when trying to identify which provides the best fit.
|
||||
*
|
||||
* @var bestFit[]
|
||||
* @var BestFit[]
|
||||
*/
|
||||
private static $trendCache = [];
|
||||
|
||||
@@ -55,10 +55,9 @@ class Trend
|
||||
$nX = count($xValues);
|
||||
|
||||
// Define X Values if necessary
|
||||
if ($nX == 0) {
|
||||
if ($nX === 0) {
|
||||
$xValues = range(1, $nY);
|
||||
$nX = $nY;
|
||||
} elseif ($nY != $nX) {
|
||||
} 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);
|
||||
}
|
||||
@@ -73,6 +72,7 @@ class Trend
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -83,8 +83,8 @@ class Trend
|
||||
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, $const);
|
||||
$order = (int) substr($trendType, -1);
|
||||
self::$trendCache[$key] = new PolynomialBestFit($order, $yValues, $xValues);
|
||||
}
|
||||
|
||||
return self::$trendCache[$key];
|
||||
@@ -92,6 +92,8 @@ class Trend
|
||||
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);
|
||||
@@ -99,8 +101,8 @@ class Trend
|
||||
}
|
||||
if ($trendType != self::TREND_BEST_FIT_NO_POLY) {
|
||||
foreach (self::$trendTypePolynomialOrders as $trendMethod) {
|
||||
$order = substr($trendMethod, -1);
|
||||
$bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues, $const);
|
||||
$order = (int) substr($trendMethod, -1);
|
||||
$bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues);
|
||||
if ($bestFit[$trendMethod]->getError()) {
|
||||
unset($bestFit[$trendMethod]);
|
||||
} else {
|
||||
|
@@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
class XMLWriter extends \XMLWriter
|
||||
{
|
||||
/** @var bool */
|
||||
public static $debugEnabled = false;
|
||||
|
||||
/** Temporary storage method */
|
||||
@@ -20,23 +21,23 @@ class XMLWriter extends \XMLWriter
|
||||
/**
|
||||
* Create a new XMLWriter instance.
|
||||
*
|
||||
* @param int $pTemporaryStorage Temporary storage location
|
||||
* @param string $pTemporaryStorageFolder Temporary storage folder
|
||||
* @param int $temporaryStorage Temporary storage location
|
||||
* @param string $temporaryStorageFolder Temporary storage folder
|
||||
*/
|
||||
public function __construct($pTemporaryStorage = self::STORAGE_MEMORY, $pTemporaryStorageFolder = null)
|
||||
public function __construct($temporaryStorage = self::STORAGE_MEMORY, $temporaryStorageFolder = null)
|
||||
{
|
||||
// Open temporary storage
|
||||
if ($pTemporaryStorage == self::STORAGE_MEMORY) {
|
||||
if ($temporaryStorage == self::STORAGE_MEMORY) {
|
||||
$this->openMemory();
|
||||
} else {
|
||||
// Create temporary filename
|
||||
if ($pTemporaryStorageFolder === null) {
|
||||
$pTemporaryStorageFolder = File::sysGetTempDir();
|
||||
if ($temporaryStorageFolder === null) {
|
||||
$temporaryStorageFolder = File::sysGetTempDir();
|
||||
}
|
||||
$this->tempFileName = @tempnam($pTemporaryStorageFolder, 'xml');
|
||||
$this->tempFileName = (string) @tempnam($temporaryStorageFolder, 'xml');
|
||||
|
||||
// Open storage
|
||||
if ($this->openUri($this->tempFileName) === false) {
|
||||
if (empty($this->tempFileName) || $this->openUri($this->tempFileName) === false) {
|
||||
// Fallback to memory...
|
||||
$this->openMemory();
|
||||
}
|
||||
@@ -54,7 +55,9 @@ class XMLWriter extends \XMLWriter
|
||||
public function __destruct()
|
||||
{
|
||||
// Unlink temporary files
|
||||
// There is nothing reasonable to do if unlink fails.
|
||||
if ($this->tempFileName != '') {
|
||||
/** @scrutinizer ignore-unhandled */
|
||||
@unlink($this->tempFileName);
|
||||
}
|
||||
}
|
||||
@@ -71,22 +74,22 @@ class XMLWriter extends \XMLWriter
|
||||
}
|
||||
$this->flush();
|
||||
|
||||
return file_get_contents($this->tempFileName);
|
||||
return file_get_contents($this->tempFileName) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper method for writeRaw.
|
||||
*
|
||||
* @param string|string[] $text
|
||||
* @param null|string|string[] $rawTextData
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function writeRawData($text)
|
||||
public function writeRawData($rawTextData)
|
||||
{
|
||||
if (is_array($text)) {
|
||||
$text = implode("\n", $text);
|
||||
if (is_array($rawTextData)) {
|
||||
$rawTextData = implode("\n", $rawTextData);
|
||||
}
|
||||
|
||||
return $this->writeRaw(htmlspecialchars($text));
|
||||
return $this->writeRaw(htmlspecialchars($rawTextData ?? ''));
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Helper\Dimension;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class Xls
|
||||
@@ -12,17 +13,17 @@ class Xls
|
||||
* x is the width in intrinsic Excel units (measuring width in number of normal characters)
|
||||
* This holds for Arial 10.
|
||||
*
|
||||
* @param Worksheet $sheet The sheet
|
||||
* @param Worksheet $worksheet The sheet
|
||||
* @param string $col The column
|
||||
*
|
||||
* @return int The width in pixels
|
||||
*/
|
||||
public static function sizeCol($sheet, $col = 'A')
|
||||
public static function sizeCol(Worksheet $worksheet, $col = 'A')
|
||||
{
|
||||
// default font of the workbook
|
||||
$font = $sheet->getParent()->getDefaultStyle()->getFont();
|
||||
$font = $worksheet->getParent()->getDefaultStyle()->getFont();
|
||||
|
||||
$columnDimensions = $sheet->getColumnDimensions();
|
||||
$columnDimensions = $worksheet->getColumnDimensions();
|
||||
|
||||
// first find the true column width in pixels (uncollapsed and unhidden)
|
||||
if (isset($columnDimensions[$col]) && $columnDimensions[$col]->getWidth() != -1) {
|
||||
@@ -30,9 +31,9 @@ class Xls
|
||||
$columnDimension = $columnDimensions[$col];
|
||||
$width = $columnDimension->getWidth();
|
||||
$pixelWidth = Drawing::cellDimensionToPixels($width, $font);
|
||||
} elseif ($sheet->getDefaultColumnDimension()->getWidth() != -1) {
|
||||
} elseif ($worksheet->getDefaultColumnDimension()->getWidth() != -1) {
|
||||
// then we have default column dimension with explicit width
|
||||
$defaultColumnDimension = $sheet->getDefaultColumnDimension();
|
||||
$defaultColumnDimension = $worksheet->getDefaultColumnDimension();
|
||||
$width = $defaultColumnDimension->getWidth();
|
||||
$pixelWidth = Drawing::cellDimensionToPixels($width, $font);
|
||||
} else {
|
||||
@@ -55,17 +56,17 @@ class Xls
|
||||
* 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 $sheet The sheet
|
||||
* @param Worksheet $worksheet The sheet
|
||||
* @param int $row The row index (1-based)
|
||||
*
|
||||
* @return int The width in pixels
|
||||
*/
|
||||
public static function sizeRow($sheet, $row = 1)
|
||||
public static function sizeRow(Worksheet $worksheet, $row = 1)
|
||||
{
|
||||
// default font of the workbook
|
||||
$font = $sheet->getParent()->getDefaultStyle()->getFont();
|
||||
$font = $worksheet->getParent()->getDefaultStyle()->getFont();
|
||||
|
||||
$rowDimensions = $sheet->getRowDimensions();
|
||||
$rowDimensions = $worksheet->getRowDimensions();
|
||||
|
||||
// first find the true row height in pixels (uncollapsed and unhidden)
|
||||
if (isset($rowDimensions[$row]) && $rowDimensions[$row]->getRowHeight() != -1) {
|
||||
@@ -73,15 +74,14 @@ class Xls
|
||||
$rowDimension = $rowDimensions[$row];
|
||||
$rowHeight = $rowDimension->getRowHeight();
|
||||
$pixelRowHeight = (int) ceil(4 * $rowHeight / 3); // here we assume Arial 10
|
||||
} elseif ($sheet->getDefaultRowDimension()->getRowHeight() != -1) {
|
||||
} elseif ($worksheet->getDefaultRowDimension()->getRowHeight() != -1) {
|
||||
// then we have a default row dimension with explicit height
|
||||
$defaultRowDimension = $sheet->getDefaultRowDimension();
|
||||
$rowHeight = $defaultRowDimension->getRowHeight();
|
||||
$pixelRowHeight = Drawing::pointsToPixels($rowHeight);
|
||||
$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($pointRowHeight);
|
||||
$pixelRowHeight = Font::fontSizeToPixels((int) $pointRowHeight);
|
||||
}
|
||||
|
||||
// now find the effective row height in pixels
|
||||
@@ -91,7 +91,7 @@ class Xls
|
||||
$effectivePixelRowHeight = $pixelRowHeight;
|
||||
}
|
||||
|
||||
return $effectivePixelRowHeight;
|
||||
return (int) $effectivePixelRowHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +105,7 @@ class Xls
|
||||
*
|
||||
* @return int Horizontal measured in pixels
|
||||
*/
|
||||
public static function getDistanceX(Worksheet $sheet, $startColumn = 'A', $startOffsetX = 0, $endColumn = 'A', $endOffsetX = 0)
|
||||
public static function getDistanceX(Worksheet $worksheet, $startColumn = 'A', $startOffsetX = 0, $endColumn = 'A', $endOffsetX = 0)
|
||||
{
|
||||
$distanceX = 0;
|
||||
|
||||
@@ -113,14 +113,14 @@ class Xls
|
||||
$startColumnIndex = Coordinate::columnIndexFromString($startColumn);
|
||||
$endColumnIndex = Coordinate::columnIndexFromString($endColumn);
|
||||
for ($i = $startColumnIndex; $i <= $endColumnIndex; ++$i) {
|
||||
$distanceX += self::sizeCol($sheet, Coordinate::stringFromColumnIndex($i));
|
||||
$distanceX += self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($i));
|
||||
}
|
||||
|
||||
// correct for offsetX in startcell
|
||||
$distanceX -= (int) floor(self::sizeCol($sheet, $startColumn) * $startOffsetX / 1024);
|
||||
$distanceX -= (int) floor(self::sizeCol($worksheet, $startColumn) * $startOffsetX / 1024);
|
||||
|
||||
// correct for offsetX in endcell
|
||||
$distanceX -= (int) floor(self::sizeCol($sheet, $endColumn) * (1 - $endOffsetX / 1024));
|
||||
$distanceX -= (int) floor(self::sizeCol($worksheet, $endColumn) * (1 - $endOffsetX / 1024));
|
||||
|
||||
return $distanceX;
|
||||
}
|
||||
@@ -136,20 +136,20 @@ class Xls
|
||||
*
|
||||
* @return int Vertical distance measured in pixels
|
||||
*/
|
||||
public static function getDistanceY(Worksheet $sheet, $startRow = 1, $startOffsetY = 0, $endRow = 1, $endOffsetY = 0)
|
||||
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($sheet, $row);
|
||||
$distanceY += self::sizeRow($worksheet, $row);
|
||||
}
|
||||
|
||||
// correct for offsetX in startcell
|
||||
$distanceY -= (int) floor(self::sizeRow($sheet, $startRow) * $startOffsetY / 256);
|
||||
$distanceY -= (int) floor(self::sizeRow($worksheet, $startRow) * $startOffsetY / 256);
|
||||
|
||||
// correct for offsetX in endcell
|
||||
$distanceY -= (int) floor(self::sizeRow($sheet, $endRow) * (1 - $endOffsetY / 256));
|
||||
$distanceY -= (int) floor(self::sizeRow($worksheet, $endRow) * (1 - $endOffsetY / 256));
|
||||
|
||||
return $distanceY;
|
||||
}
|
||||
@@ -198,19 +198,17 @@ class Xls
|
||||
* W is the width of the cell
|
||||
* H is the height of the cell
|
||||
*
|
||||
* @param Worksheet $sheet
|
||||
* @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 array
|
||||
* @return null|array
|
||||
*/
|
||||
public static function oneAnchor2twoAnchor($sheet, $coordinates, $offsetX, $offsetY, $width, $height)
|
||||
public static function oneAnchor2twoAnchor(Worksheet $worksheet, $coordinates, $offsetX, $offsetY, $width, $height)
|
||||
{
|
||||
[$column, $row] = Coordinate::coordinateFromString($coordinates);
|
||||
$col_start = Coordinate::columnIndexFromString($column);
|
||||
[$col_start, $row] = Coordinate::indexesFromString($coordinates);
|
||||
$row_start = $row - 1;
|
||||
|
||||
$x1 = $offsetX;
|
||||
@@ -221,10 +219,10 @@ class Xls
|
||||
$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($sheet, Coordinate::stringFromColumnIndex($col_start))) {
|
||||
if ($x1 >= self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_start))) {
|
||||
$x1 = 0;
|
||||
}
|
||||
if ($y1 >= self::sizeRow($sheet, $row_start + 1)) {
|
||||
if ($y1 >= self::sizeRow($worksheet, $row_start + 1)) {
|
||||
$y1 = 0;
|
||||
}
|
||||
|
||||
@@ -232,37 +230,37 @@ class Xls
|
||||
$height = $height + $y1 - 1;
|
||||
|
||||
// Subtract the underlying cell widths to find the end cell of the image
|
||||
while ($width >= self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_end))) {
|
||||
$width -= self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_end));
|
||||
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($sheet, $row_end + 1)) {
|
||||
$height -= self::sizeRow($sheet, $row_end + 1);
|
||||
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($sheet, Coordinate::stringFromColumnIndex($col_start)) == 0) {
|
||||
return;
|
||||
if (self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_start)) == 0) {
|
||||
return null;
|
||||
}
|
||||
if (self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_end)) == 0) {
|
||||
return;
|
||||
if (self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end)) == 0) {
|
||||
return null;
|
||||
}
|
||||
if (self::sizeRow($sheet, $row_start + 1) == 0) {
|
||||
return;
|
||||
if (self::sizeRow($worksheet, $row_start + 1) == 0) {
|
||||
return null;
|
||||
}
|
||||
if (self::sizeRow($sheet, $row_end + 1) == 0) {
|
||||
return;
|
||||
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($sheet, Coordinate::stringFromColumnIndex($col_start)) * 1024;
|
||||
$y1 = $y1 / self::sizeRow($sheet, $row_start + 1) * 256;
|
||||
$x2 = ($width + 1) / self::sizeCol($sheet, Coordinate::stringFromColumnIndex($col_end)) * 1024; // Distance to right side of object
|
||||
$y2 = ($height + 1) / self::sizeRow($sheet, $row_end + 1) * 256; // Distance to bottom of object
|
||||
$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);
|
||||
|
Reference in New Issue
Block a user