Merge pull request #2975 from PHPOffice/TextFunctions-New-TextSplit
Initial implementation of the `TEXTSPLIT()` Excel Function
This commit is contained in:
commit
a7df02fd55
|
|
@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||
|
||||
### Added
|
||||
|
||||
- Implementation of the `TEXTBEFORE()` and `TEXTAFTER()` Excel Functions
|
||||
- Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ use PhpOffice\PhpSpreadsheet\Calculation\Engine\CyclicReferenceStack;
|
|||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\Value;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Token\Stack;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
|
|
@ -2505,8 +2504,8 @@ class Calculation
|
|||
],
|
||||
'TEXTSPLIT' => [
|
||||
'category' => Category::CATEGORY_TEXT_AND_DATA,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'argumentCount' => '2-5',
|
||||
'functionCall' => [TextData\Text::class, 'split'],
|
||||
'argumentCount' => '2-6',
|
||||
],
|
||||
'THAIDAYOFWEEK' => [
|
||||
'category' => Category::CATEGORY_DATE_AND_TIME,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace PhpOffice\PhpSpreadsheet\Calculation\TextData;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
|
||||
class Text
|
||||
{
|
||||
|
|
@ -77,4 +78,133 @@ class Text
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* TEXTSPLIT.
|
||||
*
|
||||
* @param mixed $text the text that you're searching
|
||||
* @param null|array|string $columnDelimiter The text that marks the point where to spill the text across columns.
|
||||
* Multiple delimiters can be passed as an array of string values
|
||||
* @param null|array|string $rowDelimiter The text that marks the point where to spill the text down rows.
|
||||
* Multiple delimiters can be passed as an array of string values
|
||||
* @param bool $ignoreEmpty Specify FALSE to create an empty cell when two delimiters are consecutive.
|
||||
* true = create empty cells
|
||||
* false = skip empty cells
|
||||
* Defaults to TRUE, which creates an empty cell
|
||||
* @param bool $matchMode Determines whether the match is case-sensitive or not.
|
||||
* true = case-sensitive
|
||||
* false = case-insensitive
|
||||
* By default, a case-sensitive match is done.
|
||||
* @param mixed $padding The value with which to pad the result.
|
||||
* The default is #N/A.
|
||||
*
|
||||
* @return array the array built from the text, split by the row and column delimiters
|
||||
*/
|
||||
public static function split($text, $columnDelimiter = null, $rowDelimiter = null, bool $ignoreEmpty = false, bool $matchMode = true, $padding = '#N/A')
|
||||
{
|
||||
$text = Functions::flattenSingleValue($text);
|
||||
|
||||
$flags = self::matchFlags($matchMode);
|
||||
|
||||
if ($rowDelimiter !== null) {
|
||||
$delimiter = self::buildDelimiter($rowDelimiter);
|
||||
$rows = ($delimiter === '()')
|
||||
? [$text]
|
||||
: preg_split("/{$delimiter}/{$flags}", $text);
|
||||
} else {
|
||||
$rows = [$text];
|
||||
}
|
||||
|
||||
/** @var array $rows */
|
||||
if ($ignoreEmpty === true) {
|
||||
$rows = array_values(array_filter(
|
||||
$rows,
|
||||
function ($row) {
|
||||
return $row !== '';
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
if ($columnDelimiter !== null) {
|
||||
$delimiter = self::buildDelimiter($columnDelimiter);
|
||||
array_walk(
|
||||
$rows,
|
||||
function (&$row) use ($delimiter, $flags, $ignoreEmpty): void {
|
||||
$row = ($delimiter === '()')
|
||||
? [$row]
|
||||
: preg_split("/{$delimiter}/{$flags}", $row);
|
||||
/** @var array $row */
|
||||
if ($ignoreEmpty === true) {
|
||||
$row = array_values(array_filter(
|
||||
$row,
|
||||
function ($value) {
|
||||
return $value !== '';
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
);
|
||||
if ($ignoreEmpty === true) {
|
||||
$rows = array_values(array_filter(
|
||||
$rows,
|
||||
function ($row) {
|
||||
return $row !== [] && $row !== [''];
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return self::applyPadding($rows, $padding);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $padding
|
||||
*/
|
||||
private static function applyPadding(array $rows, $padding): array
|
||||
{
|
||||
$columnCount = array_reduce(
|
||||
$rows,
|
||||
function (int $counter, array $row): int {
|
||||
return max($counter, count($row));
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
return array_map(
|
||||
function (array $row) use ($columnCount, $padding): array {
|
||||
return (count($row) < $columnCount)
|
||||
? array_merge($row, array_fill(0, $columnCount - count($row), $padding))
|
||||
: $row;
|
||||
},
|
||||
$rows
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|array|string $delimiter the text that marks the point before which you want to split
|
||||
* Multiple delimiters can be passed as an array of string values
|
||||
*/
|
||||
private static function buildDelimiter($delimiter): string
|
||||
{
|
||||
$valueSet = Functions::flattenArray($delimiter);
|
||||
|
||||
if (is_array($delimiter) && count($valueSet) > 1) {
|
||||
$quotedDelimiters = array_map(
|
||||
function ($delimiter) {
|
||||
return preg_quote($delimiter ?? '');
|
||||
},
|
||||
$valueSet
|
||||
);
|
||||
$delimiters = implode('|', $quotedDelimiters);
|
||||
|
||||
return '(' . $delimiters . ')';
|
||||
}
|
||||
|
||||
return '(' . preg_quote(Functions::flattenSingleValue($delimiter)) . ')';
|
||||
}
|
||||
|
||||
private static function matchFlags(bool $matchMode): string
|
||||
{
|
||||
return ($matchMode === true) ? 'miu' : 'mu';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ class Xlfn
|
|||
. '|register[.]id'
|
||||
. '|textafter'
|
||||
. '|textbefore'
|
||||
. '|textsplit'
|
||||
. '|valuetotext'
|
||||
. ')(?=\\s*[(])/i';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class TextSplitTest extends AllSetupTeardown
|
||||
{
|
||||
private function setDelimiterArgument(array $argument, string $column): string
|
||||
{
|
||||
return '{' . $column . implode(',' . $column, range(1, count($argument))) . '}';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $argument
|
||||
*/
|
||||
private function setDelimiterValues(Worksheet $worksheet, string $column, $argument): void
|
||||
{
|
||||
if (is_array($argument)) {
|
||||
foreach ($argument as $index => $value) {
|
||||
++$index;
|
||||
$worksheet->getCell("{$column}{$index}")->setValue($value);
|
||||
}
|
||||
} else {
|
||||
$worksheet->getCell("{$column}1")->setValue($argument);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerTEXTSPLIT
|
||||
*/
|
||||
public function testTextSplit(array $expectedResult, array $arguments): void
|
||||
{
|
||||
$text = $arguments[0];
|
||||
$columnDelimiter = $arguments[1];
|
||||
$rowDelimiter = $arguments[2];
|
||||
|
||||
$args = 'A1';
|
||||
$args .= (is_array($columnDelimiter)) ? ', ' . $this->setDelimiterArgument($columnDelimiter, 'B') : ', B1';
|
||||
$args .= (is_array($rowDelimiter)) ? ', ' . $this->setDelimiterArgument($rowDelimiter, 'C') : ', C1';
|
||||
$args .= (isset($arguments[3])) ? ", {$arguments[3]}" : ',';
|
||||
$args .= (isset($arguments[4])) ? ", {$arguments[4]}" : ',';
|
||||
$args .= (isset($arguments[5])) ? ", {$arguments[5]}" : ',';
|
||||
|
||||
$worksheet = $this->getSheet();
|
||||
$worksheet->getCell('A1')->setValue($text);
|
||||
$this->setDelimiterValues($worksheet, 'B', $columnDelimiter);
|
||||
$this->setDelimiterValues($worksheet, 'C', $rowDelimiter);
|
||||
$worksheet->getCell('H1')->setValue("=TEXTSPLIT({$args})");
|
||||
|
||||
$result = Calculation::getInstance($this->getSpreadsheet())->calculateCellValue($worksheet->getCell('H1'));
|
||||
self::assertSame($expectedResult, $result);
|
||||
}
|
||||
|
||||
public function providerTEXTSPLIT(): array
|
||||
{
|
||||
return require 'tests/data/Calculation/TextData/TEXTSPLIT.php';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
|
||||
return [
|
||||
[
|
||||
[['Hello', 'World']],
|
||||
[
|
||||
'Hello World',
|
||||
' ',
|
||||
'',
|
||||
],
|
||||
],
|
||||
[
|
||||
[['Hello'], ['World']],
|
||||
[
|
||||
'Hello World',
|
||||
'',
|
||||
' ',
|
||||
],
|
||||
],
|
||||
[
|
||||
[['To', 'be', 'or', 'not', 'to', 'be']],
|
||||
[
|
||||
'To be or not to be',
|
||||
' ',
|
||||
'',
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
['1', '2', '3'],
|
||||
['4', '5', '6'],
|
||||
],
|
||||
[
|
||||
'1,2,3;4,5,6',
|
||||
',',
|
||||
';',
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
['Do', ' Or do not', ' There is no try', ' ', 'Anonymous'],
|
||||
],
|
||||
[
|
||||
'Do. Or do not. There is no try. -Anonymous',
|
||||
['.', '-'],
|
||||
'',
|
||||
],
|
||||
],
|
||||
[
|
||||
[['Do'], [' Or do not'], [' There is no try'], [' '], ['Anonymous']],
|
||||
[
|
||||
'Do. Or do not. There is no try. -Anonymous',
|
||||
'',
|
||||
['.', '-'],
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
['Do', ' Or do not', ' There is no try', ' '],
|
||||
['Anonymous', ExcelError::NA(), ExcelError::NA(), ExcelError::NA()],
|
||||
],
|
||||
[
|
||||
'Do. Or do not. There is no try. -Anonymous',
|
||||
'.',
|
||||
'-',
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
['', '', '1'],
|
||||
['', '', ExcelError::NA()],
|
||||
['', '2', ''],
|
||||
['3', ExcelError::NA(), ExcelError::NA()],
|
||||
['', ExcelError::NA(), ExcelError::NA()],
|
||||
['', '4', ExcelError::NA()],
|
||||
],
|
||||
[
|
||||
'--1|-|-2-|3||-4',
|
||||
'-',
|
||||
'|',
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
['1'],
|
||||
['2'],
|
||||
['3'],
|
||||
['4'],
|
||||
],
|
||||
[
|
||||
'--1|-|-2-|3||-4',
|
||||
'-',
|
||||
'|',
|
||||
true,
|
||||
],
|
||||
],
|
||||
[
|
||||
[['', 'BCD', 'FGH', 'JKLMN', 'PQRST', 'VWXYZ']],
|
||||
[
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
['A', 'E', 'I', 'O', 'U'],
|
||||
'',
|
||||
],
|
||||
],
|
||||
];
|
||||
Loading…
Reference in New Issue