Skip to content

Commit

Permalink
Merge branch 'master' into crstyle
Browse files Browse the repository at this point in the history
  • Loading branch information
oleibman authored Jan 20, 2025
2 parents a6cd613 + f98fd23 commit a72b206
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 19 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Added

- Methods to get style for row or column. [PR #4317](https://github.com/PHPOffice/PhpSpreadsheet/pull/4317)
- Method for duplicating worksheet in spreadsheet. [PR #4315](https://github.com/PHPOffice/PhpSpreadsheet/pull/4315)

### Changed

Expand All @@ -25,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).

### Fixed

- Nothing yet.
- Ods Reader Sheet Names with Period. [Issue #4311](https://github.com/PHPOffice/PhpSpreadsheet/issues/4311) [PR #4313](https://github.com/PHPOffice/PhpSpreadsheet/pull/4313)

## 2025-01-11 - 3.8.0

Expand Down
27 changes: 23 additions & 4 deletions docs/topics/worksheets.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,38 @@ insert the clone into the workbook.

```php
$clonedWorksheet = clone $spreadsheet->getSheetByName('Worksheet 1');
$clonedWorksheet->setTitle('Copy of Worksheet 1');
$clonedWorksheet->setTitle('Copy of Worksheet 1'); // must be unique
$spreadsheet->addSheet($clonedWorksheet);
```
Starting with PhpSpreadsheet 3.9.0, this can be done more simply (copied sheet's title will be set to something unique):
```php
$copiedWorksheet = $spreadsheet->duplicateWorksheetByTitle('sheetname');
```

You can also copy worksheets from one workbook to another, though this
is more complex as PhpSpreadsheet also has to replicate the styling
between the two workbooks. The `addExternalSheet()` method is provided for
this purpose.

$clonedWorksheet = clone $spreadsheet1->getSheetByName('Worksheet 1');
$spreadsheet->addExternalSheet($clonedWorksheet);
```php
$clonedWorksheet = clone $spreadsheet1->getSheetByName('Worksheet 1');
$clonedWorksheet->setTitle('Copy of Worksheet 1'); // must be unique
$spreadsheet1->addSheet($clonedWorksheet);
$spreadsheet->addExternalSheet($clonedWorksheet);
```
Starting with PhpSpreadsheet 3.8.0, this can be simplified:
```php
$clonedWorksheet = clone $spreadsheet1->getSheetByName('Worksheet 1');
$spreadsheet1->addSheet($clonedWorksheet, null, true);
$spreadsheet->addExternalSheet($clonedWorksheet);
```
Starting with PhpSpreadsheet 3.9.0, this can be simplified even further:
```php
$clonedWorksheet = $spreadsheet1->duplicateWorksheetByTitle('sheetname');
$spreadsheet->addExternalSheet($clonedWorksheet);
```

In both cases, it is the developer's responsibility to ensure that
In the cases commented "must be unique", it is the developer's responsibility to ensure that
worksheet names are not duplicated. PhpSpreadsheet will throw an
exception if you attempt to copy worksheets that will result in a
duplicate name.
Expand Down
26 changes: 22 additions & 4 deletions src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,24 @@

class FormulaTranslator
{
public static function convertToExcelAddressValue(string $openOfficeAddress): string
private static function replaceQuotedPeriod(string $value): string
{
$excelAddress = $openOfficeAddress;
$value2 = '';
$quoted = false;
foreach (mb_str_split($value, 1, 'UTF-8') as $char) {
if ($char === "'") {
$quoted = !$quoted;
} elseif ($char === '.' && $quoted) {
$char = "\u{fffe}";
}
$value2 .= $char;
}

return $value2;
}

public static function convertToExcelAddressValue(string $openOfficeAddress): string
{
// Cell range 3-d reference
// As we don't support 3-d ranges, we're just going to take a quick and dirty approach
// and assume that the second worksheet reference is the same as the first
Expand All @@ -20,15 +34,17 @@ public static function convertToExcelAddressValue(string $openOfficeAddress): st
'/\$?([^\.]+)\.([^\.]+)/miu', // Cell reference in another sheet
'/\.([^\.]+):\.([^\.]+)/miu', // Cell range reference
'/\.([^\.]+)/miu', // Simple cell reference
'/\\x{FFFE}/miu', // restore quoted periods
],
[
'$1!$2:$4',
'$1!$2:$3',
'$1!$2',
'$1:$2',
'$1',
'.',
],
$excelAddress
self::replaceQuotedPeriod($openOfficeAddress)
);

return $excelAddress;
Expand All @@ -52,14 +68,16 @@ public static function convertToExcelFormulaValue(string $openOfficeFormula): st
'/\[\$?([^\.]+)\.([^\.]+)\]/miu', // Cell reference in another sheet
'/\[\.([^\.]+):\.([^\.]+)\]/miu', // Cell range reference
'/\[\.([^\.]+)\]/miu', // Simple cell reference
'/\\x{FFFE}/miu', // restore quoted periods
],
[
'$1!$2:$3',
'$1!$2',
'$1:$2',
'$1',
'.',
],
$value
self::replaceQuotedPeriod($value)
);
// Convert references to defined names/formulae
$value = str_replace('$$', '', $value);
Expand Down
9 changes: 9 additions & 0 deletions src/PhpSpreadsheet/Spreadsheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,15 @@ public function sheetNameExists(string $worksheetName): bool
return $this->getSheetByName($worksheetName) !== null;
}

public function duplicateWorksheetByTitle(string $title): Worksheet
{
$original = $this->getSheetByNameOrThrow($title);
$index = $this->getIndex($original) + 1;
$clone = clone $original;

return $this->addSheet($clone, $index, true);
}

/**
* Add sheet.
*
Expand Down
35 changes: 25 additions & 10 deletions tests/PhpSpreadsheetTests/Document/PropertiesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use DateTimeZone;
use PhpOffice\PhpSpreadsheet\Document\Properties;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class PropertiesTest extends TestCase
Expand All @@ -16,7 +17,7 @@ class PropertiesTest extends TestCase

private float $startTime;

protected function setup(): void
protected function setUp(): void
{
do {
// loop to avoid rare situation where timestamp changes
Expand Down Expand Up @@ -44,12 +45,19 @@ public function testSetCreator(): void
self::assertSame($creator, $this->properties->getCreator());
}

#[\PHPUnit\Framework\Attributes\DataProvider('providerCreationTime')]
#[DataProvider('providerCreationTime')]
public function testSetCreated(null|int $expectedCreationTime, null|int|string $created): void
{
$expectedCreationTime = $expectedCreationTime ?? $this->startTime;

$this->properties->setCreated($created);
if ($expectedCreationTime === null) {
do {
// loop to avoid rare situation where timestamp changes
$expectedCreationTime = (float) (new DateTime())->format('U');
$this->properties->setCreated($created);
$endTime = (float) (new DateTime())->format('U');
} while ($expectedCreationTime !== $endTime);
} else {
$this->properties->setCreated($created);
}
self::assertEquals($expectedCreationTime, $this->properties->getCreated());
}

Expand All @@ -71,12 +79,19 @@ public function testSetModifier(): void
self::assertSame($creator, $this->properties->getLastModifiedBy());
}

#[\PHPUnit\Framework\Attributes\DataProvider('providerModifiedTime')]
#[DataProvider('providerModifiedTime')]
public function testSetModified(mixed $expectedModifiedTime, null|int|string $modified): void
{
$expectedModifiedTime = $expectedModifiedTime ?? $this->startTime;

$this->properties->setModified($modified);
if ($expectedModifiedTime === null) {
do {
// loop to avoid rare situation where timestamp changes
$expectedModifiedTime = (float) (new DateTime())->format('U');
$this->properties->setModified($modified);
$endTime = (float) (new DateTime())->format('U');
} while ($expectedModifiedTime !== $endTime);
} else {
$this->properties->setModified($modified);
}
self::assertEquals($expectedModifiedTime, $this->properties->getModified());
}

Expand Down Expand Up @@ -146,7 +161,7 @@ public function testSetManager(): void
self::assertSame($manager, $this->properties->getManager());
}

#[\PHPUnit\Framework\Attributes\DataProvider('providerCustomProperties')]
#[DataProvider('providerCustomProperties')]
public function testSetCustomProperties(mixed $expectedType, mixed $expectedValue, string $propertyName, null|bool|float|int|string $propertyValue, ?string $propertyType = null): void
{
if ($propertyType === null) {
Expand Down
62 changes: 62 additions & 0 deletions tests/PhpSpreadsheetTests/Reader/Ods/FormulaTranslatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace PhpOffice\PhpSpreadsheetTests\Reader\Ods;

use PhpOffice\PhpSpreadsheet\Reader\Ods\FormulaTranslator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class FormulaTranslatorTest extends TestCase
{
#[DataProvider('addressesProvider')]
public function testAddresses(string $result, string $address): void
{
self::assertSame($result, FormulaTranslator::convertToExcelAddressValue($address));
}

public static function addressesProvider(): array
{
return [
'range period in sheet name' => ["'sheet1.bug'!a1:a5", "'sheet1.bug'.a1:'sheet1.bug'.a5"],
'range special chars and period in sheet name' => ["'#sheet1.x'!a1:a5", "'#sheet1.x'.a1:'#sheet1.x'.a5"],
'cell period in sheet name' => ["'sheet1.bug'!b9", "'sheet1.bug'.b9"],
'range unquoted sheet name' => ['sheet1!b9:c12', 'sheet1.b9:sheet1.c12'],
'range unquoted sheet name with $' => ['sheet1!$b9:c$12', 'sheet1.$b9:sheet1.c$12'],
'range quoted sheet name with $' => ["'sheet1'!\$b9:c\$12", '\'sheet1\'.$b9:\'sheet1\'.c$12'],
'cell unquoted sheet name' => ['sheet1!B$9', 'sheet1.B$9'],
'range no sheet name all dollars' => ['$B$9:$C$12', '$B$9:$C$12'],
'range no sheet name some dollars' => ['B$9:$C12', 'B$9:$C12'],
'range no sheet name no dollars' => ['B9:C12', 'B9:C12'],
];
}

#[DataProvider('formulaProvider')]
public function testFormulas(string $result, string $formula): void
{
self::assertSame($result, FormulaTranslator::convertToExcelFormulaValue($formula));
}

public static function formulaProvider(): array
{
return [
'ranges no sheet name' => [
'SUM(A5:A7,B$5:$B7)',
'SUM([.A5:.A7];[.B$5:.$B7])',
],
'ranges sheet name with period' => [
'SUM(\'test.bug\'!A5:A7,\'test.bug\'!B5:B7)',
'SUM([\'test.bug\'.A5:.A7];[\'test.bug\'.B5:.B7])',
],
'ranges unquoted sheet name' => [
'SUM(testbug!A5:A7,testbug!B5:B7)',
'SUM([testbug.A5:.A7];[testbug.B5:.B7])',
],
'ranges quoted sheet name without period' => [
'SUM(\'testbug\'!A5:A7,\'testbug\'!B5:B7)',
'SUM([\'testbug\'.A5:.A7];[\'testbug\'.B5:.B7])',
],
];
}
}
59 changes: 59 additions & 0 deletions tests/PhpSpreadsheetTests/SpreadsheetDuplicateSheetTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace PhpOffice\PhpSpreadsheetTests;

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;

class SpreadsheetDuplicateSheetTest extends TestCase
{
private ?Spreadsheet $spreadsheet = null;

protected function tearDown(): void
{
if ($this->spreadsheet !== null) {
$this->spreadsheet->disconnectWorksheets();
$this->spreadsheet = null;
}
}

public function testDuplicate(): void
{
$this->spreadsheet = new Spreadsheet();
$sheet = $this->spreadsheet->getActiveSheet();
$sheet->setTitle('original');
$sheet->getCell('A1')->setValue('text1');
$sheet->getCell('A2')->setValue('text2');
$sheet->getStyle('A1')
->getFont()
->setBold(true);
$sheet3 = $this->spreadsheet->createSheet();
$sheet3->setTitle('added');
$newSheet = $this->spreadsheet
->duplicateWorksheetByTitle('original');
$this->spreadsheet->duplicateWorksheetByTitle('added');
self::assertSame('original 1', $newSheet->getTitle());
self::assertSame(
'text1',
$newSheet->getCell('A1')->getValue()
);
self::assertSame(
'text2',
$newSheet->getCell('A2')->getValue()
);
self::assertTrue(
$newSheet->getStyle('A1')->getFont()->getBold()
);
self::assertFalse(
$newSheet->getStyle('A2')->getFont()->getBold()
);
$sheetNames = [];
foreach ($this->spreadsheet->getWorksheetIterator() as $worksheet) {
$sheetNames[] = $worksheet->getTitle();
}
$expected = ['original', 'original 1', 'added', 'added 1'];
self::assertSame($expected, $sheetNames);
}
}
25 changes: 25 additions & 0 deletions tests/PhpSpreadsheetTests/Writer/Ods/AutoFilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,29 @@ public function testAutoFilterWriter(): void

self::assertSame('A1:C9', $reloaded->getActiveSheet()->getAutoFilter()->getRange());
}

public function testPeriodInSheetNames(): void
{
$spreadsheet = new Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$worksheet->setTitle('work.sheet');

$dataSet = [
['Year', 'Quarter', 'Sales'],
[2020, 'Q1', 100],
[2020, 'Q2', 120],
[2020, 'Q3', 140],
[2020, 'Q4', 160],
[2021, 'Q1', 180],
[2021, 'Q2', 75],
[2021, 'Q3', 0],
[2021, 'Q4', 0],
];
$worksheet->fromArray($dataSet, null, 'A1');
$worksheet->getAutoFilter()->setRange('A1:C9');

$reloaded = $this->writeAndReload($spreadsheet, 'Ods');

self::assertSame('A1:C9', $reloaded->getActiveSheet()->getAutoFilter()->getRange());
}
}

0 comments on commit a72b206

Please sign in to comment.