Problem
Challenge:
Check if a string (first argument,
str
) ends with the given target
string (second argument,target
).This challenge can be solved with the
.endsWith()
method. But for the
purpose of this challenge, do not use it.
I would like to especially have my solution critiqued.
function confirmEnding(str, target){
let lengthOfString = str.length;
let lengthOfTarget = target.length;
let subtractLengths = lengthOfString - lengthOfTarget;
let lastIndexOfString = str.lastIndexOf(target);
if(str.includes(target) === true){
if(lastIndexOfString === subtractLengths){
return true;
}
else{
return false;
}
}
else{
return false;
}
}
Test Cases:
confirmEnding("Bastian", "n") should return true.
Passed
confirmEnding("Congratulation", "on") should return true.
Passed
confirmEnding("Connor", "n") should return false.
Passed
confirmEnding("Walking on water and developing software from a specification are easy if both are frozen", "specification") should return false.
Passed
confirmEnding("He has to give me a new name", "name") should return true.
Passed
confirmEnding("Open sesame", "same") should return true.
Passed
confirmEnding("Open sesame", "pen") should return false.
Passed
confirmEnding("Open sesame", "game") should return false.
Passed
confirmEnding("If you want to save our world, you must hurry. We dont know how much longer we can withstand the nothing", "mountain") should return false.
Passed
confirmEnding("Abstraction", "action") should return true.
Solution
Your solution is far too complicated. It’s also inefficient, because it uses functions .lastIndexOf()
and .includes()
, both of which analyze the entire str
looking for target
, whereas an optimal solution should look only starting at a known position at the end of str
.
Here are two simple solutions:
function confirmEnding(str, target) {
return str.startsWith(target, str.length - target.length);
}
function confirmEnding(str, target) {
return target == str.substring(str.length - target.length);
}
console.log(
false == confirmEnding("s", "try long target strings") &&
false == confirmEnding("", "try an empty str") &&
true == confirmEnding("Bastian", "n") &&
true == confirmEnding("Congratulation", "on") &&
false == confirmEnding("Connor", "n") &&
false == confirmEnding("Walking on water and developing software from a specification are easy if both are frozen", "specification") &&
true == confirmEnding("He has to give me a new name", "name") &&
true == confirmEnding("Open sesame", "same") &&
false == confirmEnding("Open sesame", "pen") &&
false == confirmEnding("Open sesame", "game") &&
false == confirmEnding("If you want to save our world, you must hurry. We dont know how much longer we can withstand the nothing", "mountain") &&
true == confirmEnding("Abstraction", "action")
)
The first solution, using .startsWith()
, should be efficient, but it might be considered “cheating” to use .startsWith()
even though only .endsWith()
is prohibited.
The second solution is slightly simpler, but it involves creating a substring, so it would be less efficient.
Review
You have a bit too many variables to my taste, but that’s not much of a problem.
let lengthOfString = str.length; // is a variable that useful here?
I would use const
instead of let
because the variables are only set once.
The if-statements could be written with much less overhead.
First,
str.includes(target) === true
could be written as
str.includes(target)
But even better is to leave it out entirely, since the index would be -1 when not included anyway, which would never match str.length - target.length
. Matti Virkkunen found an edge case that escaped my attention. I would still leave this check out, but add a guard that the last index should be > -1.
Further, the false branches are completely redundant. You should return the result of the condition rather than creating unnecessary branches yielding true or false based on the condition.
Simplified Solution
The function could be shortened to:
function confirmEnding(str, target) {
const subtractLengths = str.length - target.length;
const lastIndexOfString = str.lastIndexOf(target);
return lastIndexOfString === subtractLengths && lastIndexOfString > -1;
}
Since the challenge description and test cases don’t mention null and undefined values, I didn’t include any checks for these edge cases.
Review
This review may sound a bit harsh, but bad habits are hard to break so I point them out.
- Use constants for variables that do not change.
- Don’t create single use variables if they do not improve readability.
- Don’t test for equality / inequality when the statement expression evaluates to a boolean. eg
if(str.includes(target) === true){
is the same asif (str.includes(target)) {
- Idiomatic JS has space after
if
andelse
,else
on the same line as closing}
space between){
-
when if statements have all the branches return the literal value of the evaluated expression, return the result of the expression not literals. eg You have
if
can be just
(lastIndexOfString === subtractLengths) { return true } else { return false }return lastIndexOfString === subtractLengths;
-
If a statement block returns then it should not be followed by an
else
egif(foo) { return bar ) else { return foo }
does not need the second blockif (foo) { return bar } return foo;
- Don’t repeat logic. You find the last index with
str.lastIndexOf(target);
then you test iftarget
is instr
. withstr.includes(target)
yet the index you have from the previous line holds that information alreadyif (lastIndexOfString > -1) {
You are just doubling up on the same complex test. - Avoid preempting calculations that may not be needed. The value
subtractLengths
is only needed ifstr
containstarget
, if not you have calculated a value that is never used.
Names
Your naming is very bad. Too verbose, inconsistent, and just a mess.
When naming variables remember the context in which the names are created. They need only have meaning within this context and need nothing outside this boundary.
We all can read, but most computer languages have a problem with spaces, soWeEndUpUsingSomeCrazy capitalization to get past this. This makes these names very prone to being miss read and miss typed. As JS is not compiled such errors can hide in code for a very long time. When naming less is more.
Some naming tips.
- Keep names short 1 2 words if possible.
- Use common abbreviations. eg
str
, forstring
,idx
forindex
,len
forlength
. Don’t make up abbreviations - Don’t use conjunctions and avoid prepositions in variable names.
- Meaning is inferred, and often in good code what the variable name actually is, is only relevant as an identifier, the semantic meaning being self evident from its location, scope, declaration, assignment, and use.
Rewrite
Using the above points rewriting your function with the same logic we get a smaller cleaner faster and arguably better function
function confirmEnding(str, endStr) {
const idx = str.lastIndexOf(endStr);
if (idx > -1) { return idx === (str.length - endStr.length) }
return false;
}
I would change some things to cover some edge cases you may not have thought of.
function confirmEnding21(str = "" , end = "") {
const idx = str.lastIndexOf(end);
return idx > -1 && end.length > 0 && idx === (str.length - end.length);
}
Testing
Your tests are weak.
Altogether you only have 4 different tests.
confirmEnding("Bastian", "n");
confirmEnding("Congratulation", "on");
confirmEnding("Connor", "n");
confirmEnding("Open sesame", "pen");
All the other tests are just repeating one of the above. However there are many tests you have not presented. (NOTE I am guessing the expected values so only show my expectation and functions return)
confirmEnding("",""); // expect false. Actual true
confirmEnding("abcdef", ""); // expect false. Actual true
confirmEnding("abcdef", "abcdef"); // expect true. Actual true
confirmEnding("abcde", "abcdef"); // expect false. Actual false
confirmEnding("", "abcdef"); // expect false. Actual false
valid edge cases
confirmEnding("abcdef"); // expect false. Throws Cannot read property 'length' of undefined
confirmEnding(); // expect false. Throws Cannot read property 'length' of undefined
totally unexpected values
confirmEnding(1000, 0); // expect true. Throws str.lastIndexOf is not a function
confirmEnding(1000, null);// expect false. Throws Cannot read property 'length' of null
A function throwing an exception can be normal behavior and pass the unit tests if the exception is the correct type. This is part of the reason unit tests must be extensive. Subtle changes to the code layout and style can change the behavior.
For example moving the line let lastIndexOfString = str.lastIndexOf(target);
to the top of the function will change the type of errors thrown for some input. Unit test finds these inconsistencies and prevents a harmless source change become an sleeping disaster.
Unit tests must cover all behaviors or they are a waste of your time.
The solution makes sense, but like @dfhwze said, there are too many variables.
As a future maintainer, I skim your solution and wonder what subtractLengths
is, because it’s ambiguous unless you move your eyes up to read more code to get the context. It doesn’t read very smoothly.
I would re-arrange it a bit to read more like this:
str.lastIndexOf(target) + target.length === str.length
because that’s easily parsable in English – the last mention of what you’re looking for, plus how long it is, should be the end of the string. Only make variables (or in this case const
s) if they are going to be re-used or if they help clarify the purpose of an expression.