transformRow($row); } // ------------------------------------------------------------------------- // map // ------------------------------------------------------------------------- public function testMapPassthrough(): void { $result = $this->applyOne( ['sourceColumn' => 'Name', 'outputColumn' => 'Name', 'type' => 'map'], ['Name' => 'Alice'] ); $this->assertSame('Alice', $result['Name']); } // ------------------------------------------------------------------------- // replace // ------------------------------------------------------------------------- public function testReplace(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'replace', 'search' => 'foo', 'replace' => 'bar', ], ['Col' => 'foo baz foo']); $this->assertSame('bar baz bar', $result['Col']); } public function testReplaceEmptySearchReturnsOriginal(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'replace', 'search' => '', 'replace' => 'bar', ], ['Col' => 'hello']); $this->assertSame('hello', $result['Col']); } // ------------------------------------------------------------------------- // dateformat // ------------------------------------------------------------------------- public function testDateFormat(): void { $result = $this->applyOne([ 'sourceColumn' => 'Date', 'outputColumn' => 'Date', 'type' => 'dateformat', 'fromFormat' => 'd.m.Y', 'toFormat' => 'Y-m-d', ], ['Date' => '15.03.2024']); $this->assertSame('2024-03-15', $result['Date']); } public function testDateFormatInvalidValueReturnsOriginal(): void { $result = $this->applyOne([ 'sourceColumn' => 'Date', 'outputColumn' => 'Date', 'type' => 'dateformat', 'fromFormat' => 'd.m.Y', 'toFormat' => 'Y-m-d', ], ['Date' => 'not-a-date']); $this->assertSame('not-a-date', $result['Date']); } public function testDateFormatEmptyValueReturnsEmpty(): void { $result = $this->applyOne([ 'sourceColumn' => 'Date', 'outputColumn' => 'Date', 'type' => 'dateformat', 'fromFormat' => 'd.m.Y', 'toFormat' => 'Y-m-d', ], ['Date' => '']); $this->assertSame('', $result['Date']); } // ------------------------------------------------------------------------- // split // ------------------------------------------------------------------------- public function testSplitPart0(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'split', 'delimiter' => ';', 'part' => 0, ], ['Col' => 'Coop Pronto;7007 Chur']); $this->assertSame('Coop Pronto', $result['Col']); } public function testSplitPart1(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'split', 'delimiter' => ';', 'part' => 1, ], ['Col' => 'Coop Pronto;7007 Chur']); $this->assertSame('7007 Chur', $result['Col']); } public function testSplitPartOutOfBoundsReturnsOriginal(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'split', 'delimiter' => ';', 'part' => 5, ], ['Col' => 'A;B']); $this->assertSame('A;B', $result['Col']); } // ------------------------------------------------------------------------- // regexextract // ------------------------------------------------------------------------- public function testRegexExtract(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Zip', 'type' => 'regexextract', 'pattern' => '(\d{4})', ], ['Col' => 'Shop 7007 Chur', 'Zip' => '']); $this->assertSame('7007', $result['Zip']); } public function testRegexExtractNoMatchReturnsEmpty(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Zip', 'type' => 'regexextract', 'pattern' => '(\d{4})', ], ['Col' => 'No digits here', 'Zip' => '']); $this->assertSame('', $result['Zip']); } public function testRegexExtractEmptyValueReturnsEmpty(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Zip', 'type' => 'regexextract', 'pattern' => '(\d{4})', ], ['Col' => '', 'Zip' => '']); $this->assertSame('', $result['Zip']); } // ------------------------------------------------------------------------- // trim // ------------------------------------------------------------------------- public function testTrim(): void { $result = $this->applyOne( ['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'trim'], ['Col' => ' hello world '] ); $this->assertSame('hello world', $result['Col']); } // ------------------------------------------------------------------------- // uppercase // ------------------------------------------------------------------------- public function testUppercase(): void { $result = $this->applyOne( ['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'uppercase'], ['Col' => 'Hello World'] ); $this->assertSame('HELLO WORLD', $result['Col']); } public function testUppercaseUnicode(): void { $result = $this->applyOne( ['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'uppercase'], ['Col' => 'zürich'] ); $this->assertSame('ZÜRICH', $result['Col']); } // ------------------------------------------------------------------------- // lowercase // ------------------------------------------------------------------------- public function testLowercase(): void { $result = $this->applyOne( ['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'lowercase'], ['Col' => 'Hello World'] ); $this->assertSame('hello world', $result['Col']); } // ------------------------------------------------------------------------- // ucwordsfirst // ------------------------------------------------------------------------- public function testUcwordsFirst(): void { $result = $this->applyOne( ['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'ucwordsfirst'], ['Col' => 'COOP PRONTO CHUR'] ); $this->assertSame('Coop Pronto Chur', $result['Col']); } public function testUcwordsFirstHyphen(): void { $result = $this->applyOne( ['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'ucwordsfirst'], ['Col' => 'SAINT-JEAN-DE-MAURIENNE'] ); $this->assertSame('Saint-Jean-De-Maurienne', $result['Col']); } public function testUcwordsFirstApostrophe(): void { $result = $this->applyOne( ['sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'ucwordsfirst'], ['Col' => "O'NEILL STORE"] ); $this->assertSame("O'Neill Store", $result['Col']); } // ------------------------------------------------------------------------- // truncate // ------------------------------------------------------------------------- public function testTruncate(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'truncate', 'maxLength' => 5, ], ['Col' => 'Hello World']); $this->assertSame('Hello', $result['Col']); } public function testTruncateShorterThanMaxIsUnchanged(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'truncate', 'maxLength' => 100, ], ['Col' => 'Short']); $this->assertSame('Short', $result['Col']); } public function testTruncateUnicode(): void { // 'ü' counts as 1 Unicode character, so maxLength=3 gives 3 chars: Z, ü, r $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'truncate', 'maxLength' => 3, ], ['Col' => 'Zürich']); $this->assertSame('Zür', $result['Col']); } // ------------------------------------------------------------------------- // constantvalue // ------------------------------------------------------------------------- public function testConstantValue(): void { $transformer = new ColumnTransformer([[ 'sourceColumn' => '_constant_', 'outputColumn' => 'Currency', 'type' => 'constantvalue', 'metadataKey' => 'currency_code', ]], ['currency_code' => 'CHF']); $result = $transformer->transformRow(['Currency' => '']); $this->assertSame('CHF', $result['Currency']); } public function testConstantValueMissingKeyReturnsEmpty(): void { $transformer = new ColumnTransformer([[ 'sourceColumn' => '_constant_', 'outputColumn' => 'Currency', 'type' => 'constantvalue', 'metadataKey' => 'nonexistent', ]], []); $result = $transformer->transformRow(['Currency' => '']); $this->assertSame('', $result['Currency']); } // ------------------------------------------------------------------------- // pipeline // ------------------------------------------------------------------------- public function testPipeline(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'pipeline', 'steps' => [ ['type' => 'trim'], ['type' => 'lowercase'], ['type' => 'ucwordsfirst'], ], ], ['Col' => ' COOP PRONTO ']); $this->assertSame('Coop Pronto', $result['Col']); } public function testPipelineEmptyStepsReturnsOriginal(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'pipeline', 'steps' => [], ], ['Col' => 'hello']); $this->assertSame('hello', $result['Col']); } // ------------------------------------------------------------------------- // Inline transformations[] array (flat pipeline per column entry) // ------------------------------------------------------------------------- public function testInlineTransformationsArray(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'map', 'transformations' => [ ['type' => 'trim'], ['type' => 'uppercase'], ], ], ['Col' => ' hello ']); $this->assertSame('HELLO', $result['Col']); } // ------------------------------------------------------------------------- // normalizeTransformType: snake_case and kebab-case aliases // ------------------------------------------------------------------------- public function testNormalizeTypeSnakeCase(): void { $result = $this->applyOne([ 'sourceColumn' => 'Date', 'outputColumn' => 'Date', 'type' => 'date_format', 'fromFormat' => 'd.m.Y', 'toFormat' => 'Y-m-d', ], ['Date' => '15.03.2024']); $this->assertSame('2024-03-15', $result['Date']); } public function testNormalizeTypeKebabCase(): void { $result = $this->applyOne([ 'sourceColumn' => 'Col', 'outputColumn' => 'Col', 'type' => 'ucwords-first', ], ['Col' => 'HELLO WORLD']); $this->assertSame('Hello World', $result['Col']); } // ------------------------------------------------------------------------- // outputAction // ------------------------------------------------------------------------- public function testOutputActionOverwrite(): void { $result = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map', 'outputAction' => 'overwrite', ], ['A' => 'new', 'B' => 'old']); $this->assertSame('new', $result['B']); } public function testOutputActionCreate(): void { $result = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'NewCol', 'type' => 'map', 'outputAction' => 'create', ], ['A' => 'hello']); $this->assertSame('hello', $result['NewCol']); } public function testOutputActionAppend(): void { $result = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map', 'outputAction' => 'append', ], ['A' => ' World', 'B' => 'Hello']); $this->assertSame('Hello World', $result['B']); } public function testOutputActionAppendWithDelimiter(): void { $result = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map', 'outputAction' => 'append', 'appendDelimiter' => ', ', ], ['A' => 'World', 'B' => 'Hello']); $this->assertSame('Hello, World', $result['B']); } public function testOutputActionAppendWithDelimiterSkippedWhenTargetEmpty(): void { $result = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map', 'outputAction' => 'append', 'appendDelimiter' => ', ', ], ['A' => 'Hello', 'B' => '']); $this->assertSame('Hello', $result['B']); } public function testOutputActionAppendLine(): void { $result = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map', 'outputAction' => 'append-line', ], ['A' => 'Line2', 'B' => 'Line1']); $this->assertSame("Line1\nLine2", $result['B']); } public function testOutputActionAppendLineNoLeadingNewlineWhenEmpty(): void { $result = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map', 'outputAction' => 'append-line', ], ['A' => 'Line1', 'B' => '']); $this->assertSame('Line1', $result['B']); } public function testOutputActionOverwriteIfEmpty(): void { $resultEmpty = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map', 'outputAction' => 'overwrite-if-empty', ], ['A' => 'new', 'B' => '']); $this->assertSame('new', $resultEmpty['B']); $resultFilled = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map', 'outputAction' => 'overwrite-if-empty', ], ['A' => 'new', 'B' => 'existing']); $this->assertSame('existing', $resultFilled['B']); } public function testOutputActionOverwriteIfNotEmpty(): void { $resultNotEmpty = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map', 'outputAction' => 'overwrite-if-not-empty', ], ['A' => 'new', 'B' => 'old']); $this->assertSame('new', $resultNotEmpty['B']); $resultEmpty = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map', 'outputAction' => 'overwrite-if-not-empty', ], ['A' => '', 'B' => 'old']); $this->assertSame('old', $resultEmpty['B']); } // ------------------------------------------------------------------------- // multi-output split // ------------------------------------------------------------------------- public function testMultiOutputSplit(): void { $transformer = new ColumnTransformer([[ 'outputs' => ['FirstName', 'LastName'], 'sourceColumn' => 'FullName', 'type' => 'split', 'delimiter' => ' ', ]]); $result = $transformer->transformRow(['FullName' => 'John Doe']); $this->assertSame('John', $result['FirstName']); $this->assertSame('Doe', $result['LastName']); } public function testMultiOutputSplitFewerPartsYieldsEmptyString(): void { $transformer = new ColumnTransformer([[ 'outputs' => ['Col1', 'Col2', 'Col3'], 'sourceColumn' => 'Source', 'type' => 'split', 'delimiter' => ';', ]]); $result = $transformer->transformRow(['Source' => 'A;B']); $this->assertSame('A', $result['Col1']); $this->assertSame('B', $result['Col2']); $this->assertSame('', $result['Col3']); } // ------------------------------------------------------------------------- // Error cases // ------------------------------------------------------------------------- public function testMissingOutputColumnThrows(): void { $this->expectException(\RuntimeException::class); $transformer = new ColumnTransformer([ ['sourceColumn' => 'A', 'type' => 'map'], ]); $transformer->transformRow(['A' => 'x']); } public function testMultiOutputNonSplitTypeThrows(): void { $this->expectException(\RuntimeException::class); $transformer = new ColumnTransformer([[ 'outputs' => ['Col1', 'Col2'], 'sourceColumn' => 'Source', 'type' => 'uppercase', ]]); $transformer->transformRow(['Source' => 'hello']); } // ------------------------------------------------------------------------- // getOutputColumns // ------------------------------------------------------------------------- public function testGetOutputColumnsCountsUniqueColumns(): void { $transformer = new ColumnTransformer([ ['sourceColumn' => 'A', 'outputColumn' => 'X', 'type' => 'map'], ['sourceColumn' => 'B', 'outputColumn' => 'Y', 'type' => 'map'], ['sourceColumn' => 'C', 'outputColumn' => 'X', 'type' => 'map'], // duplicate output ]); $transformer->transformRow(['A' => '1', 'B' => '2', 'C' => '3']); $this->assertSame(2, $transformer->getOutputColumns()); } // ------------------------------------------------------------------------- // timeperiod // ------------------------------------------------------------------------- /** @var array> */ private array $testPeriods = [ ['from' => '04:00:00', 'to' => '08:59:59', 'label' => 'Morgen'], ['from' => '09:00:00', 'to' => '10:59:59', 'label' => 'Vormittag'], ['from' => '11:00:00', 'to' => '13:59:59', 'label' => 'Mittag'], ['from' => '14:00:00', 'to' => '17:59:59', 'label' => 'Nachmittag'], ['from' => '18:00:00', 'to' => '21:59:59', 'label' => 'Abend'], ['from' => '22:00:00', 'to' => '03:59:59', 'label' => 'Nacht'], ]; public function testTimePeriodBasicMapping(): void { $result = $this->applyOne([ 'sourceColumn' => 'Time', 'outputColumn' => 'Period', 'type' => 'timeperiod', 'timeFormat' => 'H:i:s', 'periods' => $this->testPeriods, 'default' => '', ], ['Time' => '09:30:00', 'Period' => '']); $this->assertSame('Vormittag', $result['Period']); } public function testTimePeriodMidnightSpanning(): void { $result1 = $this->applyOne([ 'sourceColumn' => 'Time', 'outputColumn' => 'Period', 'type' => 'timeperiod', 'timeFormat' => 'H:i:s', 'periods' => $this->testPeriods, 'default' => '', ], ['Time' => '23:00:00', 'Period' => '']); $this->assertSame('Nacht', $result1['Period']); $result2 = $this->applyOne([ 'sourceColumn' => 'Time', 'outputColumn' => 'Period', 'type' => 'timeperiod', 'timeFormat' => 'H:i:s', 'periods' => $this->testPeriods, 'default' => '', ], ['Time' => '02:00:00', 'Period' => '']); $this->assertSame('Nacht', $result2['Period']); } public function testTimePeriodNoMatch(): void { // 03:45 falls outside all labelled ranges except Nacht (00:00-03:59) $result = $this->applyOne([ 'sourceColumn' => 'Time', 'outputColumn' => 'Period', 'type' => 'timeperiod', 'timeFormat' => 'H:i:s', 'periods' => [ ['from' => '09:00:00', 'to' => '17:59:59', 'label' => 'Day'], ], 'default' => 'Unknown', ], ['Time' => '03:45:00', 'Period' => '']); $this->assertSame('Unknown', $result['Period']); } public function testTimePeriodInvalidInput(): void { $result = $this->applyOne([ 'sourceColumn' => 'Time', 'outputColumn' => 'Period', 'type' => 'timeperiod', 'timeFormat' => 'H:i:s', 'periods' => $this->testPeriods, 'default' => 'N/A', ], ['Time' => '', 'Period' => '']); $this->assertSame('N/A', $result['Period']); } // ------------------------------------------------------------------------- // ucwordsfirst guard // ------------------------------------------------------------------------- public function testUcwordsFirstSkipsLowercase(): void { // Input already contains lowercase letters → must be returned unchanged $result = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'A', 'type' => 'ucwordsfirst', ], ['A' => 'Coop pronto chur']); $this->assertSame('Coop pronto chur', $result['A']); } public function testUcwordsFirstAppliesAllCaps(): void { // Fully uppercase input → capitalise first letter of each word $result = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'A', 'type' => 'ucwordsfirst', ], ['A' => 'COOP PRONTO']); $this->assertSame('Coop Pronto', $result['A']); } // ------------------------------------------------------------------------- // append-if-not-empty // ------------------------------------------------------------------------- public function testAppendIfNotEmptySkipsEmpty(): void { // Result is empty → target column must remain unchanged $result = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map', 'outputAction' => 'append-if-not-empty', 'appendDelimiter' => ' ', ], ['A' => '', 'B' => 'existing']); $this->assertSame('existing', $result['B']); } public function testAppendIfNotEmptyAppendsNonEmpty(): void { // Non-empty result → appended with delimiter $result = $this->applyOne([ 'sourceColumn' => 'A', 'outputColumn' => 'B', 'type' => 'map', 'outputAction' => 'append-if-not-empty', 'appendDelimiter' => ' ', ], ['A' => 'new', 'B' => 'existing']); $this->assertSame('existing new', $result['B']); } }