Skip to content

Commit 07f4fbe

Browse files
committed
Initial implementation of the TEXTSPLIT() Excel Function
1 parent e748ac7 commit 07f4fbe

File tree

6 files changed

+301
-4
lines changed

6 files changed

+301
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
99

1010
### Added
1111

12-
- Implementation of the `TEXTBEFORE()` and `TEXTAFTER()` Excel Functions
12+
- Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions
1313

1414
### Changed
1515

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger;
88
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
99
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
10-
use PhpOffice\PhpSpreadsheet\Calculation\Information\Value;
1110
use PhpOffice\PhpSpreadsheet\Calculation\Token\Stack;
1211
use PhpOffice\PhpSpreadsheet\Cell\Cell;
1312
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
@@ -2505,8 +2504,8 @@ class Calculation
25052504
],
25062505
'TEXTSPLIT' => [
25072506
'category' => Category::CATEGORY_TEXT_AND_DATA,
2508-
'functionCall' => [Functions::class, 'DUMMY'],
2509-
'argumentCount' => '2-5',
2507+
'functionCall' => [TextData\Text::class, 'split'],
2508+
'argumentCount' => '2-6',
25102509
],
25112510
'THAIDAYOFWEEK' => [
25122511
'category' => Category::CATEGORY_DATE_AND_TIME,

src/PhpSpreadsheet/Calculation/TextData/Text.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PhpOffice\PhpSpreadsheet\Calculation\TextData;
44

55
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
6+
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
67

78
class Text
89
{
@@ -77,4 +78,133 @@ public static function test($testValue = '')
7778

7879
return null;
7980
}
81+
82+
/**
83+
* TEXTSPLIT.
84+
*
85+
* @param mixed $text the text that you're searching
86+
* @param null|array|string $columnDelimiter The text that marks the point where to spill the text across columns.
87+
* Multiple delimiters can be passed as an array of string values
88+
* @param null|array|string $rowDelimiter The text that marks the point where to spill the text down rows.
89+
* Multiple delimiters can be passed as an array of string values
90+
* @param bool $ignoreEmpty Specify FALSE to create an empty cell when two delimiters are consecutive.
91+
* true = create empty cells
92+
* false = skip empty cells
93+
* Defaults to TRUE, which creates an empty cell
94+
* @param bool $matchMode Determines whether the match is case-sensitive or not.
95+
* true = case-sensitive
96+
* false = case-insensitive
97+
* By default, a case-sensitive match is done.
98+
* @param mixed $padding The value with which to pad the result.
99+
* The default is #N/A.
100+
*
101+
* @return array the array built from the text, split by the row and column delimiters
102+
*/
103+
public static function split($text, $columnDelimiter = null, $rowDelimiter = null, bool $ignoreEmpty = false, bool $matchMode = true, $padding = '#N/A')
104+
{
105+
$text = Functions::flattenSingleValue($text);
106+
107+
$flags = self::matchFlags($matchMode);
108+
109+
if ($rowDelimiter !== null) {
110+
$delimiter = self::buildDelimiter($rowDelimiter);
111+
$rows = ($delimiter === '()')
112+
? [$text]
113+
: preg_split("/{$delimiter}/{$flags}", $text);
114+
} else {
115+
$rows = [$text];
116+
}
117+
118+
/** @var array $rows */
119+
if ($ignoreEmpty === true) {
120+
$rows = array_values(array_filter(
121+
$rows,
122+
function ($row) {
123+
return $row !== '';
124+
}
125+
));
126+
}
127+
128+
if ($columnDelimiter !== null) {
129+
$delimiter = self::buildDelimiter($columnDelimiter);
130+
array_walk(
131+
$rows,
132+
function (&$row) use ($delimiter, $flags, $ignoreEmpty): void {
133+
$row = ($delimiter === '()')
134+
? [$row]
135+
: preg_split("/{$delimiter}/{$flags}", $row);
136+
/** @var array $row */
137+
if ($ignoreEmpty === true) {
138+
$row = array_values(array_filter(
139+
$row,
140+
function ($value) {
141+
return $value !== '';
142+
}
143+
));
144+
}
145+
}
146+
);
147+
if ($ignoreEmpty === true) {
148+
$rows = array_values(array_filter(
149+
$rows,
150+
function ($row) {
151+
return $row !== [] && $row !== [''];
152+
}
153+
));
154+
}
155+
}
156+
157+
return self::applyPadding($rows, $padding);
158+
}
159+
160+
/**
161+
* @param mixed $padding
162+
*/
163+
private static function applyPadding(array $rows, $padding): array
164+
{
165+
$columnCount = array_reduce(
166+
$rows,
167+
function (int $counter, array $row): int {
168+
return max($counter, count($row));
169+
},
170+
0
171+
);
172+
173+
return array_map(
174+
function (array $row) use ($columnCount, $padding): array {
175+
return (count($row) < $columnCount)
176+
? array_merge($row, array_fill(0, $columnCount - count($row), $padding))
177+
: $row;
178+
},
179+
$rows
180+
);
181+
}
182+
183+
/**
184+
* @param null|array|string $delimiter the text that marks the point before which you want to split
185+
* Multiple delimiters can be passed as an array of string values
186+
*/
187+
private static function buildDelimiter($delimiter): string
188+
{
189+
$valueSet = Functions::flattenArray($delimiter);
190+
191+
if (is_array($delimiter) && count($valueSet) > 1) {
192+
$quotedDelimiters = array_map(
193+
function ($delimiter) {
194+
return preg_quote($delimiter ?? '');
195+
},
196+
$valueSet
197+
);
198+
$delimiters = implode('|', $quotedDelimiters);
199+
200+
return '(' . $delimiters . ')';
201+
}
202+
203+
return '(' . preg_quote(Functions::flattenSingleValue($delimiter)) . ')';
204+
}
205+
206+
private static function matchFlags(bool $matchMode): string
207+
{
208+
return ($matchMode === true) ? 'miu' : 'mu';
209+
}
80210
}

