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:
parent
a846a93f52
commit
90422bf1d2
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)) ||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -245,6 +245,7 @@ ROWS = RÆKKER
|
|||
RTD = RTD
|
||||
TRANSPOSE = TRANSPONER
|
||||
VLOOKUP = LOPSLAG
|
||||
*RC = RK
|
||||
|
||||
##
|
||||
## Matematiske og trigonometriske funktioner (Math & Trig Functions)
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@ ROWS = ZEILEN
|
|||
RTD = RTD
|
||||
TRANSPOSE = MTRANS
|
||||
VLOOKUP = SVERWEIS
|
||||
*RC = ZS
|
||||
|
||||
##
|
||||
## Mathematische und trigonometrische Funktionen (Math & Trig Functions)
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ ROWS = FILAS
|
|||
RTD = RDTR
|
||||
TRANSPOSE = TRANSPONER
|
||||
VLOOKUP = BUSCARV
|
||||
*RC = FC
|
||||
|
||||
##
|
||||
## Funciones matemáticas y trigonométricas (Math & Trig Functions)
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ ROWS = RIVIT
|
|||
RTD = RTD
|
||||
TRANSPOSE = TRANSPONOI
|
||||
VLOOKUP = PHAKU
|
||||
*RC = RS
|
||||
|
||||
##
|
||||
## Matemaattiset ja trigonometriset funktiot (Math & Trig Functions)
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ ROWS = LIGNES
|
|||
RTD = RTD
|
||||
TRANSPOSE = TRANSPOSE
|
||||
VLOOKUP = RECHERCHEV
|
||||
*RC = LC
|
||||
|
||||
##
|
||||
## Fonctions mathématiques et trigonométriques (Math & Trig 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)
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ ROWS = RADER
|
|||
RTD = RTD
|
||||
TRANSPOSE = TRANSPONER
|
||||
VLOOKUP = FINN.RAD
|
||||
*RC = RK
|
||||
|
||||
##
|
||||
## Matematikk- og trigonometrifunksjoner (Math & Trig Functions)
|
||||
|
|
|
|||
|
|
@ -244,6 +244,7 @@ ROWS = RIJEN
|
|||
RTD = RTG
|
||||
TRANSPOSE = TRANSPONEREN
|
||||
VLOOKUP = VERT.ZOEKEN
|
||||
*RC = RK
|
||||
|
||||
##
|
||||
## Wiskundige en trigonometrische functies (Math & Trig 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)
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ ROWS = LINS
|
|||
RTD = RTD
|
||||
TRANSPOSE = TRANSPOR
|
||||
VLOOKUP = PROCV
|
||||
*RC = LC
|
||||
|
||||
##
|
||||
## Funções matemáticas e trigonométricas (Math & Trig Functions)
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@ ROWS = RADER
|
|||
RTD = RTD
|
||||
TRANSPOSE = TRANSPONERA
|
||||
VLOOKUP = LETARAD
|
||||
*RC = RK
|
||||
|
||||
##
|
||||
## Matematiska och trigonometriska funktioner (Math & Trig Functions)
|
||||
|
|
|
|||
|
|
@ -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]));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in New Issue