Regular expression replace preg_replace() to remove 2 letter at beginning Eexcept NW/SE etc. direction markers

Viewed 48

I am dealing with a relatively inconsistent and messy data source, and need some help with a very specific regular expression.

A lot of the strings we get are prepended with 2 letter alphanumeric (upper or lower cases) followed by a space, that we need to purge, so we can do something like the following:

$town = "PE Springfield" // truncate to "Springfield"
$town = "Kr Nashville" // truncate to "Nashville"

But in some cases the prefixes are directives such as NE, Sw etc. which we need to keep.

$town = "NW Brockvillle" // keep to "NW Brockville"
$town = "Se Nashville" // uppercase to " SE Nashville"

I could write a more complex series of if/else statements to process the direction strings separately then merge it back, but I'm hoping there is a brilliant regular expression.

The regex I have so far for NW/NE matching (all cases) or any 2 alphanumeric opening followed by space is:

$cleanpattern[] = '/^[Nn][Ww]\s/';
$cleanpattern[] = '/^[Nn][Ee]\s/';
$cleanpattern[] = '/^[Ss][Ww]\s/';
$cleanpattern[] = '/^[Ss][Ee]\s/';
$cleanpattern[] = '/^[A-Za-z]{2}\s/';

Note that by some grace, this data source never supplies just N, S, E or W (single letter). But I suppose it doesn't hurt to cover that just in case for future?

Then for replacement strings, I have the following array (whether the Sw/ne comes in lower or upper, I want them all uppercase):

$replacementpattern[] = 'NW ';
$replacementpattern[] = 'NE ';
$replacementpattern[] = 'SW ';
$replacementpattern[] = 'SE ';
$replacementpattern[] = '';

When I run the preg_replace($cleanpattern, $replacementpattern, $town, 1) the problem is that after the NW/SE etc. are checked and set, they are then purged by the 5th rule. Turns out the 4th parameter of "LIMIT" on this function doesn't limit the loop within the pattern/replace arrays, but rather only how many replacements are done within each string.

2 Answers

One way you "could" do it, not saying it's "great"...

RegEx

^(((?:NW|NE|SW|SE|N|E|S|W)\s)|[a-z]{2}\s)

Capture the valid cases and invalid ones in separate capture groups, then in the replacement only output the valid ones.

Example

Here we also use a callback to uppercase the letters as needed.

<?php
$tests = [
    "PE Springfield", // truncate to "Springfield"
    "Kr Nashville", // truncate to "Nashville"
    "NW Brockvillle", // keep to "NW Brockville"
    "Se Nashville" // uppercase to " SE Nashville"
];

foreach ($tests as $subject) {
    $result = preg_replace_callback('/^(((?:NW|NE|SW|SE|N|E|S|W)\s)|[a-z]{2}\s)/i', 
        function ($groups) {
            return isset($groups[2]) ? strtoupper($groups[2]) : '';
        }, $subject);
    echo "$subject = $result\n";
}

Output

PE Springfield = Springfield
Kr Nashville = Nashville
NW Brockvillle = NW Brockvillle
Se Nashville = SE Nashville

Detailed RegEx breakdown

Search

Options: Case insensitive;

Assert position at the beginning of the string «^» Match the regex below and capture its match into backreference number 1 «(((?:NW|NE|SW|SE|N|E|S|W)\s)|[a-z]{2}\s)»

  • Match this alternative (attempting the next alternative only if this one fails) «((?:NW|NE|SW|SE|N|E|S|W)\s)»
    • Match the regex below and capture its match into backreference number 2 «((?:NW|NE|SW|SE|N|E|S|W)\s)»
      • Match the regular expression below «(?:NW|NE|SW|SE|N|E|S|W)»
        • Match this alternative (attempting the next alternative only if this one fails) «NW»
        • Or match this alternative (attempting the next alternative only if this one fails) «NE»
        • Or match this alternative (attempting the next alternative only if this one fails) «SW»
        • Or match this alternative (attempting the next alternative only if this one fails) «SE»
        • Or match this alternative (attempting the next alternative only if this one fails) «N»
        • Or match this alternative (attempting the next alternative only if this one fails) «E»
        • Or match this alternative (attempting the next alternative only if this one fails) «S»
        • Or match this alternative (the entire group fails if this one fails to match) «W»
      • Match a single character that is a “whitespace character” (any Unicode separator, tab, line feed, carriage return, vertical tab, form feed, next line) «\s»
    • Or match this alternative (the entire group fails if this one fails to match) «[a-z]{2}\s»
      • Match a single character in the range between “a” and “z” (case insensitive) «[a-z]{2}»
        • Exactly 2 times «{2}»
      • Match a single character that is a “whitespace character” (any Unicode separator, tab, line feed, carriage return, vertical tab, form feed, next line) «\s»

Replacement

$2

Just output backreference #2

Not different than @Dean's great approach (preg_replace_callback and a capture group) but with a kind of magic in the pattern to avoid alternations. The goal: no need to test if the capture group exists in the callback since it always succeeds.

$result = preg_replace_callback(
    '~^([ns]?[ew]?\b ?)(?:^[a-z]{2} )?~mi',
    fn($m) => strtoupper($m[1]),
    $str
);

demo

Explanation:

In the capture group ([ns]?[ew]?\b ?) all is optional except the word-boundary \b. This word-boundary succeeds in two cases:

  • between a letter and the space (at least one letter is matched)
  • at the start of the string when the letters and the space aren't matched in this group.

Consequences for this capture group:

  • letters are always followed by a space
  • a space without at least one letter before isn't possible
  • even if nothing is matched, the capture group succeeds with an empty string.

When the capture group matches the empty string, the optional non-capturing group (?:^[a-z]{2} )? can eventually succeed. Note that it starts with the ^ anchor to be sure that the capture group before it is empty, and the word-boundary isn't a problem since it succeeds between the start of the string and the first letter of the non-capturing group, obviously, when the capturing group isn't empty, ^ fails and the non-capturing group too.

Related