173

Anyone know of a very fast way to replace the last occurrence of a string with another string in a string?

Note, the last occurrence of the string might not be the last characters in the string.

Example:

$search = 'The';
$replace = 'A';
$subject = 'The Quick Brown Fox Jumps Over The Lazy Dog';

Expected Output:

The Quick Brown Fox Jumps Over A Lazy Dog
mickmackusa
  • 43,625
  • 12
  • 83
  • 136
Kirk Ouimet
  • 27,280
  • 43
  • 127
  • 177
  • You might find [`s($str)->replaceLast($search, $replace)`](https://github.com/delight-im/PHP-Str/blob/8fd0c608d5496d43adaa899642c1cce047e076dc/src/Str.php#L305) helpful, as found in [this standalone library](https://github.com/delight-im/PHP-Str). – caw Jul 27 '16 at 00:08

15 Answers15

262

You can use this function:

function str_lreplace($search, $replace, $subject)
{
    $pos = strrpos($subject, $search);

    if($pos !== false)
    {
        $subject = substr_replace($subject, $replace, $pos, strlen($search));
    }

    return $subject;
}
Mike Mackintosh
  • 13,917
  • 6
  • 60
  • 87
Mischa
  • 42,876
  • 8
  • 99
  • 111
  • This was still returning true no matter what. Consider modifying it to be: if($pos) { $subject = substr_replace($subject, $replace, $pos, strlen($search)); return $subject; } else { return false; } – Jazzy Jul 11 '13 at 16:35
  • 5
    @Jason It doesn't return `TRUE` no matter what. It returns a string no matter what. If a replacement can't be made it returns the original `$subject`, just like [`substr_replace `](http://php.net/manual/en/function.substr-replace.php) and [`str_replace`](http://php.net/manual/en/function.str-replace.php) do. – Mischa Jul 12 '13 at 00:58
  • @Mischa Isn't that the same thing in this case? I was attempting to do something like !str_lreplace, but if it doesn't return false, it's considered true, right? Either way, this helped me out and I appreciate it. Thanks. – Jazzy Jul 13 '13 at 23:26
  • @Jason, glad it helped. Yes, a string is *truthy*, but it's not the same as `TRUE`. Anyway, I get your point. In this answer I tried to follow what PHP does in similar functions, but you are of course free to use the implementation suggested by yourself if you want to use `!str_lreplace`. – Mischa Jul 14 '13 at 03:36
  • 2
    How can this work? `strpos — Find the position of the first occurrence of a substring in a string` - edit: wow. Php geniuses really made a function called `strpos` and `strrpos` ? Thanks.... – BarryBones41 Oct 05 '15 at 21:22
  • 1
    @Barry this is one case where PHP does not deserve the blame :-) The names are patterned on the decades-old `strstr`, [`strrstr`](https://linux.die.net/man/3/strrstr) of the standard C library, which are the same functions. (But did they _have_ to change the name?) – alexis Dec 05 '18 at 20:55
  • In case we just want to replace the last character, we don't need strrpos since we already know the position. We can just do $position = strlen($subject) - 1; and if ($subject[$position] == $search) before using substr_replace. – baptx May 22 '21 at 17:34
  • I know this is an old answer, but this won't work with arrays (like `str_replace` does), as strrpos can only accept strings as arguments. – jg2703 Nov 21 '22 at 14:42
32

Another 1-liner but without preg:

$subject = 'bourbon, scotch, beer';
$search = ',';
$replace = ', and';

echo strrev(implode(strrev($replace), explode(strrev($search), strrev($subject), 2))); //output: bourbon, scotch, and beer
ricka
  • 1,107
  • 1
  • 11
  • 13
27
$string = 'this is my world, not my world';
$find = 'world';
$replace = 'farm';
$result = preg_replace(strrev("/$find/"),strrev($replace),strrev($string),1);
echo strrev($result); //output: this is my world, not my farm
zack
  • 315
  • 3
  • 2
  • Why it works with all strings reversed? Is there some (I assume) specific performance gain when using regular-expressions? – Kamafeather Apr 03 '19 at 11:06
  • No it actually decreases performance but it's because you want the last occurrence only so you limit the search to one and reverse everything if you wanted the first you wouldn't have to reverse anything – Tofandel Oct 10 '19 at 22:45
20

The following rather compact solution uses the PCRE positive lookahead assertion to match the last occurrence of the substring of interest, that is, an occurrence of the substring which is not followed by any other occurrences of the same substring. Thus the example replaces the last 'fox' with 'dog'.

$string = 'The quick brown fox, fox, fox jumps over the lazy fox!!!';
echo preg_replace('/(fox(?!.*fox))/', 'dog', $string);

OUTPUT: 

The quick brown fox, fox, fox jumps over the lazy dog!!!
frzsombor
  • 2,274
  • 1
  • 22
  • 40
John Sonderson
  • 3,238
  • 6
  • 31
  • 45
  • I don't understand why this later answer has received more upvotes than [Alix Axel's answer](https://stackoverflow.com/a/3837298/2943403). This pattern take 3 times as many "steps" compared to Alix's pattern. The negative lookahead is an unnecessary expense because it doesn't improve accuracy at all -- versus greedy dot matching. – mickmackusa Nov 18 '20 at 21:00
14

You could do this:

$str = 'Hello world';
$str = rtrim($str, 'world') . 'John';

Result is 'Hello John';

yivi
  • 42,438
  • 18
  • 116
  • 138
Nicolas Finelli
  • 2,170
  • 1
  • 14
  • 9
  • 5
    This works as long as there isn't any repeated characters. In my situation I'm striping the page number off the archive date so I have "2015-12/2" and it takes all / and all 2 off the end becoming "2015-1". – Mike Jun 03 '16 at 21:00
  • 1
    This will only work if the last occurrence searched is the last word and has no additional chars after it. – AwesomeGuy Feb 14 '19 at 11:55
  • 1
    This doesn't work because [`rtrim`](https://secure.php.net/manual/en/function.rtrim.php) doesn't behave the way you're thinking. It will strip from the end any characters that exist in the search string in whatever order (and always append the replacement), e.g. "Hello word" -> "Hello John", "Hello lord" -> "Hello John", "Hello motor" -> "Hello motJohn", "Hello worldy" -> "Hello worldyJohn". – Jake Mar 05 '19 at 18:15
  • 4
    I don't recommend that anyone use this (unexplained) code which is prone to quietly failing. This answer makes no attempt to "find" the last occurrence of a string -- it just keeps consuming characters from the right side of the input string while the character mask is satisfied. This does the same thing: `rtrim($str, 'dlorw')` – mickmackusa Nov 17 '20 at 12:58
10

This is an ancient question, but why is everyone overlooking the simplest regexp-based solution? Normal regexp quantifiers are greedy, people! If you want to find the last instance of a pattern, just stick .* in front of it. Here's how:

$text = "The quick brown fox, fox, fox, fox, jumps over etc.";
$fixed = preg_replace("((.*)fox)", "$1DUCK", $text);
print($fixed);

This will replace the last instance of "fox" to "DUCK", like it's supposed to, and print:

The quick brown fox, fox, fox, DUCK, jumps over etc.
alexis
  • 48,685
  • 16
  • 101
  • 161
  • 1
    Thanks! The perfect function to wrap around my expression to accomplish this. In my case I'm replacing the last comma with ", and ". Glad I scrolled down a bit in this answer list. – rlhane May 29 '20 at 15:35
  • The "simplest regexp-based solution" was "overlooked" by you -- this technique was [posted 8 years earlier on this page by Alix Axel](https://stackoverflow.com/a/3837298/2943403). His/Her answer is identical except his/her snippet is dynamic. Your answer has hardcoded the needle into the search pattern and the replacement string into the replace parameter. I have gone one step further to simplify the replacement by using the `\K` character [here](https://stackoverflow.com/a/64876047/2943403) – mickmackusa Nov 18 '20 at 20:49
  • @mickmackusa, you're right, good catch. Indeed Alix Axel's answer uses greedy search on the left... but let's just say I don't see it as particularly "simple" :-D. (Also there is no reason to use ~ as the delimiter... just makes things more confusing.) – alexis Nov 20 '20 at 14:48
  • I would argue that your parenthetical delimiters are more confusing _to me_ -- and I am very comfortable with reading regex. I often use tildes as delimiters, even if there are no forward slashes in the pattern -- it is my personal preference. Seeing parentheses in a pattern, my mind assumes a capture group or lookaround. Alix's answer IS less simple mostly because it is dynamic. My answer tightens up the syntax slightly. – mickmackusa Nov 20 '20 at 21:24
  • the parens _are_ a capture group. – alexis Nov 26 '20 at 09:40
7

This will also work:

function str_lreplace($search, $replace, $subject)
{
    return preg_replace('~(.*)' . preg_quote($search, '~') . '(.*?)~', '$1' . $replace . '$2', $subject, 1);
}

UPDATE Slightly more concise version (http://ideone.com/B8i4o):

function str_lreplace($search, $replace, $subject)
{
    return preg_replace('~(.*)' . preg_quote($search, '~') . '~', '$1' . $replace, $subject, 1);
}
Alix Axel
  • 151,645
  • 95
  • 393
  • 500
3

You can use strrpos() to find last match.

$string = "picture_0007_value";
$findChar =strrpos($string,"_");

$string[$findChar]=".";

echo $string;

Output : picture_0007.value

Faruk UNAL
  • 114
  • 5
  • 2
    This is only correct when replacing a single-byte character string which has a length of one AND the search character exists -- in all other cases, this fails. – mickmackusa Nov 17 '20 at 13:30
3
$string = "picture_0007_value";
$findChar =strrpos($string,"_");
if($findChar !== FALSE) {
  $string[$findChar]=".";
}

echo $string;

Apart from the mistakes in the code, Faruk Unal has the best anwser. One function does the trick.

xwero
  • 31
  • 2
  • You need to check, if **$findChar** is not false (same way as in the accepted answer). If the string does not contain searched string, you get notice and the first character will be replaced. – shaggy Mar 04 '17 at 13:45
  • This is great, but as it stands it can only replace 1 character with 1 character. – Pete Oct 11 '18 at 20:58
  • The above comments are correct. This is only correct when replacing a single-byte character string which has a length of one. – mickmackusa Nov 17 '20 at 13:34
2

Shorthand for accepted answer

function str_lreplace($search, $replace, $subject){ 
    return is_numeric($pos=strrpos($subject,$search))?
    substr_replace($subject,$replace,$pos,strlen($search)):$subject;
}
Abbas
  • 552
  • 5
  • 8
  • This is also less readable than the accepted answer. Why not use a better coding standard? Please read the PSR coding standards. – mickmackusa Nov 17 '20 at 13:21
1

A short version:

$NewString = substr_replace($String,$Replacement,strrpos($String,$Replace),strlen($Replace));
Maciek Semik
  • 1,872
  • 23
  • 43
  • This unexplained "short version" (less readable version) should not be used because if the needle (`$Replace`) is not found, then the string (`$String`) is unintentionally damaged/mutated. This is a mistake that earlier answers did not make. This answer should be removed so that researchers do not copy this inappropriate snippet. https://3v4l.org/easUY – mickmackusa Nov 18 '20 at 20:40
0

While using regex is typically less performant than non-regex techniques, I do appreciate the control and flexibility that it affords.

In my snippet, I will set the pattern to be case-insensitive (\i, although my sample input will not challenge this rule) and include word boundaries (\b, although they were not explicitly called for).

I am also going to use the \K metacharacter to reset the fullstring match so that no capture groups / backreferences are needed.

Code: (Demo)

$search = 'The';
$replace = 'A';
$subject = "The Quick Brown Fox Jumps Over The Lazy Dog's Thermos!";

echo preg_replace(
         '/.*\K\b' . preg_quote($search, '/') . '\b/i',
         $replace,
         $subject
     );

Output:

  The Quick Brown Fox Jumps Over A Lazy Dog's Thermos!
# ^^^                            ^            ^^^
# not replaced                replaced        not replaced

Without word boundaries: (Demo)

echo preg_replace(
         '/.*\K' . preg_quote($search, '/') . '/i',
         $replace,
         $subject
     );

Output:

  The Quick Brown Fox Jumps Over The Lazy Dog's Armos!
# ^^^                            ^^^            ^
# not replaced              not replaced        replaced
mickmackusa
  • 43,625
  • 12
  • 83
  • 136
  • Don't confuse strings with words. Words are space-delimited strings, strings can be parts of words. The right code should convert "Thermos" to "Aermos" and leave "The Lazy" untouched. – Frank Nov 17 '20 at 14:58
  • Yes. I am deliberately using word boundaries although not asked for. There was no [mcve] from the OP so we won't know if case sensitivity and word boundaries are necessary. I am being generous and offering several new techniques to the page which researchers may want. I can extend my answer and add another snippet without boundaries if you like. – mickmackusa Nov 17 '20 at 20:43
0

Below corrected zack's code (https://stackoverflow.com/a/11144999/9996503). Be careful with regular expressions! Consider this:

$string = 'Round brackets (parentheses) "()", square brackets "()"';
$find = '()';
$replace = '[]';
// Zack's code:
$result = preg_replace(strrev("/$find/"),strrev($replace),strrev($string),1);
var_dump($result); // Output: NULL
// Correct code:
$result = strrev(preg_replace('/'.preg_quote(strrev($find)).'/', strrev($replace), strrev($string), 1));
echo $result; //Output: Round brackets (parentheses) "()", square brackets "[]"
msegit
  • 110
  • 8
-1

Use the "$" on a reg expression to match the end of the string

$string = 'The quick brown fox jumps over the lazy fox';
echo preg_replace('/fox$/', 'dog', $string);

//output
'The quick brown fox jumps over the lazy dog'
Neil Holcomb
  • 518
  • 4
  • 11
-1

For the interested: I've written a function that utilises preg_match so that you're able to replace from right hand side using regex.

function preg_rreplace($search, $replace, $subject) {
    preg_match_all($search, $subject, $matches, PREG_SET_ORDER);
    $lastMatch = end($matches);

    if ($lastMatch && false !== $pos = strrpos($subject, $lastMatchedStr = $lastMatch[0])) {
        $subject = substr_replace($subject, $replace, $pos, strlen($lastMatchedStr));
    }

    return $subject;
}

Or as a shorthand combination/implementation of both options:

function str_rreplace($search, $replace, $subject) {
    return (false !== $pos = strrpos($subject, $search)) ?
        substr_replace($subject, $replace, $pos, strlen($search)) : $subject;
}
function preg_rreplace($search, $replace, $subject) {
    preg_match_all($search, $subject, $matches, PREG_SET_ORDER);
    return ($lastMatch = end($matches)) ? str_rreplace($lastMatch[0], $replace, $subject) : $subject;
}

based on https://stackoverflow.com/a/3835653/3017716 and https://stackoverflow.com/a/23343396/3017716

Ken
  • 2,859
  • 4
  • 24
  • 26