Search and Replace - Better Advanced Solution

The current ‘Advanced’ solution:

  • doesn’t address the constraints of the question
  • doesn’t use ‘advanced’ features of JS
  • doesn’t provide a more concise solution
  • introduces potential bugs/edge cases (ex before/after are different lengths).

In short, it answers a question that isn’t being asked. The ‘good’ parts of this answer are the application of regular expressions via RegExp.test() and string.replace(). Why not use a solution that puts more emphasis on those.

Here’s a solution that leverages the full capabilities of Regex to provide a very concise answer.

function myReplace(str, before, after) {
  var isTitleCase = /^[A-Z]/.test(before);
  after = (isTitleCase)
    ? after[0].toUpperCase() + after.slice(1)
    : after[0].toLowerCase() + after.slice(1);
  return str.replace(before, after);
}
  1. The ^ selector allows the regex to operate from the start of the string, and absent expansion operators (ie *, +) the capital alpha selector [A-Z] only applies to the first char. Many of the proposed solutions extract the first char (ie before[0]), instead of using the built-in capabilities of RegEx.

  2. The ternary operator is a bit ‘advanced’ but provides a very useful shortcut to apply quick transforms depending on a bool state.

  3. This version keeps string mutation (ie string recreation) to a minimum. The ternary operator creates 3 (ie 1st char of after, the rest of after, the concatenated form of after) strings, replace creates 1.

  4. It takes a ridiculously tiny amount of code using RegEx vs the non-RegEx alternatives.

Here’ s another, more advanced but slightly less concise, solution.

function myReplace(str, before, after) {
  var isTitleCase = /^[A-Z]/.test(before);
  after = (isTitleCase)
    ? after.replace(/^[A-Za-z]/, after.match(/^[A-Za-z]/)[0].toUpperCase())
    : after.replace(/^[A-Za-z]/, after.match(/^[A-Za-z]/)[0].toLowerCase());
  return str.replace(before, after);
}

Works the same as the 1st solution except it uses RegEx to rebuilt the ‘after’ string.

  1. RegEx.match() is used to extract the first letter

  2. RegEx.replace() us used to swap the first letter

  3. The upper/lower matching demonstrates how to capture in a case-insensitive way.

  4. The number of mutations is the same.

Note: We could just apply .toLowerCase() on ‘after’ if it isn’t title-case but these answers assume the question is looking to maintain the title-case only.

Anyway, it looked like there was some room for improvement. Learning RegEx can be extremely difficult without good examples. I thought this might be a good place to present one.

3 Likes

Even better. I like it.

I think it’s quite hard to say what is a basic solution and what is advanced.

Here is a ‘non-RegEx’ version with less code.

function myReplace(str, before, after) {
    if (before[0].toUpperCase() == before[0]) after = after[0].toUpperCase() + after.substr(1)
    return str.replace(before, after);
}

:smile:

2 Likes

You’re missing an edge case. If the before is lower case and after us titlecase, after should be changed to lowercase.

Also, String.replace() is the same as RegEx.replace() so you’re still using regex.

I agree (passed the tests, though :wink:) - you could do

after = (before[0].toUpperCase() == before[0]) ? after[0].toUpperCase() + after.substr(1) : after[0].toLowerCase() + after.substr(1)

or

after = (before[0].toUpperCase() == before[0] ? after[0].toUpperCase() : after[0].toLowerCase()) + after.substr(1)

which would get the edge case.

Can you explain this more? Replace is a method on string String.prototype.replace() - JavaScript | MDN and takes either a regex or a string.

Anyway, all three of the ‘hint’ solutions (basic/intermediate/advanced) use string.replace. What does a “non-regex” solution look like?

A reduce solution, with nested ternary operator and template literals just to make everything impossible to read:

function myReplace(str, before, after) {
  // split and reduce the array
  return str.split(' ').reduce((acc, curr) => {
    return curr === before ? // is the word we are looking for?
    ( curr[0] === curr[0].toUpperCase() // is the original uppercase?
      ? `${acc} ${after.charAt(0).toUpperCase()}${after.slice(1)}` 
       : `${acc} ${after}`
     ) 
    : `${acc} ${curr}`; // if not return the word.
  }, '').trim() // for leading space
}
2 Likes

Nice :+1: but a bit too readable. I ‘optimised’ it further for you. :smile:

myReplace=(s,b,r)=>s.split(' ').reduce((a,c)=>c===b?(c[0]===c[0].toUpperCase()?${a} ${r.charAt(0).toUpperCase()}${r.slice(1)}:${a} ${r}):${a} ${c},'').trim()

1 Like

@r1chard5mith @Marmiz Instead of trolling, why don’t you make an attempt to provide useful feedback?

If you enjoy writing incomprehensible code, go join a code golf competition.

I did provide you useful feedback - I showed you a shorter way to solve the problem without using RegEx (which I find easier to read than the examples you originally gave)

Then you told me it was using Regex anyway and I asked you to explain. I also asked “What does a “non-regex” solution look like?”. Do you have a better solution?

Be civil, please. No one is trolling here.

Nowhere did I mention in the initial proposal that my intent is to produce unreadable/incomprehensible code. I thought it was pretty clear that the intent was to:

  1. Provide a concise solution
  2. Use advanced (ie less well-known features of js) to do so

@r1chard5mith Your first suggestion missed an important edge case. If the before is lowercase, and the after is uppercase, the after should be modified to also be lowercase.

Demonstrating Regex usage is one of the main points of this proposal. Providing a non-regex solution doesn’t fit into the scope of the question.

I ‘get’ that Regex isn’t everybody’s cup of tea and that’s OK. Personally, I avoided using it as much as possible for years before I finally made an honest effort to understand it. The point is, for devs working on these problems who are interested in learning Regex, it’s useful to see working examples.

In that same thread, ternary statements are another less-well-known feature of JS that can be useful in situations where you require a simple either/or check. Once again, not everybody’s cup of tea but when used intentionally, can cut down code size.

For instance, it was a very common pattern to use ternary statements to define parameter defaults prior to the feature being included in the new ES6 standard.

For example:

function someFunction(param) {
  var someVar = (param) ? param : 'some default value';
}

… can now be done in ES6 with parameter defaults:

function someFunction(someParam='some default value') {

}

@Marmiz The point isn’t whether or not it uses regex. I was just pointing out that String.replace uses regex.

Also, did you read the initial proposal? The intent is not to make the solution impossible to read. Ternary statements aren’t ‘impossible to read’ when used conservatively. Providing a solution that is more difficult to read – while creative – is not constructive to the conversation.

@r1chard5mith This proposal isn’t specifically targeted to ‘everyone’. It’s marked ‘Advanced’ specifically because it uses some of the more complex and less-well-understood features of JS to produce concise and efficient code.

@PortableStick I posted this seeking constructive feedback/suggestions. Maybe, I have a more liberal interpretation of the term ‘trolling’ than most. I consider, intentionally providing non-constructive feedback, as a form of trolling.

Code golf competitions exist specifically as a outlet for devs who are bored and/or looking for a challenge that involves creativity solving problems with overly-complex and hard-to-read solutions.

I proposed an alternative solution that may be a good learning utility for others. If this is the wrong place for that type of feedback. Feel free to flag the question so a Moderator can delete it.

Learn to relax, man. All input is constructive if you know how to handle it.

String.replace itself isn’t a Regexp method - if a regex is used as the search expression then it will evaluate the expression using the builtin Regexp module (using that to build a search string), and it in turn allows the replace expression to use regex subsyrings. But if a string is passed it just uses that directly. replace just does string substitution, its not to do with regex.

Edit @evanplaice see line 135. String.replace is not the same as Regexp.replace at all, it uses a different algorithm (as defined in the ECMAScript standard) and different code so that it can actively avoid delegating to Regexp modules if at all possible [and ideally drop into fast C++ native string replace methods? That would be engine specific though]:

  1. Coerce string to object
  2. If the search string is not type string + a bunch of other conditions, then call out to Regexp search/replace methods.
  3. Else the search string is type string, so just replace it with whatever is specified as the replacement.
  4. Return the new string.

At stage 2, the function does its checks to see if it should delegate out to Regexp (the JS Regexp modules and the C++/Rust code that powers it), but the algorithm is set up to defensively avoid using the [slow] methods in the Regexp modules.

2 Likes

Stupidly, I hadn’t realised that the ‘hints’ page is actually a comment thread itself and lots of other people have posted solutions there already. There are a couple of solutions there that doesn’t use replace. It’s very interesting to look at the different ways it can be done. My favourite is this one, freeCodeCamp Algorithm Challenge Guide: Search and Replace which is splitting on the word to be matched and then reconstructing it as the input to join. Very clever.

@DanCouper TIL. Thanks, I didn’t know the JS source was accessible online.

So, you’re saying that the JS version of RegEx is used as a sort of polyfill when the native C implementation of str::replace is missing?

I’ve never heard of Rust being used for browser dev, did you add that for completeness or are there actually browsers written in Rust?

@r1chard5mith I would have posted this in Hints, but replies have been locked there.

If you like that example, it works almost almost identically to how built the output string in the first example.

Remove the split/join parts to simplify things a bit

(before[0] == before[0].toUpperCase()) 
    ? after.charAt(0).toUpperCase() + after.slice(1)
    : after;

Then inline the regex test so the ternary test is the same

  (/^[A-Z]/.test(before))
    ? after[0].toUpperCase() + after.slice(1)
    : after[0].toLowerCase() + after.slice(1);

They share a lot in common, my version uses a different result when the ternary is false because it also converts the after string to lowercase if the before string is lowercase.

The first character test works the same:

// this, is the same
before[0] == before[0].toUpperCase()

// as this
/^[A-Z]/

// the carat specifies that the search should start from the beginning of the string
^ 
// the next selector checks just the first character to see if it's within the range of uppercase letters
[A-Z] 

// forward slashes are used to delimit a RegEx string the way quotes are used to delimit a text string
/some regex expression/

// RegExp.prototype.test() takes a string as input, checks it against the regex string, and returns a boolean result
var booleanResult = /some regex/.test(input);

// so the following returns true only if the first character of 'before' is uppercase
/^[A-Z]/.test(before)

There’s actually an edge case this doesn’t cover that wasn’t defined in the question. If the first letter of before is not a letter (ie symbol, number) this will still convert after to lowercase. If I wanted this solution to be more rebust it should do 2 comparisons:

  1. if the first char of before is uppercase, after should be titlecase
  2. if the first char of before is lowercase, after should be lowercase
  3. if neither of those true, either throw an error or return after unchanged.

So this would be a better solution:

function myReplace(str, before, after) {
  after = (() => {
    if (/^[A-Z]/.test(before))
      return after[0].toUpperCase() + after.slice(1);
    if (/^[a-z]/.test(before))
      return after[0].toLowerCase() + after.slice(1);
    return after;
  })();
  return str.replace(before, after);
}

Note: the => (fat arrow) syntax is just a quick way to define a unnamed function and if statements work without {} (brackes) if the body only takes up one line. May look weird but the arrow function body is just an if/if else/else evaluation.

Re rust: yes, parts of firefox.

My mistake, I was still in the process of editing the last part.

The sad thing is that javascript has limited regex support (in this case for modifiers), so you can’t do the whole replace with a single regex call.

Here’s a one liner (because I like one liners)

function myReplace(str, before, after) {
  return str.replace(
    new RegExp(before, 'ig'),
    match => after.replace(
      /^./,
      first => /^[A-Z]/.test(match) ? first.toUpperCase() :
               /^[a-z]/.test(match) ? first.toLowerCase() : first
    )
  );
}

This solution is probably 1 or 2 years old but it still holds its weight; maybe you can only change the way it checks for casing with RegExp instead of charCode but hey, whatever.

function myReplace(str, before, after) {
  return str.replace(new RegExp(before,"ig"), (toReplace) =>
    toReplace.charCodeAt(0) < 97
      ? after[0].toUpperCase() + after.slice(1)
      : after
  )
}