21
\$\begingroup\$

For a homework assignemnt, I was asked to complete this CodeStepByStep problem. It's coded correctly and gave me a right answer. I'm just wondering if there's an easier way to solve this problem without the big blocky code in the if statement. The fact that I have to check each lowercase vowel AND THEN the uppercase vowel seems verbose.

Write a function named vowelCount that accepts a string and returns the number of vowels (a, e, i, o, or u) that the string contains.

For example, the call of vowelCount("kookaburra") should return 5 (two o's, 2 a's, and one u). When passed a string without any vowels (such as an empty string, "01234", or "sky"), 0 should be returned.

function vowelCount(string) {
    if(string.length == 0) { return 0; }
    let count = 0;
    for(let i = 0; i < string.length; i++) {
        let v = string[i];
        if(v == "a" || v == "e" || v == "i" || v == "o" || v == "u" ||
           v == "A" || v == "E" || v == "I" || v == "O" || v == "U") {
            count += 1;
        }
    }
    return count;
}
\$\endgroup\$
  • 5
    \$\begingroup\$ I would just like to note that while the question and answers are perfectly ok, they only work for English. Many other languages, such as French, Norwegian, etc have a number of additional vowels. While it is irrelevant for a homework, it is good to keep that in mind, because many applications have become global, and localisation can become a topic, if what you do, is supposed to work in more countries than just the US. For example, when checking allowed symbols in a person's name. \$\endgroup\$ – Gnudiff May 22 at 7:13
37
\$\begingroup\$

An alternative route is to use string.replace() and Regular Expressions to strip everything but the vowels from the string. Then count the length of the resulting string. This avoids iteration altogether.

const vowelCount = s => s.replace(/[^aeiou]/gi, '').length

console.log(vowelCount('The quick brown fox jumps over the lazy dog'))

The regex is /[^aeiou]/gi which means match everything that's NOT (^) in the set (aeiou), matching globally (g flag) and without regard to case (i flag). string.replace() then uses this pattern to replace all matching characters with a blank string. What remains are your vowels.

\$\endgroup\$
  • 15
    \$\begingroup\$ This avoids iteration altogether - well, just wanted to point out that I'm confident the replace() method does its own iterating internally. (Still, I do think your point is valid.) \$\endgroup\$ – Marc.2377 May 22 at 2:02
26
\$\begingroup\$

I'm just wondering if there's an easier way to solve this problem without the big blocky code in the if statement.

Well, you could put all of those in an array:

const vowels = ['A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o', 'u'];

And then the if condition can be simplified using Array.prototype.includes():

