R1C1 Format and Internationalization, plus Relative Offsets (#3052)

* R1C1 Format and Internationalization, plus Relative Offsets

Fix #1704, albeit imperfectly. Excel's implementation of this feature makes it impossible to fix perfectly. I don't know why it was necessary to internationalize R1C1 in the first place - the benefits are so minimal,and the result is worksheets that break when opened in different locales. Ugh. I can't even find complete documentation about the format in different languages; I am using https://answers.microsoft.com/en-us/officeinsider/forum/all/indirect-function-is-broken-at-least-for-excel-in/1fcbcf20-a103-4172-abf1-2c0dfe848e60 as my definitive reference.

This fix concentrates on the original report, using the INDIRECT function; there may be other areas similarly affected. As with ambiguous date formats, PhpSpreadsheet will do a little better than Excel itself when reading spreadsheets with internationalized R1C1 by trying all possibilities before giving up. When it does give up, it will now return `#REF!`, as Excel does, rather than throwing an exception, which is certainly friendlier. Although read now works better, when writing it will use whatever the user specified, so spreadsheets breaking in the wrong locale will still happen.

There were some bugs that turned up as I added test cases, all of them concerning relative addressing in R1C1 format, e.g. `R[+1]C[-1]`. The regexp for validating the format allowed for minus signs, but not plus signs. Also, the relevant functions did not allow for passing the current cell address, which made relative addressing impossible. The code now allows these, and suitable test cases are added.

* Use Locale for Formats, but Not for XML

Implementing a suggestion from @MarkBaker to use the system locale for determining R1C1 format rather than looping through a set of regexes and accepting any that work. This is closer to how Excel itself operates. The assumption we are making is to use the first character of the translated ROW and COLUMN functions. This will not work for Russian or Bulgarian, where each starts with the same letter, but it appears that Russian, at least, still uses R1C1. So our algorithm will not use non-ASCII characters, nor characters where ROW and COLUMN start with the same letter, falling back to R/C in those cases. Turkish falls into that category. Czech uses an accented character for one of the functions, and I'm guessing to use the unaccented character in that case. Polish COLUMN function is NR.KOLUMNY, and I'm guessing to use K in that case.

The function that converts R1C1 references is also used by the XML reader *where the format is always R1C1*, not locale-based (confirmed by successfully opening in Excel an XML spreadsheet when my language is set to French). The conversion code now handles that distinction through the use of an extra parameter. Xml Reader Load Test is duplicated to confirm that spreadsheet is loaded properly whether the locale is English or French. (No, I did not add an INDIRECT function to the Xml spreadsheet.)

Tests CsvIssue2232Test and TranslationTest both changed locale without resetting it when done. That omission was exposed by the new code, and both are now corrected.

* OpenOffice and Gnumeric

OpenOffice and Gnumeric make it much easier to test with other languages - they can be handled with an environment variable. Sensibly, they require R and C as the characters for R1C1 notation regardless of the language. Change code to recognize this difference from Excel.

* Handle Output of ADDRESS Function

One other function has to deal with R1C1 format as a string. Unlike INDIRECT, which receives the string on input, ADDRESS generates the string on output. Ensure that the ADDRESS output is consistent with the INDIRECT input.

ADDRESS expects its 4th arg to be bool, but it can also accept int, and many examples on the net supply it as an int. This had not been handled properly, but is now corrected.

* More Structured Test

I earlier introduced a new test for relative R1C1 addressing. Rewrite it to be clearer.

* Add Row for This to Locale Spreadsheet

It took a while for me to figure out how it all works. I have added a new row (with English value `*RC`) to Translations.xlsx, in the "Lookup and Reference" section of sheet "Excel Functions". By starting the "function name" with an asterisk, it will not be confused with a "real" function (confirmed by a new test). This approach also gives us the flexibility to do something similar if another surprise case occurs in future; in particular, I think this is more flexible than adding this as another option on the "Excel Localisation" sheet. It also means that any errors or omissions in the list below will be handled as with any other translation problem, by updating the spreadsheet without needing to touch any code.

The spreadsheet has the following entries in the *RC row:
- first letter of ROW/COLUMN functions for da, de, es, fi, fr, hu, nl, nb, pt, pt_br, sv
- no value for locales where ROW/COLUMN functions start with same letter - bg, ru, tr
- no value for locales with a multi-part name for ROW and/or COLUMN - it, pl (I had not previously noted Italian as an exception)
- no value for locales where ROW and/or COLUMN starts with a non-ASCII character - cs (this would also apply to bg and ru which are already included under "same letter")
- it does nothing for locales which are defined on the "Excel Localisation" sheet but have no entries yet on the "Excel Functions" sheet (e.g. eu)

Note that all but the first bullet item will continue to use R/C, which leaves them no worse off than they were before this change.
This commit is contained in:
oleibman 2022-09-16 08:25:26 -07:00 committed by GitHub
parent a846a93f52
commit 90422bf1d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 436 additions and 38 deletions

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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}";
}
}