src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ class Xlfn
146146
. '|register[.]id'
147147
. '|textafter'
148148
. '|textbefore'
149+
. '|textsplit'
149150
. '|valuetotext'
150151
. ')(?=\\s*[(])/i';
151152

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData;
4+
5+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6+
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
7+
8+
class TextSplitTest extends AllSetupTeardown
9+
{
10+
private function setDelimiterArgument(array $argument, string $column): string
11+
{
12+
return '{' . $column . implode(',' . $column, range(1, count($argument))) . '}';
13+
}
14+
15+
/**
16+
* @param array|string $argument
17+
*/
18+
private function setDelimiterValues(Worksheet $worksheet, string $column, $argument): void
19+
{
20+
if (is_array($argument)) {
21+
foreach ($argument as $index => $value) {
22+
++$index;
23+
$worksheet->getCell("{$column}{$index}")->setValue($value);
24+
}
25+
} else {
26+
$worksheet->getCell("{$column}1")->setValue($argument);
27+
}
28+
}
29+
30+
/**
31+
* @dataProvider providerTEXTSPLIT
32+
*/
33+
public function testTextSplit(array $expectedResult, array $arguments): void
34+
{
35+
$text = $arguments[0];
36+
$columnDelimiter = $arguments[1];
37+
$rowDelimiter = $arguments[2];
38+
39+
$args = 'A1';
40+
$args .= (is_array($columnDelimiter)) ? ', ' . $this->setDelimiterArgument($columnDelimiter, 'B') : ', B1';
41+
$args .= (is_array($rowDelimiter)) ? ', ' . $this->setDelimiterArgument($rowDelimiter, 'C') : ', C1';
42+
$args .= (isset($arguments[3])) ? ", {$arguments[3]}" : ',';
43+
$args .= (isset($arguments[4])) ? ", {$arguments[4]}" : ',';
44+
$args .= (isset($arguments[5])) ? ", {$arguments[5]}" : ',';
45+
46+
$worksheet = $this->getSheet();
47+
$worksheet->getCell('A1')->setValue($text);
48+
$this->setDelimiterValues($worksheet, 'B', $columnDelimiter);
49+
$this->setDelimiterValues($worksheet, 'C', $rowDelimiter);
50+
$worksheet->getCell('H1')->setValue("=TEXTSPLIT({$args})");
51+
52+
$result = Calculation::getInstance($this->getSpreadsheet())->calculateCellValue($worksheet->getCell('H1'));
53+
self::assertSame($expectedResult, $result);
54+
}
55+
56+
public function providerTEXTSPLIT(): array
57+
{
58+
return require 'tests/data/Calculation/TextData/TEXTSPLIT.php';
59+
}
60+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
4+
5+
return [
6+
[
7+
[['Hello', 'World']],
8+
[
9+
'Hello World',
10+
' ',
11+
'',
12+
],
13+
],
14+
[
15+
[['Hello'], ['World']],
16+
[
17+
'Hello World',
18+
'',
19+
' ',
20+
],
21+
],
22+
[
23+
[['To', 'be', 'or', 'not', 'to', 'be']],
24+
[
25+
'To be or not to be',
26+
' ',
27+
'',
28+
],
29+
],
30+
[
31+
[
32+
['1', '2', '3'],
33+
['4', '5', '6'],
34+
],
35+
[
36+
'1,2,3;4,5,6',
37+
',',
38+
';',
39+
],
40+
],
41+
[
42+
[
43+
['Do', ' Or do not', ' There is no try', ' ', 'Anonymous'],
44+
],
45+
[
46+
'Do. Or do not. There is no try. -Anonymous',
47+
['.', '-'],
48+
'',
49+
],
50+
],
51+
[
52+
[['Do'], [' Or do not'], [' There is no try'], [' '], ['Anonymous']],
53+
[
54+
'Do. Or do not. There is no try. -Anonymous',
55+
'',
56+
['.', '-'],
57+
],
58+
],
59+
[
60+
[
61+
['Do', ' Or do not', ' There is no try', ' '],
62+
['Anonymous', ExcelError::NA(), ExcelError::NA(), ExcelError::NA()],
63+
],
64+
[
65+
'Do. Or do not. There is no try. -Anonymous',
66+
'.',
67+
'-',
68+
],
69+
],
70+
[
71+
[
72+
['', '', '1'],
73+
['', '', ExcelError::NA()],
74+
['', '2', ''],
75+
['3', ExcelError::NA(), ExcelError::NA()],
76+
['', ExcelError::NA(), ExcelError::NA()],
77+
['', '4', ExcelError::NA()],
78+
],
79+
[
80+
'--1|-|-2-|3||-4',
81+
'-',
82+
'|',
83+
],
84+
],
85+
[
86+
[
87+
['1'],
88+
['2'],
89+
['3'],
90+
['4'],
91+
],
92+
[
93+
'--1|-|-2-|3||-4',
94+
'-',
95+
'|',
96+
true,
97+
],
98+
],
99+
[
100+
[['', 'BCD', 'FGH', 'JKLMN', 'PQRST', 'VWXYZ']],
101+
[
102+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
103+
['A', 'E', 'I', 'O', 'U'],
104+
'',
105+
],
106+
],
107+
];

0 commit comments

Comments
 (0)