diff --git a/infra/LocaleGenerator.php b/infra/LocaleGenerator.php index bb97754d..992bde17 100644 --- a/infra/LocaleGenerator.php +++ b/infra/LocaleGenerator.php @@ -146,7 +146,7 @@ class LocaleGenerator $translationValue = $translationCell->getValue(); if ($this->isFunctionCategoryEntry($translationCell)) { $this->writeFileSectionHeader($functionFile, "{$translationValue} ({$functionName})"); - } elseif (!array_key_exists($functionName, $this->phpSpreadsheetFunctions)) { + } elseif (!array_key_exists($functionName, $this->phpSpreadsheetFunctions) && substr($functionName, 0, 1) !== '*') { $this->log("Function {$functionName} is not defined in PhpSpreadsheet"); } elseif (!empty($translationValue)) { $functionTranslation = "{$functionName} = {$translationValue}" . self::EOL; diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index b5a26f62..94eadd85 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3110,7 +3110,7 @@ class Calculation [$localeFunction] = explode('##', $localeFunction); // Strip out comments if (strpos($localeFunction, '=') !== false) { [$fName, $lfName] = array_map('trim', explode('=', $localeFunction)); - if ((isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) { + if ((substr($fName, 0, 1) === '*' || isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) { self::$localeFunctions[$fName] = $lfName; } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php index c8cdf2dd..0d2db8b2 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Cell\AddressHelper; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; class Address @@ -72,6 +73,9 @@ class Address $sheetName = self::sheetName($sheetName); + if (is_int($referenceStyle)) { + $referenceStyle = (bool) $referenceStyle; + } if ((!is_bool($referenceStyle)) || $referenceStyle === self::REFERENCE_STYLE_A1) { return self::formatAsA1($row, $column, $relativity, $sheetName); } @@ -113,7 +117,8 @@ class Address if (($relativity == self::ADDRESS_ROW_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { $row = "[{$row}]"; } + [$rowChar, $colChar] = AddressHelper::getRowAndColumnChars(); - return "{$sheetName}R{$row}C{$column}"; + return "{$sheetName}$rowChar{$row}$colChar{$column}"; } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php index 7408a66e..76a194b3 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php @@ -13,12 +13,12 @@ class Helpers public const CELLADDRESS_USE_R1C1 = false; - private static function convertR1C1(string &$cellAddress1, ?string &$cellAddress2, bool $a1): string + private static function convertR1C1(string &$cellAddress1, ?string &$cellAddress2, bool $a1, ?int $baseRow = null, ?int $baseCol = null): string { if ($a1 === self::CELLADDRESS_USE_R1C1) { - $cellAddress1 = AddressHelper::convertToA1($cellAddress1); + $cellAddress1 = AddressHelper::convertToA1($cellAddress1, $baseRow ?? 1, $baseCol ?? 1); if ($cellAddress2) { - $cellAddress2 = AddressHelper::convertToA1($cellAddress2); + $cellAddress2 = AddressHelper::convertToA1($cellAddress2, $baseRow ?? 1, $baseCol ?? 1); } } @@ -35,7 +35,7 @@ class Helpers } } - public static function extractCellAddresses(string $cellAddress, bool $a1, Worksheet $sheet, string $sheetName = ''): array + public static function extractCellAddresses(string $cellAddress, bool $a1, Worksheet $sheet, string $sheetName = '', ?int $baseRow = null, ?int $baseCol = null): array { $cellAddress1 = $cellAddress; $cellAddress2 = null; @@ -52,7 +52,7 @@ class Helpers if (strpos($cellAddress, ':') !== false) { [$cellAddress1, $cellAddress2] = explode(':', $cellAddress); } - $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1); + $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1, $baseRow, $baseCol); return [$cellAddress1, $cellAddress2, $cellAddress]; } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php index 417a1f79..91a14491 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Indirect @@ -63,6 +64,8 @@ class Indirect */ public static function INDIRECT($cellAddress, $a1fmt, Cell $cell) { + [$baseCol, $baseRow] = Coordinate::indexesFromString($cell->getCoordinate()); + try { $a1 = self::a1Format($a1fmt); $cellAddress = self::validateAddress($cellAddress); @@ -78,7 +81,11 @@ class Indirect $cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress)); } - [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName); + try { + [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName, $baseRow, $baseCol); + } catch (Exception $e) { + return ExcelError::REF(); + } if ( (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress1, $matches)) || diff --git a/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx b/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx index d013aee0..7b9fb0dd 100644 Binary files a/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx and b/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx differ diff --git a/src/PhpSpreadsheet/Calculation/locale/da/functions b/src/PhpSpreadsheet/Calculation/locale/da/functions index 6260760b..03b68c9b 100644 --- a/src/PhpSpreadsheet/Calculation/locale/da/functions +++ b/src/PhpSpreadsheet/Calculation/locale/da/functions @@ -245,6 +245,7 @@ ROWS = RÆKKER RTD = RTD TRANSPOSE = TRANSPONER VLOOKUP = LOPSLAG +*RC = RK ## ## Matematiske og trigonometriske funktioner (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/de/functions b/src/PhpSpreadsheet/Calculation/locale/de/functions index 331232f7..d49fc5f1 100644 --- a/src/PhpSpreadsheet/Calculation/locale/de/functions +++ b/src/PhpSpreadsheet/Calculation/locale/de/functions @@ -243,6 +243,7 @@ ROWS = ZEILEN RTD = RTD TRANSPOSE = MTRANS VLOOKUP = SVERWEIS +*RC = ZS ## ## Mathematische und trigonometrische Funktionen (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/es/functions b/src/PhpSpreadsheet/Calculation/locale/es/functions index 1f9f2891..88012aa1 100644 --- a/src/PhpSpreadsheet/Calculation/locale/es/functions +++ b/src/PhpSpreadsheet/Calculation/locale/es/functions @@ -245,6 +245,7 @@ ROWS = FILAS RTD = RDTR TRANSPOSE = TRANSPONER VLOOKUP = BUSCARV +*RC = FC ## ## Funciones matemáticas y trigonométricas (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/fi/functions b/src/PhpSpreadsheet/Calculation/locale/fi/functions index 33068d93..18f7c8c8 100644 --- a/src/PhpSpreadsheet/Calculation/locale/fi/functions +++ b/src/PhpSpreadsheet/Calculation/locale/fi/functions @@ -245,6 +245,7 @@ ROWS = RIVIT RTD = RTD TRANSPOSE = TRANSPONOI VLOOKUP = PHAKU +*RC = RS ## ## Matemaattiset ja trigonometriset funktiot (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/fr/functions b/src/PhpSpreadsheet/Calculation/locale/fr/functions index 78b603e9..621cb0db 100644 --- a/src/PhpSpreadsheet/Calculation/locale/fr/functions +++ b/src/PhpSpreadsheet/Calculation/locale/fr/functions @@ -240,6 +240,7 @@ ROWS = LIGNES RTD = RTD TRANSPOSE = TRANSPOSE VLOOKUP = RECHERCHEV +*RC = LC ## ## Fonctions mathématiques et trigonométriques (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/hu/functions b/src/PhpSpreadsheet/Calculation/locale/hu/functions index 46b30127..4a375ea2 100644 --- a/src/PhpSpreadsheet/Calculation/locale/hu/functions +++ b/src/PhpSpreadsheet/Calculation/locale/hu/functions @@ -245,6 +245,7 @@ ROWS = SOROK RTD = VIA TRANSPOSE = TRANSZPONÁLÁS VLOOKUP = FKERES +*RC = SO ## ## Matematikai és trigonometrikus függvények (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/nb/functions b/src/PhpSpreadsheet/Calculation/locale/nb/functions index b0a0f949..d352e1f4 100644 --- a/src/PhpSpreadsheet/Calculation/locale/nb/functions +++ b/src/PhpSpreadsheet/Calculation/locale/nb/functions @@ -245,6 +245,7 @@ ROWS = RADER RTD = RTD TRANSPOSE = TRANSPONER VLOOKUP = FINN.RAD +*RC = RK ## ## Matematikk- og trigonometrifunksjoner (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/nl/functions b/src/PhpSpreadsheet/Calculation/locale/nl/functions index 0e4f1597..ce0b30cc 100644 --- a/src/PhpSpreadsheet/Calculation/locale/nl/functions +++ b/src/PhpSpreadsheet/Calculation/locale/nl/functions @@ -244,6 +244,7 @@ ROWS = RIJEN RTD = RTG TRANSPOSE = TRANSPONEREN VLOOKUP = VERT.ZOEKEN +*RC = RK ## ## Wiskundige en trigonometrische functies (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/pt/br/functions b/src/PhpSpreadsheet/Calculation/locale/pt/br/functions index feba30d9..5781b0c7 100644 --- a/src/PhpSpreadsheet/Calculation/locale/pt/br/functions +++ b/src/PhpSpreadsheet/Calculation/locale/pt/br/functions @@ -242,6 +242,7 @@ ROWS = LINS RTD = RTD TRANSPOSE = TRANSPOR VLOOKUP = PROCV +*RC = LC ## ## Funções matemáticas e trigonométricas (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/pt/functions b/src/PhpSpreadsheet/Calculation/locale/pt/functions index 8a94d826..70a3bb0c 100644 --- a/src/PhpSpreadsheet/Calculation/locale/pt/functions +++ b/src/PhpSpreadsheet/Calculation/locale/pt/functions @@ -245,6 +245,7 @@ ROWS = LINS RTD = RTD TRANSPOSE = TRANSPOR VLOOKUP = PROCV +*RC = LC ## ## Funções matemáticas e trigonométricas (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/sv/functions b/src/PhpSpreadsheet/Calculation/locale/sv/functions index 2531b4c1..491ecfb9 100644 --- a/src/PhpSpreadsheet/Calculation/locale/sv/functions +++ b/src/PhpSpreadsheet/Calculation/locale/sv/functions @@ -243,6 +243,7 @@ ROWS = RADER RTD = RTD TRANSPOSE = TRANSPONERA VLOOKUP = LETARAD +*RC = RK ## ## Matematiska och trigonometriska funktioner (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Cell/AddressHelper.php b/src/PhpSpreadsheet/Cell/AddressHelper.php index 535bdee0..a23a78b6 100644 --- a/src/PhpSpreadsheet/Cell/AddressHelper.php +++ b/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -2,23 +2,44 @@ namespace PhpOffice\PhpSpreadsheet\Cell; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Exception; class AddressHelper { public const R1C1_COORDINATE_REGEX = '/(R((?:\[-?\d*\])|(?:\d*))?)(C((?:\[-?\d*\])|(?:\d*))?)/i'; + /** @return string[] */ + public static function getRowAndColumnChars() + { + $rowChar = 'R'; + $colChar = 'C'; + if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_EXCEL) { + $rowColChars = Calculation::localeFunc('*RC'); + if (mb_strlen($rowColChars) === 2) { + $rowChar = mb_substr($rowColChars, 0, 1); + $colChar = mb_substr($rowColChars, 1, 1); + } + } + + return [$rowChar, $colChar]; + } + /** * Converts an R1C1 format cell address to an A1 format cell address. */ public static function convertToA1( string $address, int $currentRowNumber = 1, - int $currentColumnNumber = 1 + int $currentColumnNumber = 1, + bool $useLocale = true ): string { - $validityCheck = preg_match('/^(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))$/i', $address, $cellReference); + [$rowChar, $colChar] = $useLocale ? self::getRowAndColumnChars() : ['R', 'C']; + $regex = '/^(' . $rowChar . '(\[?[-+]?\d*\]?))(' . $colChar . '(\[?[-+]?\d*\]?))$/i'; + $validityCheck = preg_match($regex, $address, $cellReference); - if ($validityCheck === 0) { + if (empty($validityCheck)) { throw new Exception('Invalid R1C1-format Cell Reference'); } @@ -92,7 +113,7 @@ class AddressHelper // Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent, // then modify the formula to use that new reference foreach ($cellReferences as $cellReference) { - $A1CellReference = self::convertToA1($cellReference[0][0], $currentRowNumber, $currentColumnNumber); + $A1CellReference = self::convertToA1($cellReference[0][0], $currentRowNumber, $currentColumnNumber, false); $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0])); } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressInternationalTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressInternationalTest.php new file mode 100644 index 00000000..87e0bb4e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressInternationalTest.php @@ -0,0 +1,79 @@ +locale = Settings::getLocale(); + } + + protected function tearDown(): void + { + Settings::setLocale($this->locale); + // CompatibilityMode is restored in parent + parent::tearDown(); + } + + /** + * @dataProvider providerInternational + */ + public function testR1C1International(string $locale, string $r, string $c): void + { + if ($locale !== '') { + Settings::setLocale($locale); + } + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=LEFT(ADDRESS(1,1,1,0),1)'); + $sheet->getCell('A2')->setValue('=MID(ADDRESS(1,1,1,0),3,1)'); + self::assertSame($r, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame($c, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerInternational(): array + { + return [ + 'Default' => ['', 'R', 'C'], + 'English' => ['en', 'R', 'C'], + 'French' => ['fr', 'L', 'C'], + 'German' => ['de', 'Z', 'S'], + 'Made-up' => ['xx', 'R', 'C'], + 'Spanish' => ['es', 'F', 'C'], + 'Bulgarian' => ['bg', 'R', 'C'], + 'Czech' => ['cs', 'R', 'C'], // maybe should be R/S + 'Polish' => ['pl', 'R', 'C'], // maybe should be W/K + 'Turkish' => ['tr', 'R', 'C'], + ]; + } + + /** + * @dataProvider providerCompatibility + */ + public function testCompatibilityInternational(string $compatibilityMode, string $r, string $c): void + { + Functions::setCompatibilityMode($compatibilityMode); + Settings::setLocale('de'); + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=LEFT(ADDRESS(1,1,1,0),1)'); + $sheet->getCell('A2')->setValue('=MID(ADDRESS(1,1,1,0),3,1)'); + self::assertSame($r, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame($c, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerCompatibility(): array + { + return [ + [Functions::COMPATIBILITY_EXCEL, 'Z', 'S'], + [Functions::COMPATIBILITY_OPENOFFICE, 'R', 'C'], + [Functions::COMPATIBILITY_GNUMERIC, 'R', 'C'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php index 2b92030e..2a6ae883 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php @@ -3,17 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PHPUnit\Framework\TestCase; class AddressTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerADDRESS * diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php new file mode 100644 index 00000000..7d6d0a65 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php @@ -0,0 +1,132 @@ +locale = Settings::getLocale(); + } + + protected function tearDown(): void + { + Settings::setLocale($this->locale); + // CompatibilityMode is restored in parent + parent::tearDown(); + } + + /** + * @dataProvider providerInternational + */ + public function testR1C1International(string $locale): void + { + Settings::setLocale($locale); + $sameAsEnglish = ['en', 'xx', 'ru', 'tr', 'cs', 'pl']; + $sheet = $this->getSheet(); + $sheet->getCell('C1')->setValue('text'); + $sheet->getCell('A2')->setValue('en'); + $sheet->getCell('B2')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A3')->setValue('fr'); + $sheet->getCell('B3')->setValue('=INDIRECT("L1C3", false)'); + $sheet->getCell('A4')->setValue('de'); + $sheet->getCell('B4')->setValue('=INDIRECT("Z1S3", false)'); + $sheet->getCell('A5')->setValue('es'); + $sheet->getCell('B5')->setValue('=INDIRECT("F1C3", false)'); + $sheet->getCell('A6')->setValue('xx'); + $sheet->getCell('B6')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A7')->setValue('ru'); + $sheet->getCell('B7')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A8')->setValue('cs'); + $sheet->getCell('B8')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A9')->setValue('tr'); + $sheet->getCell('B9')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A10')->setValue('pl'); + $sheet->getCell('B10')->setValue('=INDIRECT("R1C3", false)'); + $maxRow = $sheet->getHighestRow(); + for ($row = 2; $row <= $maxRow; ++$row) { + $rowLocale = $sheet->getCell("A$row")->getValue(); + if (in_array($rowLocale, $sameAsEnglish, true) && in_array($locale, $sameAsEnglish, true)) { + $expectedResult = 'text'; + } else { + $expectedResult = ($locale === $sheet->getCell("A$row")->getValue()) ? 'text' : '#REF!'; + } + self::assertSame($expectedResult, $sheet->getCell("B$row")->getCalculatedValue(), "Locale $locale error in cell B$row $rowLocale"); + } + } + + public function providerInternational(): array + { + return [ + 'English' => ['en'], + 'French' => ['fr'], + 'German' => ['de'], + 'Made-up' => ['xx'], + 'Spanish' => ['es'], + 'Russian' => ['ru'], + 'Czech' => ['cs'], + 'Polish' => ['pl'], + 'Turkish' => ['tr'], + ]; + } + + /** + * @dataProvider providerRelativeInternational + */ + public function testRelativeInternational(string $locale, string $cell, string $relative): void + { + Settings::setLocale($locale); + $sheet = $this->getSheet(); + $sheet->getCell('C3')->setValue('text'); + $sheet->getCell($cell)->setValue("=INDIRECT(\"$relative\", false)"); + self::assertSame('text', $sheet->getCell($cell)->getCalculatedValue()); + } + + public function providerRelativeInternational(): array + { + return [ + 'English A3' => ['en', 'A3', 'R[]C[+2]'], + 'French B4' => ['fr', 'B4', 'L[-1]C[+1]'], + 'German C5' => ['de', 'C5', 'Z[-2]S[]'], + 'Spanish E1' => ['es', 'E1', 'F[+2]C[-2]'], + ]; + } + + /** + * @dataProvider providerCompatibility + */ + public function testCompatibilityInternational(string $compatibilityMode): void + { + Functions::setCompatibilityMode($compatibilityMode); + if ($compatibilityMode === Functions::COMPATIBILITY_EXCEL) { + $expected1 = '#REF!'; + $expected2 = 'text'; + } else { + $expected2 = '#REF!'; + $expected1 = 'text'; + } + Settings::setLocale('fr'); + $sheet = $this->getSheet(); + $sheet->getCell('C3')->setValue('text'); + $sheet->getCell('A1')->setValue('=INDIRECT("R3C3", false)'); + $sheet->getCell('A2')->setValue('=INDIRECT("L3C3", false)'); + self::assertSame($expected1, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame($expected2, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerCompatibility(): array + { + return [ + [Functions::COMPATIBILITY_EXCEL], + [Functions::COMPATIBILITY_OPENOFFICE], + [Functions::COMPATIBILITY_GNUMERIC], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php index 7601e336..accfc058 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php @@ -132,4 +132,48 @@ class IndirectTest extends AllSetupTeardown $result = \PhpOffice\PhpSpreadsheet\Calculation\Functions::flattenSingleValue($result); self::assertSame('This is it', $result); } + + /** + * @param null|int|string $expectedResult + * + * @dataProvider providerRelative + */ + public function testR1C1Relative($expectedResult, string $address): void + { + $sheet = $this->getSheet(); + $sheet->fromArray([ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], + ['a3', 'b3', 'c3'], + ['a4', 'b4', 'c4'], + ]); + $sheet->getCell('B2')->setValue('=INDIRECT("' . $address . '", false)'); + self::assertSame($expectedResult, $sheet->getCell('B2')->getCalculatedValue()); + } + + public function providerRelative(): array + { + return [ + 'same row with bracket next column' => ['c2', 'R[]C[+1]'], + 'same row without bracket next column' => ['c2', 'RC[+1]'], + 'same row without bracket next column no plus sign' => ['c2', 'RC[1]'], + 'same row previous column' => ['a2', 'RC[-1]'], + 'previous row previous column' => ['a1', 'R[-1]C[-1]'], + 'previous row same column with bracket' => ['b1', 'R[-1]C[]'], + 'previous row same column without bracket' => ['b1', 'R[-1]C'], + 'previous row next column' => ['c1', 'R[-1]C[+1]'], + 'next row no plus sign previous column' => ['a3', 'R[1]C[-1]'], + 'next row previous column' => ['a3', 'R[+1]C[-1]'], + 'next row same column' => ['b3', 'R[+1]C'], + 'next row next column' => ['c3', 'R[+1]C[+1]'], + 'two rows down same column' => ['b4', 'R[+2]C'], + 'invalid row' => ['#REF!', 'R[-2]C'], + 'invalid column' => ['#REF!', 'RC[-2]'], + 'circular reference' => [0, 'RC'], // matches Excel's treatment + 'absolute row absolute column' => ['c2', 'R2C3'], + 'absolute row relative column' => ['a2', 'R2C[-1]'], + 'relative row absolute column lowercase' => ['a2', 'rc1'], + 'uninitialized cell' => [null, 'RC[+2]'], // Excel result is 0 + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php b/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php index e2384460..ac1f15a1 100644 --- a/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php @@ -19,18 +19,23 @@ class TranslationTest extends TestCase */ private $returnDate; + /** @var string */ + private $locale; + protected function setUp(): void { $this->compatibilityMode = Functions::getCompatibilityMode(); $this->returnDate = Functions::getReturnDateType(); Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + $this->locale = Settings::getLocale(); } protected function tearDown(): void { Functions::setCompatibilityMode($this->compatibilityMode); Functions::setReturnDateType($this->returnDate); + Settings::setLocale($this->locale); } /** diff --git a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php index e7429a25..ca9d5183 100644 --- a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php +++ b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php @@ -11,31 +11,59 @@ class LocaleGeneratorTest extends TestCase { public function testLocaleGenerator(): void { + $directory = realpath(__DIR__ . '/../../src/PhpSpreadsheet/Calculation/locale/') ?: ''; + self::assertNotEquals('', $directory); $phpSpreadsheetFunctionsProperty = (new ReflectionClass(Calculation::class)) ->getProperty('phpSpreadsheetFunctions'); $phpSpreadsheetFunctionsProperty->setAccessible(true); $phpSpreadsheetFunctions = $phpSpreadsheetFunctionsProperty->getValue(); $localeGenerator = new LocaleGenerator( - (string) realpath(__DIR__ . '/../../src/PhpSpreadsheet/Calculation/locale/'), + $directory . DIRECTORY_SEPARATOR, 'Translations.xlsx', $phpSpreadsheetFunctions ); $localeGenerator->generateLocales(); $testLocales = [ + 'bg', + 'cs', + 'da', + 'de', + 'en', + 'es', + 'fi', 'fr', + 'hu', + 'it', + 'nb', 'nl', + 'pl', 'pt', - 'pt_br', 'ru', + 'sv', + 'tr', ]; - foreach ($testLocales as $locale) { - $locale = str_replace('_', '/', $locale); - $path = realpath(__DIR__ . "/../../src/PhpSpreadsheet/Calculation/locale/{$locale}"); - self::assertFileExists("{$path}/config"); - self::assertFileExists("{$path}/functions"); + $count = count(glob($directory . DIRECTORY_SEPARATOR . '*') ?: []) - 1; // exclude Translations.xlsx + self::assertCount($count, $testLocales); + $testLocales[] = 'pt_br'; + $testLocales[] = 'en_uk'; + $noconfig = ['en']; + $nofunctions = ['en', 'en_uk']; + foreach ($testLocales as $originalLocale) { + $locale = str_replace('_', DIRECTORY_SEPARATOR, $originalLocale); + $path = $directory . DIRECTORY_SEPARATOR . $locale; + if (in_array($originalLocale, $noconfig, true)) { + self::assertFileDoesNotExist($path . DIRECTORY_SEPARATOR . 'config'); + } else { + self::assertFileExists($path . DIRECTORY_SEPARATOR . 'config'); + } + if (in_array($originalLocale, $nofunctions, true)) { + self::assertFileDoesNotExist($path . DIRECTORY_SEPARATOR . 'functions'); + } else { + self::assertFileExists($path . DIRECTORY_SEPARATOR . 'functions'); + } } } } diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php index f9321102..429874dc 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php @@ -2,11 +2,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Csv; -use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\IValueBinder; use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder; use PhpOffice\PhpSpreadsheet\Reader\Csv; +use PhpOffice\PhpSpreadsheet\Settings; use PHPUnit\Framework\TestCase; class CsvIssue2232Test extends TestCase @@ -16,14 +16,19 @@ class CsvIssue2232Test extends TestCase */ private $valueBinder; + /** @var string */ + private $locale; + protected function setUp(): void { $this->valueBinder = Cell::getValueBinder(); + $this->locale = Settings::getLocale(); } protected function tearDown(): void { Cell::setValueBinder($this->valueBinder); + Settings::setLocale($this->locale); } /** @@ -78,7 +83,7 @@ class CsvIssue2232Test extends TestCase Cell::setValueBinder($binder); } - Calculation::getInstance()->setLocale('fr'); + Settings::setLocale('fr'); $reader = new Csv(); $filename = 'tests/data/Reader/CSV/issue.2232.csv'; diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php index 97476ed5..ae79dd0e 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php @@ -14,19 +14,25 @@ class PageSetupTest extends TestCase private const MARGIN_UNIT_CONVERSION = 2.54; // Inches to cm /** - * @var Spreadsheet + * @var ?Spreadsheet */ private $spreadsheet; - protected function setup(): void + /** @var string */ + private $filename = 'tests/data/Reader/Xml/PageSetup.xml'; + + protected function tearDown(): void { - $filename = 'tests/data/Reader/Xml/PageSetup.xml'; - $reader = new Xml(); - $this->spreadsheet = $reader->load($filename); + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } } public function testPageSetup(): void { + $reader = new Xml(); + $this->spreadsheet = $reader->load($this->filename); $assertions = $this->pageSetupAssertions(); foreach ($this->spreadsheet->getAllSheets() as $worksheet) { @@ -49,6 +55,8 @@ class PageSetupTest extends TestCase public function testPageMargins(): void { + $reader = new Xml(); + $this->spreadsheet = $reader->load($this->filename); $assertions = $this->pageMarginAssertions(); foreach ($this->spreadsheet->getAllSheets() as $worksheet) { diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php index 29d81299..9846b861 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php @@ -4,18 +4,51 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Xml; use DateTimeZone; use PhpOffice\PhpSpreadsheet\Reader\Xml; +use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Shared\Date; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class XmlLoadTest extends TestCase { - public function testLoad(): void + /** @var ?Spreadsheet */ + private $spreadsheet; + + /** @var string */ + private $locale; + + protected function setUp(): void + { + $this->locale = Settings::getLocale(); + } + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + Settings::setLocale($this->locale); + } + + public function testLoadEnglish(): void + { + $this->xtestLoad(); + } + + public function testLoadFrench(): void + { + Settings::setLocale('fr'); + $this->xtestLoad(); + } + + public function xtestLoad(): void { $filename = __DIR__ . '/../../../..' . '/samples/templates/excel2003.xml'; $reader = new Xml(); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(2, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(1); @@ -71,7 +104,7 @@ class XmlLoadTest extends TestCase $reader = new Xml(); $filter = new XmlFilter(); $reader->setReadFilter($filter); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(2, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(1); self::assertEquals('Report Data', $sheet->getTitle()); @@ -87,7 +120,7 @@ class XmlLoadTest extends TestCase . '/samples/templates/excel2003.xml'; $reader = new Xml(); $reader->setLoadSheetsOnly(['Unknown Sheet', 'Report Data']); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(1, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(0); self::assertEquals('Report Data', $sheet->getTitle()); @@ -102,7 +135,7 @@ class XmlLoadTest extends TestCase . '/../../../..' . '/samples/templates/excel2003.short.bad.xml'; $reader = new Xml(); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(1, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(0); self::assertEquals('Sample Data', $sheet->getTitle()); diff --git a/tests/data/Calculation/LookupRef/ADDRESS.php b/tests/data/Calculation/LookupRef/ADDRESS.php index b9e170d5..14a3cf7d 100644 --- a/tests/data/Calculation/LookupRef/ADDRESS.php +++ b/tests/data/Calculation/LookupRef/ADDRESS.php @@ -48,6 +48,22 @@ return [ false, 'EXCEL SHEET', ], + '0 instead of bool for 4th arg' => [ + "'EXCEL SHEET'!R2C3", + 2, + 3, + null, + 0, + 'EXCEL SHEET', + ], + '1 instead of bool for 4th arg' => [ + "'EXCEL SHEET'!\$C\$2", + 2, + 3, + null, + 1, + 'EXCEL SHEET', + ], [ "'EXCEL SHEET'!\$C\$2", 2, diff --git a/tests/data/Calculation/Translations.php b/tests/data/Calculation/Translations.php index 604c6721..0965bac1 100644 --- a/tests/data/Calculation/Translations.php +++ b/tests/data/Calculation/Translations.php @@ -80,4 +80,14 @@ return [ 'nb', '=MAX(ABS({2,-3;-4,5}), ABS{-2,3;4,-5})', ], + 'not fooled by *RC' => [ + '=3*RC(B1)', + 'fr', + '=3*RC(B1)', + ], + 'handle * for ROW' => [ + '=3*LIGNE(B1)', + 'fr', + '=3*ROW(B1)', + ], ];