Python String Slices in PHP

written in php

This past weekend, I decided I wanted to bring one of my favourite shortcuts in Python to PHP strings: its slice notation. Limiting myself to userland code, there wasn’t much I could do to replicate the syntax. Implementing the ArrayAccess interface would allow me to overload array dimensions in a class, though I’d be stuck parsing string offsets for the necessary arguments.

My first attempt, writing only from how I remembered using the slice notation, got me half way there. It handled start and stop well in the case of positive steps, but failed to handle a variety of negative steps. It was a simple mistake, as I wasn’t handling all possible adjustments to the slice boundaries. After looking at CPython’s implementation, I was able to spot where I went wrong and hopefully achieve an implementation that mimics Python’s own behaviour. Here’s a small preview of its use:

1
2
3
4
5
6
7
8
use SliceableStringy\SliceableStringy as S;

$sliceable = S::create('Fòô Bàř', 'UTF-8');

$sliceable[':'];      // 'Fòô Bàř'
$sliceable['::-1'];   // 'řàB ôòF'
$sliceable['-3:6'];   // 'Bà'
$sliceable['-3::-2']; // 'BôF'

How did I test it? Aside from the unit tests, I decided to write a quick fixture generator so that I could compare a large number of input, and their output, between CPython and my PHP code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
str = 'spec'
count = 0;

args = [None] + range(-8, 8)
stepArgs = [None] + range(-8, 0) + range(1, 8)

print 'Args,Result'

for i in args:
    for j in args:
        for k in stepArgs:
            start = '' if i == None else i
            stop = '' if j == None else j
            step = '' if k == None else k
            print "[%s:%s:%s],%s" % (start, stop, step, str[i:j:k])

The code above generates a CSV with rows having the format “[Args],Result”, e.g. “[:-6:-3],cs”. I chose the range of arguments to test based on how boundaries are adjusted given the lengths of the string. Afterward, it was just a matter of writing a functional test that would compare the results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
it('achieves identical output to python slices', function() {
    // Get expected results from fixtures
    $expectedResults = file(__DIR__ . '/fixtures/expectedResults.csv');

    $str = S::create('spec');
    $count = 0;

    $args = array_merge([null], range(-8, 7));
    $stepArgs = array_merge([null], range(-8, -1), range(1, 7));

    // Build array of results
    foreach ($args as $i) {
        foreach ($args as $j) {
            foreach ($stepArgs as $k) {
                // Compare against python results
                $count++;
                $result = "[$i:$j:$k]," . $str["$i:$j:$k"] . "\n";
                expect($result)->toEqual($expectedResults[$count]);
            }
        }
    }
});

I’m fortunate that despite the size of the test space (> 4600 fixture entries), it’s only testing string operations, and so the whole suite runs in ~0.06s on my MacBook Air.

For anyone interested, I’ve uploaded the library to GitHub. It’s a single class, SliceableStringy, that inherits from Stringy. It can be found at: https://github.com/danielstjules/SliceableStringy


Comments