Skip to content

Commit d43acb3

Browse files
author
Paul
committed
Added Replace strategy and accompanying tests and updated readme.
1 parent d111f7a commit d43acb3

File tree

5 files changed

+188
-4
lines changed

5 files changed

+188
-4
lines changed

README.md

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Contents
3737
1. [IfExists](#ifexists)
3838
1. [Join](#join)
3939
1. [Merge](#merge)
40+
1. [Replace](#replace)
4041
1. [TakeFirst](#takefirst)
4142
1. [ToList](#tolist)
4243
1. [TryCatch](#trycatch)
@@ -301,6 +302,7 @@ The following strategies ship with Mapper and provide a suite of commonly used f
301302
- [IfExists](#ifexists) – Delegates to one expression or another depending on whether the specified condition maps to null.
302303
- [Join](#join) – Joins sub-string expressions together with a glue string.
303304
- [Merge](#merge) – Merges two data sets together giving precedence to the latter if keys collide.
305+
- [Replace](#replace) – Replaces one or more substrings.
304306
- [TakeFirst](#takefirst) – Takes the first value from a collection one or more times.
305307
- [ToList](#tolist) – Converts data to a single-element list unless it is already a list.
306308
- [TryCatch](#trycatch) – Tries the primary strategy and falls back to an expression if an exception is thrown.
@@ -313,9 +315,9 @@ The following strategies ship with Mapper and provide a suite of commonly used f
313315

314316
### Copy
315317

316-
Copies a portion of input data, or specified data, according to a lookup path. Supports traversing nested arrays.
318+
Copies a portion of input data, or specified data, according to a lookup path. Supports traversing nested arrays. By default the current record is used as the data source but if the *data* parameter is specified it is used instead.
317319

318-
`Copy` is probably the most common strategy whether used by itself or injected into other strategies. Since both its *path* and *data* parameters can be mapped expressions it is highly versatile and can be combined with other strategies, or even itself, to produce powerful expressions.
320+
`Copy` is probably the most common strategy whether used by itself or injected into other strategies. Since both its *path* and *data* parameters can be mapped expressions it is highly versatile and can be combined with other strategies, or even itself, to produce powerful transformations.
319321

320322
#### Signature
321323

@@ -348,7 +350,7 @@ $data = [
348350

349351
> 123
350352
351-
#### Specified data example
353+
#### Data override example
352354

353355
When data is specified in the second parameter it is used instead of the data sent from `Mapper`.
354356

@@ -361,7 +363,7 @@ When data is specified in the second parameter it is used instead of the data se
361363

362364
> 'baz'
363365
364-
#### Path resolver example
366+
#### Recursive path resolver example
365367

366368
Since the path can be derived from other strategies we can nest `Copy` instances to look up values referenced by other keys.
367369

@@ -729,6 +731,39 @@ Merge(Strategy|Mapping|array|mixed $first, Strategy|Mapping|array|mixed $second)
729731

730732
> [1, 2, 3, 3, 4, 5]
731733
734+
### Replace
735+
736+
Replaces all occurrences one or more substrings.
737+
738+
Any number of searches and replacements can be specified. Searches and replacements are parsed in pairs. If no replacements are specified, all matches are removed instead of replaced. If fewer replacements than searches are specified, the last replacement will be used for the remaining searches. If more replacements than searches are specified, the extra replacements are ignored.
739+
740+
Searches can be specified as either string literals or wrapped in an `Expression` and treated as a regular expression. `Expression` and string searches can be mixed as desired. Regular expression replacements can reference sub-matches, e.g. `$1`.
741+
742+
#### Signature
743+
744+
```php
745+
Replace(Strategy|Mapping|array|mixed $expression, string|Expression|array $searches, string|string[]|null $replacements)
746+
```
747+
748+
1. `$expression` – Expression to search in.
749+
2. `$searches` – Search string(s).
750+
3. `$replacements` – Optional. Replacement string(s).
751+
752+
#### Example
753+
754+
```php
755+
(new Mapper)->map(
756+
['Hello World'],
757+
new Replace(
758+
new Copy(0),
759+
['Hello', new Expression('[\h*world$]i')],
760+
['こんにちは', '世界']
761+
)
762+
)
763+
```
764+
765+
> 'こんにちは世界'
766+
732767
### TakeFirst
733768

734769
Takes the first value from a collection one or more times according to the specified depth. If the depth exceeds the number of nesting levels of the collection the last item encountered will be returned.

src/Expression.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
namespace ScriptFUSION\Mapper;
3+
4+
/**
5+
* Represents an expression value.
6+
*/
7+
final class Expression
8+
{
9+
private $expression;
10+
11+
public function __construct($expression)
12+
{
13+
$this->expression = "$expression";
14+
}
15+
16+
public function __toString()
17+
{
18+
return $this->expression;
19+
}
20+
}

src/Strategy/Replace.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
namespace ScriptFUSION\Mapper\Strategy;
3+
4+
use ScriptFUSION\Mapper\Expression;
5+
use ScriptFUSION\Mapper\Mapping;
6+
7+
/**
8+
* Replaces one or more substrings.
9+
*/
10+
class Replace extends Delegate
11+
{
12+
private $searches;
13+
14+
private $replacements;
15+
16+
/**
17+
* Initializes this instance with the specified expression to search in, search strings and replacement strings.
18+
*
19+
* Any number of searches and replacements can be specified. Searches and replacements are parsed in pairs. If no
20+
* replacements are specified, all matches are removed instead of replaced. If fewer replacements than searches are
21+
* specified, the last replacement will be used for the remaining searches. If more replacements than searches are
22+
* specified, the extra replacements are ignored.
23+
*
24+
* @param Strategy|Mapping|array|mixed $expression Expression to search in.
25+
* @param $searches string|Expression|array Search string(s).
26+
* @param $replacements string|string[]|null Optional. Replacement string(s).
27+
*/
28+
public function __construct($expression, $searches, $replacements = null)
29+
{
30+
parent::__construct($expression);
31+
32+
$this->searches = is_object($searches) ? [$searches] : (array)$searches;
33+
$this->replacements = (array)$replacements;
34+
}
35+
36+
public function __invoke($data, $context = null)
37+
{
38+
$output = parent::__invoke($data, $context);
39+
$replacements = $this->replacements;
40+
$replace = null;
41+
42+
foreach ($this->searches as $search) {
43+
$replace = count($replacements) ? array_shift($replacements) : $replace;
44+
45+
if ($search instanceof Expression) {
46+
$output = preg_replace($search, $replace, $output);
47+
} else {
48+
$output = str_replace($search, $replace, $output);
49+
}
50+
}
51+
52+
return $output;
53+
}
54+
}

test/Functional/DocumentationTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
namespace ScriptFUSIONTest\Functional;
33

44
use ScriptFUSION\Mapper\DataType;
5+
use ScriptFUSION\Mapper\Expression;
56
use ScriptFUSION\Mapper\Mapper;
67
use ScriptFUSION\Mapper\Strategy\Callback;
78
use ScriptFUSION\Mapper\Strategy\Collection;
@@ -16,6 +17,7 @@
1617
use ScriptFUSION\Mapper\Strategy\IfExists;
1718
use ScriptFUSION\Mapper\Strategy\Join;
1819
use ScriptFUSION\Mapper\Strategy\Merge;
20+
use ScriptFUSION\Mapper\Strategy\Replace;
1921
use ScriptFUSION\Mapper\Strategy\TakeFirst;
2022
use ScriptFUSION\Mapper\Strategy\ToList;
2123
use ScriptFUSION\Mapper\Strategy\TryCatch;
@@ -295,6 +297,21 @@ public function testMerge()
295297
);
296298
}
297299

300+
public function testReplace()
301+
{
302+
self::assertSame(
303+
'こんにちは世界',
304+
(new Mapper)->map(
305+
['Hello World'],
306+
new Replace(
307+
new Copy(0),
308+
['Hello', new Expression('[\h*world$]i')],
309+
['こんにちは', '世界']
310+
)
311+
)
312+
);
313+
}
314+
298315
public function testTakeFirst()
299316
{
300317
self::assertSame(
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
namespace ScriptFUSIONTest\Unit\Mapper\Strategy;
3+
4+
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
5+
use ScriptFUSION\Mapper\Expression;
6+
use ScriptFUSION\Mapper\Strategy\Replace;
7+
use ScriptFUSIONTest\MockFactory;
8+
9+
final class ReplaceTest extends \PHPUnit_Framework_TestCase
10+
{
11+
use MockeryPHPUnitIntegration;
12+
13+
/**
14+
* @dataProvider provideReplacements
15+
*/
16+
public function testReplace($input, $search, $replace, $output)
17+
{
18+
$replace = (new Replace($input, $search, $replace))->setMapper(MockFactory::mockMapper($input));
19+
20+
self::assertSame($output, $replace([]));
21+
}
22+
23+
public function provideReplacements()
24+
{
25+
return [
26+
'Single removal' => ['foo', 'o', null, 'f'],
27+
'Substring removal' => ['foo', 'oo', null, 'f'],
28+
29+
'Single replacement' => ['foo', 'f', 'b', 'boo'],
30+
'Substring replacement' => ['foo', 'foo', 'bar', 'bar'],
31+
32+
'Multiple removal' => ['foo', ['f', 'o'], null, ''],
33+
'Multiple replacement' => ['foo', ['f', 'o'], ['h', 'a'], 'haa'],
34+
35+
'Insufficient replacements (uniform types)' => ['foo', ['f', 'o'], ['x'], 'xxx'],
36+
'Insufficient replacements (mixed types)' => ['foo', ['f', 'o'], 'x', 'xxx'],
37+
'Extra replacements (uniform types)' => ['foo', ['f'], ['b', 'w'], 'boo'],
38+
'Extra replacements (mixed types)' => ['foo', 'f', ['b', 'w'], 'boo'],
39+
40+
'Recursive replacement' => ['foo', ['f', 'b'], ['b', 'w'], 'woo'],
41+
42+
'Regex single removal' => ['foo', new Expression('[o]'), null, 'f'],
43+
'Regex substring removal' => ['foo', new Expression('[oo]'), null, 'f'],
44+
45+
'Regex single replacement' => ['foo', new Expression('[o$]'), 'x', 'fox'],
46+
'Regex substring replacement' => ['foo', new Expression('[^foo$]'), 'bar', 'bar'],
47+
48+
'Regex multiple removal' => ['foo', [new Expression('[f]'), new Expression('[o]')], null, ''],
49+
'Regex multiple replacement' => ['foo', [new Expression('[f]'), new Expression('[o]')], ['h', 'a'], 'haa'],
50+
51+
'Regex insufficient replacements' => ['foo', [new Expression('[f]'), new Expression('[o]')], ['x'], 'xxx'],
52+
'Regex extra replacements' => ['foo', new Expression('[f]'), ['b', 'w'], 'boo'],
53+
54+
'Mixed mode replacement' => ['foo', ['f', new Expression('[o$]')], ['c', 'x'], 'cox'],
55+
'Sub-match replacement' => ['foo', new Expression('[(f)oo]'), 'o$1$1', 'off'],
56+
];
57+
}
58+
}

0 commit comments

Comments
 (0)