Skip to content

Commit a72b206

Browse files
authored
Merge branch 'master' into crstyle
2 parents a6cd613 + f98fd23 commit a72b206

File tree

8 files changed

+227
-19
lines changed

8 files changed

+227
-19
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1010
### Added
1111

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

1415
### Changed
1516

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

2627
### Fixed
2728

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

3031
## 2025-01-11 - 3.8.0
3132

docs/topics/worksheets.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,19 +95,38 @@ insert the clone into the workbook.
9595

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

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

107-
$clonedWorksheet = clone $spreadsheet1->getSheetByName('Worksheet 1');
108-
$spreadsheet->addExternalSheet($clonedWorksheet);
111+
```php
112+
$clonedWorksheet = clone $spreadsheet1->getSheetByName('Worksheet 1');
113+
$clonedWorksheet->setTitle('Copy of Worksheet 1'); // must be unique
114+
$spreadsheet1->addSheet($clonedWorksheet);
115+
$spreadsheet->addExternalSheet($clonedWorksheet);
116+
```
117+
Starting with PhpSpreadsheet 3.8.0, this can be simplified:
118+
```php
119+
$clonedWorksheet = clone $spreadsheet1->getSheetByName('Worksheet 1');
120+
$spreadsheet1->addSheet($clonedWorksheet, null, true);
121+
$spreadsheet->addExternalSheet($clonedWorksheet);
122+
```
123+
Starting with PhpSpreadsheet 3.9.0, this can be simplified even further:
124+
```php
125+
$clonedWorksheet = $spreadsheet1->duplicateWorksheetByTitle('sheetname');
126+
$spreadsheet->addExternalSheet($clonedWorksheet);
127+
```
109128

110-
In both cases, it is the developer's responsibility to ensure that
129+
In the cases commented "must be unique", it is the developer's responsibility to ensure that
111130
worksheet names are not duplicated. PhpSpreadsheet will throw an
112131
exception if you attempt to copy worksheets that will result in a
113132
duplicate name.

src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,24 @@
66

77
class FormulaTranslator
88
{
9-
public static function convertToExcelAddressValue(string $openOfficeAddress): string
9+
private static function replaceQuotedPeriod(string $value): string
1010
{
11-
$excelAddress = $openOfficeAddress;
11+
$value2 = '';
12+
$quoted = false;
13+
foreach (mb_str_split($value, 1, 'UTF-8') as $char) {
14+
if ($char === "'") {
15+
$quoted = !$quoted;
16+
} elseif ($char === '.' && $quoted) {
17+
$char = "\u{fffe}";
18+
}
19+
$value2 .= $char;
20+
}
1221

22+
return $value2;
23+
}
24+
25+
public static function convertToExcelAddressValue(string $openOfficeAddress): string
26+
{
1327
// Cell range 3-d reference
1428
// As we don't support 3-d ranges, we're just going to take a quick and dirty approach
1529
// and assume that the second worksheet reference is the same as the first
@@ -20,15 +34,17 @@ public static function convertToExcelAddressValue(string $openOfficeAddress): st
2034
'/\$?([^\.]+)\.([^\.]+)/miu', // Cell reference in another sheet
2135
'/\.([^\.]+):\.([^\.]+)/miu', // Cell range reference
2236
'/\.([^\.]+)/miu', // Simple cell reference
37+
'/\\x{FFFE}/miu', // restore quoted periods
2338
],
2439
[
2540
'$1!$2:$4',
2641
'$1!$2:$3',
2742
'$1!$2',
2843
'$1:$2',
2944
'$1',
45+
'.',
3046
],
31-
$excelAddress
47+
self::replaceQuotedPeriod($openOfficeAddress)
3248
);
3349

3450
return $excelAddress;
@@ -52,14 +68,16 @@ public static function convertToExcelFormulaValue(string $openOfficeFormula): st
5268
'/\[\$?([^\.]+)\.([^\.]+)\]/miu', // Cell reference in another sheet
5369
'/\[\.([^\.]+):\.([^\.]+)\]/miu', // Cell range reference
5470
'/\[\.([^\.]+)\]/miu', // Simple cell reference
71+
'/\\x{FFFE}/miu', // restore quoted periods
5572
],
5673
[
5774
'$1!$2:$3',
5875
'$1!$2',
5976
'$1:$2',
6077
'$1',
78+
'.',
6179
],
62-
$value
80+
self::replaceQuotedPeriod($value)
6381
);
6482
// Convert references to defined names/formulae
6583
$value = str_replace('$$', '', $value);

src/PhpSpreadsheet/Spreadsheet.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,15 @@ public function sheetNameExists(string $worksheetName): bool
529529
return $this->getSheetByName($worksheetName) !== null;
530530
}
531531