if( vowels.includes(v)) {

The fact that I have to check each lowercase vowel AND THEN the uppercase vowel seems verbose.

You could also include either the uppercase or lowercase letters, and then call String.prototype.toUpperCase() or String.prototype.toLowerCase(), though if performance is your goal, then the extra function call might be something to consider.


Additionally, a for...of loop could be used instead of the regular for loop, to avoid the need to index into the array.

for( const v of string) {

And the postfix increment operator (i.e. ++) could be used to increase count instead of adding 1.

The check for zero length string can be removed, since the loop won't be run.

const vowels = ['A', 'E', 'I', 'O', 'U'];
function vowelCount(string) {
    let count = 0;
    for( const v of string) {
        if( vowels.includes(v.toUpperCase())) {
            count++;
        }
    }
    return count;
}

console.log(vowelCount('kookaburra'));

console.log(vowelCount('sky'));


What follows are some advanced techniques that many wouldn't expect a beginner/intermediate-level student to utilize. If you really wanted to shorten this code, you could convert the string to an array with the spread operator and then use array reduction with Array.prototype.reduce():

const vowels = ['A', 'E', 'I', 'O', 'U'];
const charInVowels = c => vowels.includes(c.toUpperCase());
const checkChar = (count, c) => charInVowels(c) ? ++count : count; 
const vowelCount = string => [...string].reduce(checkChar, 0);

console.log(vowelCount('kookaburra'));

console.log(vowelCount('sky'));

—-

P.s.did you intentionally put an auto-comment about off-topic posts as the body of your profile??

\$\endgroup\$
  • \$\begingroup\$ Also, if( string.length == 0) { return 0; } is unnecessary. Looping through an empty string will just happen zero times and zero will be returned anyway. \$\endgroup\$ – JollyJoker May 21 at 7:32
  • 3
    \$\begingroup\$ I know that this answer is correct, but being a homework assignment, if the level of the student is such that counting the vowels in a string is of reasonable difficulty, as a teacher I would be very suspicious if someone submitted code with filter or reduce \$\endgroup\$ – ChatterOne May 21 at 11:05
  • 1
    \$\begingroup\$ @JollyJoker - yeah I did consider suggesting the OP remove that; I have gone ahead and added that. \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ May 21 at 15:16
  • \$\begingroup\$ @ChatterOne Would you not also be very suspicious if someone submitted code with code suggested in one of the other answers? I have added a preface to that last paragraph. \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ May 21 at 15:17
  • \$\begingroup\$ @SᴀᴍOnᴇᴌᴀ Honestly yes, but this goes down a somewhat "depends on the person" kind of path. Maybe the teacher accepts it even knowing that it's copy-pasted from internet, if the students can clearly show they understand how it works. \$\endgroup\$ – ChatterOne May 22 at 6:55
7
\$\begingroup\$

Sᴀᴍ Onᴇᴌᴀ answer has the right idea for small strings, but can be improved by using a Set to hold the vowels rather than an array. This reduces the overhead of Array.includes which will iterate each character in the vowels array for non matching characters

You can create a set as const vowels = new Set([..."AEIOUaeiou"]);

To encapsulate the constant vowels use a function to scope vowels outside the global scope and closure to make it available to the function.

const countVowels = (() => {
    const VOWELS = new Set([..."AEIOUaeiou"]);
    return function(str) {
         var count = 0;
         for (const c of str) { count += VOWELS.has(c) }
         return count;
    }
})();

Or

const countVowels = (() => {
    const VOWELS = new Set([..."AEIOUaeiou"]);
    return str => [...str].reduce((count, c) => count += VOWELS.has(c), 0);
})();

UNICODE vowels

Of course the first snippet is the better as it is \$O(1)\$ storage and as it uses a Set (hash table) it is \$O(n)\$ complexity (where \$n\$ is the length of the string) rather than \$O(n*m)\$ (where \$m\$ is the number of vowels). This becomes more important if you are to include the full set of unicode vowels

const countVowels = (() => {
    // Reference https://en.wikipedia.org/wiki/Phonetic_symbols_in_Unicode
    const VOWELS = new Set([..."AEIOUaeiouiyɨʉiyɯuɪʏeøɘɵɤoɛœɜɞʌɔaɶɑɒʊəɐæɪ̈ʊ̈IYƗɄIYƜUꞮʏEØɘƟɤOƐŒꞫɞɅƆAɶⱭⱰƱƏⱯÆꞮ̈Ʊ̈"]);
    return function(str) {
         var count = 0;
         for (const c of str) { count += VOWELS.has(c) }
         return count;
    }
})();

NOTE the above snippet does not work.. see below.

Don't split unicode

If you are using Unicode it is important to release that each unicode 16 bit character does not always represent a single visual character

For example the last two vowels "ɪ̈ʊ̈" require two characters to display. eg the string "\u026A\u0308\u028A\u0308" === "ɪ̈ʊ̈" is true. You can not just count them as in the expression "ɪ̈ʊ̈".split("").length will evaluate to 4.

This is even more problematic as \u026A and \u028A the first character codes are also vowels "ɪʊ"

To solve for the full set of vowels and keep the complexity at \$O(n)\$ and storage at \$O(1)\$ we can use 3 sets

const countVowels = (() => {
    const VOWELS = new Set([ ..."AEIOUaeiouiyɨʉiyɯuʏeøɘɵɤoɛœɜɞʌɔaɶɑɒəɐæIYƗɄIYƜUʏEØɘƟɤOƐŒꞫɞɅƆAɶⱭⱰƏⱯÆ"]);
    const VOWELS_DOUBLE = new Set(["ɪ̈", "ʊ̈", "Ɪ̈", "Ʊ̈"]);
    const VOWELS_SINGLE = new Set([..."ʊɪƱꞮ"]);
    return function(str) {
         var count = 0, prev;
         for (const c of str) { 
              count += VOWELS.has(c);
              count += VOWELS_DOUBLE.has(prev + c) || VOWELS_SINGLE.has(prev);
              prev = c;
         }
         return count + VOWELS_SINGLE.has(prev);
    }
})();
\$\endgroup\$
  • 2
    \$\begingroup\$ I was about to write a new answer, but it just a small modification to yours. Instead of reduce, I'd use .filter(c => VOWELS.has(c)).length \$\endgroup\$ – JollyJoker May 21 at 7:27
  • \$\begingroup\$ @JollyJoker My first example is the best as it is O(1) storage while using any of the array methods requires that the string be converted to an array that means storage is O(n), on top of that using Array.filter to count means worst case would double the memory use \$\endgroup\$ – Blindman67 May 21 at 9:28
2
\$\begingroup\$

Maybe there is a risk that the very same authority which is asking you to count vowels will soon ask you to count consonants, just to have you see how flexible your code is.

So it could be a good idea to start with a function that takes two arguments:

  1. the string under test and
  2. the set of accepted vowels.

Like function countCharsFromVowelSet() in the below code snippet.

Note that deciding what exactly is an acceptable vowel is a language and country dependent decision.

const countCharsFromVowelSet = function(str, vowelSet) {
     let arr   = [...str];
     let count = (arr.filter(c => vowelSet.includes(c))).length;
     return count;
};

/* auxiliary function builder function:  */
const makeCharCounter = function(charSet) {
    return (str => countCharsFromVowelSet(str, charSet));
};

const EnglishVowelList   = "AEIOUaeiou";
const GermanVowelList    = "AEIOUYÄÖÜaeiouyäöü";

const countEnglishVowels = makeCharCounter(EnglishVowelList);
const countGermanVowels  = makeCharCounter(GermanVowelList);

text1  = "William Shakespeare";
count1 = countEnglishVowels(text1);
text2  = "Die Schöpfung";
count2 = countGermanVowels(text2);

console.log("There are " + count1.toString() + " vowels in: " + text1);
console.log("There are " + count2.toString() + " vowels in: " + text2);
\$\endgroup\$
  • \$\begingroup\$ Welcome to Code review and thanks for supplying an answer. Why use var for the variables inside the functions, as opposed to let and const? Array.from(str) could be optimized with [...str]. And the function returned by makeCharCounter could be simplified to str => countCharsFromVowelSet(str, charSet); \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ May 23 at 17:37
  • 1
    \$\begingroup\$ @ Sᴀᴍ Onᴇᴌᴀ: yes you're right, thanks for pointing this out. I am more familiar with Python than with Javascript. My point was mostly about architecture and flexibility ; give a thought to possible specification changes. The vowel set for a given language is a piece of data to be imported from that part of the source code in charge of dealing with internationalization (i18n). You don't want to have to write the multiple OR construct for vowel-rich languages such as Russian and French. \$\endgroup\$ – jpmarinier May 24 at 13:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.