28

Reversing a String in JavaScript (utf-32 compatible)

 4 years ago
source link: http://www.zsoltnagy.eu/exercise-reversing-a-string-in-javascript/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

In this article, you will find different ways to reverse a string. Attempt to solve this exercise alone, and run the code in the below terminal.

Even if your solution is successful, I suggest reading all the solutions.

Exercise

Exercise: Write a reverse function that reverses its string argument.

 
const text = 'ES6 in Practice';
 
const reverse = str => {
    // write your solution here
}
 
console.log(reverse(text));
 

The console.log expression should write the ecitcarP ni 6SE message to the console.

Your solution: reversing a string

Write your solution here.

const text = 'ES6 in Practice';

const reverse = str => {

// write your solution here

}

console.log(reverse(text));

Problem

JavaScript strings don't have a reverse method. Only arrays can be reversed like this:

 
const arr = [1, 2, 3];
 
arr.reverse();
// [3, 2, 1]
 

If you try to reverse a text, it won't work:

 
const text = 'ES6 in Practice';
 
text.reverse();
Uncaught TypeError: text.reverse is not a function
    at <anonymous>:1:6
 

Planning the solution

When you attempt to pass a coding intervies, expect the exercise to be underspecified . This means that you have to ask questions to clarify any ambiguities.

For instance, you might correctly solve the string reversal exercise, just to find out that a test case calls your solution with null , {} , 52 etc.

If your solution returns "[object Object]" , you have to explain why you think this is the intended answer.

However, if your solution throws an error like the one below, you may be in trouble.

 
VM342:4 Uncaught TypeError: str.split is not a function
    at reverse (<anonymous>:4:16)
    at <anonymous>:1:1
 

Your approach may change depending on the answer:

null

Regardless of the approach, make sure you clarify how your program will react to errors before solving the exercise.

We will choose the last approach: errors to be thrown when the input is incorrect. This allows us to focus just on the reversal.

Reversing a string using the array reverse method

The first solution relies on the reverse method of arrays:

 
const reverse = str => {
    // Step 1: convert str to an array of characters
    // Step 2: reverse the array
    // Step 3: join the characters to a string
}
 

It is always important create a plan before writing code. I am illustrating this plan on this easy exercise. If you develop this habit, you will be able to solve harder interview exercises in the same way.

Let's see the solution step by step:

 
const text = 'ES6 in Practice';
 
const arr = text.split('');
// (15) ["E", "S", "6", " ", "i", "n", " ", "P", "r", "a", "c", "t", "i", "c", "e"]
 
arr.reverse()
// reverses arr in place. Result:
// ["e", "c", "i", "t", "c", "a", "r", "P", " ", "n", "i", " ", "6", "S", "E"]
 
arr.join('')
// "ecitcarP ni 6SE"
 

By using chaining , you can write these three instructions in just one statement:

 
text
  .split('')
  .reverse()
  .join('')
// "ecitcarP ni 6SE" 
 

Remark: in general, when chaining calls after each other, writing each method call in a new line results in more readable code. Exceptions apply. For instance, the below code is fairly readable:

 
text.split('').reverse().join('')
 

Let's create the function reversing a string:

 
const reverse = str => {
    return str.split('').reverse().join('');
}
 

If the body of a function can be summed up in a simple reverse statement, it makes sense to drop the braces and the return statements and just stick to describing the input => returnValue correspondance with the arrow function:

 
const reverse = str => str.split('').reverse().join('');
 

Simplifying the reversal with the Spread operator

We can write this function in an even more compact way if we use the spread operator to enumerate the characters of the input character by character:

 
// str.split('') is the same as [...str]
 
const reverse = str => [...str].reverse().join('');
 

Reversing an array with reduce

In the unlikely case you didn't find the reverse method of arrays, but you know how to use the reduce higher order function, you can enumerate the characters of the string and build the return value from right to left:

 
const reverse = str =>
    [...str].reduce( 
        (accumulator, ch) => ch + accumulator 
    );
 

Of course if you are more comfortable with the ES5 syntax, during the interview, you can simply write old style functions:

 
const addCharacterToFront = function(text, ch) {
    return ch + text;
}
 
const reverse = function(str) {
    return [...str].reduce(addCharacterToFront);
}
 

I highly recommend learning the ES6 syntax though, as it is a basic expectation in 2020.

Reversing an array iteratively and with recursion

If everything else fails, you can go back to the basic building blocks of JavaScript: sequence, selection, iteration, and functions. You can write loops and function calls to get the solution.

 
const reverse = str => {
    let result = '';
    for ( let ch of str ) {
        result = ch + result;
    }
    return result;
}
 

The alternative is to think recursively.