532+
public function duplicateWorksheetByTitle(string $title): Worksheet
533+
{
534+
$original = $this->getSheetByNameOrThrow($title);
535+
$index = $this->getIndex($original) + 1;
536+
$clone = clone $original;
537+
538+
return $this->addSheet($clone, $index, true);
539+
}
540+
532541
/**
533542
* Add sheet.
534543
*

tests/PhpSpreadsheetTests/Document/PropertiesTest.php

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use DateTimeZone;
99
use PhpOffice\PhpSpreadsheet\Document\Properties;
1010
use PhpOffice\PhpSpreadsheet\Shared\Date;
11+
use PHPUnit\Framework\Attributes\DataProvider;
1112
use PHPUnit\Framework\TestCase;
1213

1314
class PropertiesTest extends TestCase
@@ -16,7 +17,7 @@ class PropertiesTest extends TestCase
1617

1718
private float $startTime;
1819

19-
protected function setup(): void
20+
protected function setUp(): void
2021
{
2122
do {
2223
// loop to avoid rare situation where timestamp changes
@@ -44,12 +45,19 @@ public function testSetCreator(): void
4445
self::assertSame($creator, $this->properties->getCreator());
4546
}
4647

47-
#[\PHPUnit\Framework\Attributes\DataProvider('providerCreationTime')]
48+
#[DataProvider('providerCreationTime')]
4849
public function testSetCreated(null|int $expectedCreationTime, null|int|string $created): void
4950
{
50-
$expectedCreationTime = $expectedCreationTime ?? $this->startTime;
51-
52-
$this->properties->setCreated($created);
51+
if ($expectedCreationTime === null) {
52+
do {
53+
// loop to avoid rare situation where timestamp changes
54+
$expectedCreationTime = (float) (new DateTime())->format('U');
55+
$this->properties->setCreated($created);
56+
$endTime = (float) (new DateTime())->format('U');
57+
} while ($expectedCreationTime !== $endTime);
58+
} else {
59+
$this->properties->setCreated($created);
60+
}
5361
self::assertEquals($expectedCreationTime, $this->properties->getCreated());
5462
}
5563

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

74-
#[\PHPUnit\Framework\Attributes\DataProvider('providerModifiedTime')]
82+
#[DataProvider('providerModifiedTime')]
7583
public function testSetModified(mixed $expectedModifiedTime, null|int|string $modified): void
7684
{
77-
$expectedModifiedTime = $expectedModifiedTime ?? $this->startTime;
78-
79-
$this->properties->setModified($modified);
85+
if ($expectedModifiedTime === null) {
86+
do {
87+
// loop to avoid rare situation where timestamp changes
88+
$expectedModifiedTime = (float) (new DateTime())->format('U');
89+
$this->properties->setModified($modified);
90+
$endTime = (float) (new DateTime())->format('U');
91+
} while ($expectedModifiedTime !== $endTime);
92+
} else {
93+
$this->properties->setModified($modified);
94+
}
8095
self::assertEquals($expectedModifiedTime, $this->properties->getModified());
8196
}
8297

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

149-
#[\PHPUnit\Framework\Attributes\DataProvider('providerCustomProperties')]
164+
#[DataProvider('providerCustomProperties')]
150165
public function testSetCustomProperties(mixed $expectedType, mixed $expectedValue, string $propertyName, null|bool|float|int|string $propertyValue, ?string $propertyType = null): void
151166
{
152167
if ($propertyType === null) {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Ods;
6+
7+
use PhpOffice\PhpSpreadsheet\Reader\Ods\FormulaTranslator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class FormulaTranslatorTest extends TestCase
12+
{
13+
#[DataProvider('addressesProvider')]
14+
public function testAddresses(string $result, string $address): void
15+
{
16+
self::assertSame($result, FormulaTranslator::convertToExcelAddressValue($address));
17+
}
18+
19+
public static function addressesProvider(): array
20+
{
21+
return [
22+
'range period in sheet name' => ["'sheet1.bug'!a1:a5", "'sheet1.bug'.a1:'sheet1.bug'.a5"],
23+
'range special chars and period in sheet name' => ["'#sheet1.x'!a1:a5", "'#sheet1.x'.a1:'#sheet1.x'.a5"],
24+
'cell period in sheet name' => ["'sheet1.bug'!b9", "'sheet1.bug'.b9"],
25+
'range unquoted sheet name' => ['sheet1!b9:c12', 'sheet1.b9:sheet1.c12'],
26+
'range unquoted sheet name with $' => ['sheet1!$b9:c$12', 'sheet1.$b9:sheet1.c$12'],
27+
'range quoted sheet name with $' => ["'sheet1'!\$b9:c\$12", '\'sheet1\'.$b9:\'sheet1\'.c$12'],
28+
'cell unquoted sheet name' => ['sheet1!B$9', 'sheet1.B$9'],
29+
'range no sheet name all dollars' => ['$B$9:$C$12', '$B$9:$C$12'],
30+
'range no sheet name some dollars' => ['B$9:$C12', 'B$9:$C12'],
31+
'range no sheet name no dollars' => ['B9:C12', 'B9:C12'],
32+
];
33+
}
34+
35+
#[DataProvider('formulaProvider')]
36+
public function testFormulas(string $result, string $formula): void
37+
{
38+
self::assertSame($result, FormulaTranslator::convertToExcelFormulaValue($formula));
39+
}
40+
41+
public static function formulaProvider(): array
42+
{
43+
return [
44+
'ranges no sheet name' => [
45+
'SUM(A5:A7,B$5:$B7)',
46+
'SUM([.A5:.A7];[.B$5:.$B7])',
47+
],
48+
'ranges sheet name with period' => [
49+
'SUM(\'test.bug\'!A5:A7,\'test.bug\'!B5:B7)',
50+
'SUM([\'test.bug\'.A5:.A7];[\'test.bug\'.B5:.B7])',
51+
],
52+
'ranges unquoted sheet name' => [
53+
'SUM(testbug!A5:A7,testbug!B5:B7)',
54+
'SUM([testbug.A5:.A7];[testbug.B5:.B7])',
55+
],
56+
'ranges quoted sheet name without period' => [
57+
'SUM(\'testbug\'!A5:A7,\'testbug\'!B5:B7)',
58+
'SUM([\'testbug\'.A5:.A7];[\'testbug\'.B5:.B7])',
59+
],
60+
];
61+
}
62+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests;
6+
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class SpreadsheetDuplicateSheetTest extends TestCase
11+
{
12+
private ?Spreadsheet $spreadsheet = null;
13+
14+
protected function tearDown(): void
15+
{
16+
if ($this->spreadsheet !== null) {
17+
$this->spreadsheet->disconnectWorksheets();
18+
$this->spreadsheet = null;
19+
}
20+
}
21+
22+
public function testDuplicate(): void
23+
{
24+
$this->spreadsheet = new Spreadsheet();
25+
$sheet = $this->spreadsheet->getActiveSheet();
26+
$sheet->setTitle('original');
27+
$sheet->getCell('A1')->setValue('text1');
28+
$sheet->getCell('A2')->setValue('text2');
29+
$sheet->getStyle('A1')
30+
->getFont()
31+
->setBold(true);
32+
$sheet3 = $this->spreadsheet->createSheet();
33+
$sheet3->setTitle('added');
34+
$newSheet = $this->spreadsheet
35+
->duplicateWorksheetByTitle('original');
36+
$this->spreadsheet->duplicateWorksheetByTitle('added');
37+
self::assertSame('original 1', $newSheet->getTitle());
38+
self::assertSame(
39+
'text1',
40+
$newSheet->getCell('A1')->getValue()
41+
);
42+
self::assertSame(
43+
'text2',
44+
$newSheet->getCell('A2')->getValue()
45+
);
46+
self::assertTrue(
47+
$newSheet->getStyle('A1')->getFont()->getBold()
48+
);
49+
self::assertFalse(
50+
$newSheet->getStyle('A2')->getFont()->getBold()
51+
);
52+
$sheetNames = [];
53+
foreach ($this->spreadsheet->getWorksheetIterator() as $worksheet) {
54+
$sheetNames[] = $worksheet->getTitle();
55+
}
56+
$expected = ['original', 'original 1', 'added', 'added 1'];
57+
self::assertSame($expected, $sheetNames);
58+
}
59+
}

tests/PhpSpreadsheetTests/Writer/Ods/AutoFilterTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,29 @@ public function testAutoFilterWriter(): void
3232

3333
self::assertSame('A1:C9', $reloaded->getActiveSheet()->getAutoFilter()->getRange());
3434
}
35+
36+
public function testPeriodInSheetNames(): void
37+
{
38+
$spreadsheet = new Spreadsheet();
39+
$worksheet = $spreadsheet->getActiveSheet();
40+
$worksheet->setTitle('work.sheet');
41+
42+
$dataSet = [
43+
['Year', 'Quarter', 'Sales'],
44+
[2020, 'Q1', 100],
45+
[2020, 'Q2', 120],
46+
[2020, 'Q3', 140],
47+
[2020, 'Q4', 160],
48+
[2021, 'Q1', 180],
49+
[2021, 'Q2', 75],
50+
[2021, 'Q3', 0],
51+
[2021, 'Q4', 0],
52+
];
53+
$worksheet->fromArray($dataSet, null, 'A1');
54+
$worksheet->getAutoFilter()->setRange('A1:C9');
55+
56+
$reloaded = $this->writeAndReload($spreadsheet, 'Ods');
57+
58+
self::assertSame('A1:C9', $reloaded->getActiveSheet()->getAutoFilter()->getRange());
59+
}
3560
}

0 commit comments

Comments
 (0)