View File

@ -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];
}

View File

@ -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)) ||

View File

@ -245,6 +245,7 @@ ROWS = RÆKKER
RTD = RTD
TRANSPOSE = TRANSPONER
VLOOKUP = LOPSLAG
*RC = RK
##
## Matematiske og trigonometriske funktioner (Math & Trig Functions)

View File

@ -243,6 +243,7 @@ ROWS = ZEILEN
RTD = RTD
TRANSPOSE = MTRANS
VLOOKUP = SVERWEIS
*RC = ZS
##
## Mathematische und trigonometrische Funktionen (Math & Trig Functions)

View File

@ -245,6 +245,7 @@ ROWS = FILAS
RTD = RDTR
TRANSPOSE = TRANSPONER
VLOOKUP = BUSCARV
*RC = FC
##
## Funciones matemáticas y trigonométricas (Math & Trig Functions)

View File

@ -245,6 +245,7 @@ ROWS = RIVIT
RTD = RTD
TRANSPOSE = TRANSPONOI
VLOOKUP = PHAKU
*RC = RS
##
## Matemaattiset ja trigonometriset funktiot (Math & Trig Functions)

View File

@ -240,6 +240,7 @@ ROWS = LIGNES
RTD = RTD
TRANSPOSE = TRANSPOSE
VLOOKUP = RECHERCHEV
*RC = LC
##
## Fonctions mathématiques et trigonométriques (Math & Trig Functions)

View File

@ -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)

View File

@ -245,6 +245,7 @@ ROWS = RADER
RTD = RTD
TRANSPOSE = TRANSPONER
VLOOKUP = FINN.RAD
*RC = RK
##
## Matematikk- og trigonometrifunksjoner (Math & Trig Functions)

View File

@ -244,6 +244,7 @@ ROWS = RIJEN
RTD = RTG
TRANSPOSE = TRANSPONEREN
VLOOKUP = VERT.ZOEKEN
*RC = RK
##
## Wiskundige en trigonometrische functies (Math & Trig Functions)

View File

@ -242,6 +242,7 @@ ROWS = LINS
RTD = RTD
TRANSPOSE = TRANSPOR
VLOOKUP = PROCV
*RC = LC
##
## Funções matemáticas e trigonométricas (Math & Trig Functions)

View File

@ -245,6 +245,7 @@ ROWS = LINS
RTD = RTD
TRANSPOSE = TRANSPOR
VLOOKUP = PROCV
*RC = LC
##
## Funções matemáticas e trigonométricas (Math & Trig Functions)

View File

@ -243,6 +243,7 @@ ROWS = RADER
RTD = RTD
TRANSPOSE = TRANSPONERA
VLOOKUP = LETARAD
*RC = RK
##
## Matematiska och trigonometriska funktioner (Math & Trig Functions)

View File

@ -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]));
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Settings;
class AddressInternationalTest extends AllSetupTeardown
{
/** @var string */
private $locale;
protected function setUp(): void
{
parent::setUp();
$this->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'],
];
}
}

View File

@ -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
*

View File

@ -0,0 +1,132 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Settings;
class IndirectInternationalTest extends AllSetupTeardown
{
/** @var string */
private $locale;
protected function setUp(): void
{
parent::setUp();
$this->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],
];
}
}

View File

@ -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
];
}
}

View File

@ -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);
}
/**

View File

@ -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');
}
}
}
}

View File

@ -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';

View File

@ -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) {

View File

@ -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());

View File

@ -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,

View File

@ -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)',
],
];