Reversing a string of length 1 or less is obvious: we just return the string.

Reversing a string of larger length works as follows:

  1. Take the head and the tail of the string. The head is the first character, the tail consists of the remaining characters.
  2. Reverse the tail.
  3. Add the head to the end of the reversed string.
 
const reverse = str => {
    if (str.length <= 1) {
        return str;
    }
 
    const head = str[0];
    const tail = str.slice(1);
 
    return reverse(tail) + head;
}
 

Obviously, the reversal can be written in just one statement:

 
const reverse = str => {
    return str.length <= 1 ? str :
        reverse(str.slice(1)) + str[0];
}
 

However, for readability sake, it makes more sense sticking to the readable solution.

Using the fact that strings are iterables, you can also use pattern matching (destructuring) to write a neat solution:

 
const reverse = ([head, ...tail]) =>
    tail.length === 0 ? head : reverse(tail) + head;
 

In this solution, you have to explain when your values are of type string and when they are arrays. This is a complicated solution. The price we pay for this solution is that it results in an incorrect answer when called with the empty string:

 
const reverse = ([head, ...tail]) =>
    tail.length === 0 ? head : reverse(tail) + head;
 
console.log( 'result: ', reverse('') );
// result:  undefined
 

Make sure you don't try to show off by appearing too smart, because it can easily backfire.

In this specifi example, you can change the exit condition to make the code work:

 
const reverse = ([head, ...tail]) =>
    typeof head === 'undefined' ? '' : reverse(tail) + head;
 

Don't forget that recursive function calls are resource intensive, because they occupy space on the stack. By using recursion, even a constant space complexity solution becomes linear, if we have to iterate on the whole input due to the function call stack.

Therefore, it is advisable to turn recursive solutions into iterative ones.

What about long unicode characters?

Reversing a string sounds too easy, right? Well, now comes the test. Try to reverse the following string:

 
var sonGoku = '孫 悟空:sweat_smile::sweat_smile:';
 
reverse(sonGoku);
// ???
 

For a list of emojis, check out emojipedia.org .

Let's test all our solutions in separate blocks. I added blocks in this example to separate the namespaces so that we can define our reverse function over and over again:

 
{ 
    const reverse = ([head, ...tail]) =>
        typeof head === 'undefined' ? '' : reverse(tail) + head;
    reverse('孫 悟空:sweat_smile::sweat_smile:');
}
// ":sweat_smile::sweat_smile:空悟 孫"
 
{ 
    const reverse = str => str.split('').reverse().join('');
    reverse('孫 悟空:sweat_smile::sweat_smile:');
}
// "�:sweat_smile:�空悟 孫"
 
{ 
    const reverse = str => [...str].reverse().join('');
    reverse('孫 悟空:sweat_smile::sweat_smile:');
}
// ":sweat_smile::sweat_smile:空悟 孫"
 
{ 
    const reverse = str =>
        [...str].reduce( 
            (accumulator, ch) => ch + accumulator 
        );
    reverse('孫 悟空:sweat_smile::sweat_smile:');
}
// ":sweat_smile::sweat_smile:空悟 孫"
 
{ 
    const reverse = str => {
        let result = '';
        for ( let ch of str ) {
            result = ch + result;
        }
        return result;
    }
    reverse('孫 悟空:sweat_smile::sweat_smile:');
}
// ":sweat_smile::sweat_smile:空悟 孫"
 
{ 
    const reverse = str => {
        if (str.length <= 1) {
            return str;
        }
 
        const head = str[0];
        const tail = str.slice(1);
 
        return reverse(tail) + head;
    }
    reverse('孫 悟空:sweat_smile::sweat_smile:');
}
// "�:sweat_smile:�空悟 孫"
 
{ 
    const reverse = ([head, ...tail]) =>
        typeof head === 'undefined' ? '' : reverse(tail) + head;
    reverse('孫 悟空:sweat_smile::sweat_smile:');
}
// ":sweat_smile::sweat_smile:空悟 孫"
 

What is the problem with long unicode characters?

As written in ES6 in Practice, long unicode characters need special care. The rule of thumb is that you should use ES6 features to treat strings. ES5 features are often unsafe, becasue they don't take into consideration that characters may have different lengths.

Safe language constructs:

[...str]
for ( let ch of str ) {...}

Unsafe language constructs:

  • iteration based on str.length : ':sweat_smile:'.length is 2 , while '孫'.length is 1
  • str.slice(startIndex)
  • str.substr(startIndex)
  • for ( let index in str ) {...}
  • many others

Summary

Reversing a string seems very easy at first glance. However, there are many traps and pitfalls that you have to consider. Make sure you use safe ES6 code so that you can reverse any string, including emojis.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK