CoDEVIANT #6 (3/26/19)
I’m not going to lie. I really didn’t want to do this one today. I just felt too sleepy and tired…but I’m down for the cause and I want to be better at this. So I hauled my lazy ass out of bed and put on for my nerds.
Problem 1: Format a string of names like ‘Bart, Lisa & Maggie’
(https://www.codewars.com/kata/format-a-string-of-names-like-bart-lisa-and-maggie/train/javascript)
Given: an array containing hashes of names
Return: a string formatted as a list of names separated by commas except for the last two names, which should be separated by an ampersand.
Example:
list([ {name: ‘Bart’}, {name: ‘Lisa’}, {name: ‘Maggie’} ])// returns ‘Bart, Lisa & Maggie’list([ {name: ‘Bart’}, {name: ‘Lisa’} ])// returns ‘Bart & Lisa’list([ {name: ‘Bart’} ])// returns ‘Bart’list([])// returns ‘’
Note: all the hashes are pre-validated and will only contain A-Z, a-z, ‘-’ and ‘.’.
How Adrian Solved It!
Here were the points outlining my thought process:
- We need to create some conditional logic here to evaluate the length
- Of the names array, so we can have a better idea of what kind of punctuation marks to put between the names.
Basic Criteria
- If there is more than one name, the last element should be preceded by ‘& ‘
- If there are more than two names, the rule above applies, but all the others should be preceded by ‘, ‘
- If there is just one name than it should just the one name returned
- If there are no names, then we should return an empty ‘’.
So here is my solution:
function list(names){let nameArray = []; names.forEach(function(object){ nameArray.push(object.name); })console.log(nameArray);console.log(nameArray.length); switch(nameArray.length) { case 0: return ‘’; break; case 1: return nameArray[0]; break; case 2: return `${nameArray[0]} & ${nameArray[1]}` default: let last = nameArray.pop();console.log(nameArray); let string = nameArray.join(‘, ‘);console.log(last);console.log(nameArray.concat(last)); return(string + ` & ${last}`); break; }
}
You’ll notice I have a bunch of console.log commands strewn about. It’s completely normal to have them when you’re tweaking or building code to begin with, but once you compile or deploy actual apps or websites, you’re going to want to delete them so as not to slow down your work or potentially expose important or private keys/data, etc.
Anyways…
I create an empty array called namesArray
let namesArray = [];
Since the argument names is already an array, holding objects with key-value pairs of name: <Simpsons character in quotes>, we can use array-methods on names. And we use .forEach on names. .forEach( ) is a method that can take a callback function as it’s argument which in turn can take an argument that will be used to refer to each of the elements in the array that .forEach( ) is being used on (in our case, names). So for as long as names is, the callback’s argument object will refer to another element in names. It will dig into the structure of names, target the value for the key “name”, and pushes that value into our namesArray
example:let names = [{name: ‘Bart’},{name: ‘Lisa’},{name: ‘Maggie’}];names.forEach(function(object){namesArray.push(object.name);//first time it will be names[0] which equals -> {name: ‘Bart’}, and then cause ‘Bart’ to be sent to namesArray//namesArray at this point is [‘Bart’]//second time it will be names[1] which equals -> {name: ‘Lisa’}, and then cause ‘Lisa’ to join ‘Bart’ in the //namesArray array//namesArray at this point is [‘Bart’, ‘Lisa’]//third time it will be names[2] which equals -> {name: ‘Maggie’}, and then cause ‘Maggie’ to join her siblings //in the namesArray array//namesArray at this point is [‘Bart’, ‘Lisa’, ‘Maggie’]});//Then because we have no other array elements in names then we stop.
Now I have successfully separated the names of the characters we are going to deal with away from that potentially tricky array/object mixed formation, referred to I believe, as a hash.
At this point, I can implement some conditional logic to return values based on the number of names in our namesArray array.
So what are our criteria again?
- If there is more than one name, the last element should be preceded by ‘& ‘
- If there are more than two names, the rule above applies, but all the others should be preceded by ‘, ‘
- If there is just one name than it should just the one name returned
- If there are no names, then we should return an empty ‘’.
Okay, cool…so let’s make a switch case with nameArray.length as the argument
switch(nameArray.length) {//fun stuff coming!}
If there are no names, we should return ‘’
switch(nameArray.length) { case 0: return ‘’; break; default: break;}
If there is just one name than it should just the one name returned. remember that arrays are zero-indexed, so that first value would not be array[1], but rather array[0] ;)
switch(nameArray.length) { case 0: return ‘’; break; case 1: return nameArray[0]; break; default: break;}
- If there is more than one name, the last element should be preceded by ‘& ‘
switch(nameArray.length) { case 0: return ‘’; break; case 1: return nameArray[0]; break; case 2: return `${nameArray[0]} & ${nameArray[1]}`; break; default: break;}
Now before I continue, you might be wondering what money-signs have to do with this shit.
We are using a feature called Template Literals. It was a feature first introduced in ES2015, a modern, recently released version of JavaScript that is quite ubiquitous in development…and actually has been for a minute.
It allows you pass in variables that are in scope for the function being run at a given moment. Things that are in scope are basically things that the function is aware of and that’s putting it super super simply. At any rate, you activate the use of template literals by having two backpacks like so instead of quotes for regular strings.
` `
Then if you are planning on passing in a variable or a calculation between two variables inside a string that is set up with these back-ticks, you wrap the variables or the calculation syntax like so:
`${ <stuff you’re passing in> }`
So if you have a variable of food with the value of ‘chicken’, you can avoid doing the following:
var food = ‘chicken’;console.log(‘I can eat’ + food); // logs “I can eat chicken”
and instead do
var food = ‘chicken’;console.log(`I can eat ${food}`); // logs “I can eat chicken”
Ok big whoop. But what if you want to do some calculation?
Avoid this:
var a = 5;var b = 10;console.log(‘Fifteen is ‘ + (a + b) + ‘ and\nnot ‘ + (2 * a + b) + ‘.’);// “Fifteen is 15 and// not 20.”
Do this:
var a = 5;var b = 10;console.log(`Fifteen is ${a + b} andnot ${2 * a + b}.`);// “Fifteen is 15 and// not 20.”
Pretty good, no?
So now, back to…
So far we have covered all of our name amount potentialities except for when there are more than 2 names…so since we have 2 and less to nothing covered. Let’s make 2 or more names be our default case in the switch-statement
switch(nameArray.length) { case 0: return ‘’; break; case 1: return nameArray[0]; break; case 2: return `${nameArray[0]} & ${nameArray[1]}`; break; default: let last = nameArray.pop(); console.log(nameArray); let string = nameArray.join(‘, ‘); console.log(last); console.log(nameArray.concat(last)); return(string + ` & ${last}`); break;}
Let’s break down what I did here.
- I use the array-method of .pop( ) to turn the last element of the array nameArray into a variable called last
- I create a variable called string to equal the result of the use of the array-method .join( ) on nameArray, with the array-method argument being ‘, ‘ which turns the array nameArray into a string with each of it’s elements separated by a comma & a space. Because a comma and a space are what I passed into the array-method .join( ) when it was used on nameArray ;)
- Then I return the string + the result of using template literals to put “& “ and the value of the variable we created called last.
So when we clean it all up (get rid of all the un-needed console.logs) it will look like this:
function list(names){let nameArray = []; names.forEach(function(object){ nameArray.push(object.name); }) switch(nameArray.length) { case 0: return ‘’; break; case 1: return nameArray[0]; break; case 2: return `${nameArray[0]} & ${nameArray[1]}` default: let last = nameArray.pop(); let string = nameArray.join(‘, ‘); return(string + ` & ${last}`); break; }}
So let’s see how a̶l̶l̶ ̶t̶h̶e̶ ̶p̶e̶o̶p̶l̶e̶ ̶w̶i̶t̶h̶ ̶j̶o̶b̶s̶,̶ ̶a̶ ̶n̶o̶n̶-̶n̶e̶g̶a̶t̶i̶v̶e̶ ̶n̶u̶m̶b̶e̶r̶ ̶i̶n̶ ̶t̶h̶e̶i̶r̶ ̶c̶h̶e̶c̶k̶i̶n̶g̶ ̶a̶c̶c̶o̶u̶n̶t̶,̶ ̶a̶n̶d̶ ̶w̶h̶o̶ ̶h̶a̶v̶e̶ ̶t̶h̶e̶i̶r̶ ̶s̶h̶i̶t̶ ̶t̶o̶g̶e̶t̶h̶e̶r̶ my colleagues did it!
function list(names){ return names.reduce(function(prev, current, index, array){ if (index === 0){ return current.name; } else if (index === array.length — 1){ return prev + ‘ & ‘ + current.name; } else { return prev + ‘, ‘ + current.name; } }, ‘’);}
Hmmm….this is kinda…hairy (at least to me).
So here’s what I see:
- We use the .reduce( ) method on the names array that is passed in as an argument.
- Remember from previous entries in the series, that .reduce( ) mashes the values of an array to a single value.
- .reduce( ) takes two arguments, the first of which is a callback function with a max of 4 sub arguments
- * initialValue -> prev (the previously returned value of the function)
- * currentValue -> current (the current value being focused on)
- * index -> index (the array index of the current element)
- * arr -> array (the array the current element belongs to)
In our case, this will return the argument names, which we have already established is an array containing the names of our favorite Springfield residents.
- .reduce( )’s 2nd argument is a value to be passed to the function as the initial value.
- In this case: “ “
So how does it actually work?
function list(names){ return names.reduce(function(prev, current, index, array){ if (index === 0){ return current.name; } else if (index === array.length — 1){ return prev + ‘ & ‘ + current.name; } else { return prev + ‘, ‘ + current.name; } }, ‘’);}
Let’s assume we have no names.
Well if there is nothing to reduce in our array, then we’ll just return the ‘ ‘
If we have one name
Then when index === 0 (which it most certainly will the first time through), after that if there are no more values to go through, it will return the value of names[0] as being passed into the ‘ ‘ value at the end.
If the index happens to be equal to the length of the array minus one ((a necessary evil because of how zero-indexed constructs & how computers count vs how people count cause some friction when it comes to counting items))
then we return whatever the prev value of the initialValue is (which is also referenced by that “ “
**the 2nd argument of the .reduce( ) method) with a space, &, and the value of current.name
If the index is NOT the last one:
we return the prev plus a comma and then the value of current.name.
Takeaways:
This is a perfect example of knowing how versatile your methods can be and the full extent of what you can make them do. Punk ass, pee-wee idiot developers like me wouldn’t have thought to do something like this, but if you stop to think about it, it’s really succinct and pretty.
These developers used one method .reduce( )
But yours truly used:
- .pop( )
- .push( )
- .join( )
It’s like I’m starting some kind of dance craze or something.
I’m going to be honest…I tried to do two other ones before the problem I will present next, and they were just too damned hard. But I bookmarked them and I’m going to do them tomorrow. Promise ;)
Problem 2: Extract File Name
(https://www.codewars.com/kata/extract-file-name/train/javascript)
You have to extract a portion of the file name as follows:
- Assume it will start with date represented as long number
- Followed by an underscore
- Youll have then a filename with an extension
- it will always have an extra extension at the end
Inputs:
1231231223123131_FILE_NAME.EXTENSION.OTHEREXTENSION1_This_is_an_otherExample.mpg.OTHEREXTENSIONadasdassdassds341231231223123131_myFile.tar.gz2
Outputs
FILE_NAME.EXTENSIONThis_is_an_otherExample.mpgmyFile.tar
Acceptable characters for random tests:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-0123456789
The recommend way to solve it is using RegEx and specifically groups.
How did Adrian Solve It?
class FileNameExtractor { static extractFileName (dirtyFileName) { let firstUnderScorePosition = dirtyFileName.indexOf(‘_’) dirtyFileName = dirtyFileName.slice(firstUnderScorePosition + 1); let fileArray = dirtyFileName.split(‘.’) return `${fileArray[0]}.${fileArray[1]}`; }}
I was given:
class FileNameExtractor { static extractFileName (dirtyFileName) { return ’the answer’; }}
to start with.
So I knew the following:
- I needed to find the first occurrence of an underscore, because the long date would be immediately before it and we don’t want that in our final result
let firstUnderScorePosition = dirtyFileName.indexOf(‘_’)
- Then I wanted to slice out the underscore and everything above it by making the argument equal itself but with the date and the first underscore removed
dirtyFileName = dirtyFileName.slice(firstUnderScorePosition + 1);
- Then I wanted to create an array called fileArray and make it equal to dirtyFileName having .split( ) used on it but with a period as the argument so that we would wind up with the chunks we wanted, sans period
let fileArray = dirtyFileName.split(‘.’)//If dirtyFileName at this point equals ‘FILE_NAME.EXTENSION.OTHEREXTENSION’//this results in [ ‘FILE_NAME’, ‘EXTENSION’, ‘OTHEREXTENSION’ ]
- Now I only want the first two with a period between them, so I return the elements at indices 0 & 1 with a period between them like so:
return `${fileArray[0]}.${fileArray[1]}`;
And that’s it!
The smart people made this solution:
class FileNameExtractor {static extractFileName = dirty => dirty.match(/^\d+_([^.]+\.[^.]+)/)[1];}
…what…the…fuck.
So I ran it in the test area…and it doesn’t pass. Not all that glitters is golden, plus RegEx can get pretty hairy.
The runner up was this:
class FileNameExtractor { static extractFileName (dirtyFileName) { var numSlice = dirtyFileName.indexOf(‘_’) var extSlice = dirtyFileName.lastIndexOf(‘.’) dirtyFileName = dirtyFileName.slice(numSlice+1, extSlice) return dirtyFileName }}
Here’s what’s going on:
- Just like I did, we find the index of the first ‘_’
- But in a smarter way, they chose to find the last index of ‘.’
- Then they made dirtyFileName equal itself going through the slicing procedure where they cut from just after the first ‘_’ to right before the last ‘.’
- And then they profit.
The main take away is that whenever you can extract a few steps, go ahead and strive for it. But I don’t think either solution is bad, this one is just a tad bit better. If I was a RegEx master, I bet I could have figured out a way to do it in one line…but I’m not and it’s getting late.