Produce a function which takes in some text and produces a string that alternates between upper and lower casing between every character
Step 2 - Eek out the best possible perf
Step 3 - Find an ice bucket, fill it with coffee, and drink it while you're coding
Before we lean too heavily on performance, two disclaimers:
- Readability > Performance - For most production codebases, dev time is more expensive than computer time. I'd rather my code be easily groked by another dev than save a couple ms on a chip.
- Premature Optimization is the Root of All Evil - Sometimes, in critical hotpaths, you might really need to squeeze out performance if a component is getting hit 1,000s of times, but wait until your telemetry is telling you need to invest time there before diving in. There are so many more important performance improvements by several orders of magnitude than splitting an array appropriately. But as a brain teaser, I did learn a lot about performant JS to leverage as a tie breaker if deciding between two approaches
Okay, so here's my first attempt at a solution. Pause the video here if you want to try yourself (well, not the video, but the screen)
> "sarcasm capialization".split("").map((c,i) => i % 2 == 0 ? c.toLowerCase() : c.toUpperCase()).join("")
"sArCaSm cApIaLiZaTiOn"
Let's keep tweaking this to get it better
The case conversions cost a lot, so we can half the number by converting the entire string up front and only toggling in the opposite direction every other loop
let sarcasticFont = (text) => {
return text.toLowerCase().split("").map((c,i) => i % 2 == 0 ? c : c.toUpperCase()).join("")
}
Just as a tiny pivot, let's pull the first casing conversion and save as a variable... no funny business yet
let sarcasticFont = (text) => {
let lower = text.toLowerCase().split("")
return lower.split("").map((c,i) => i % 2 == 0 ? c : c.toUpperCase()).join("")
}
If we continue with the idea that case conversions are expensive (because they create brand new strings that need to be stored), let's also pull out the uppercase to single instance as well. We'll convert the case and create an array so we can grab each character by position within the loop.
let sarcasticFont = (text) => {
let lower = text.toLowerCase().split("")
let upper = text.toUpperCase().split("")
return lower.map((c,i) => i % 2 == 0 ? c : upper[i]).join("")
}
But the .split("")
costs some time and we can just identify characters within a string by their position natively with "string"[3]
or "string".charAt(3)
. As long as their isn't a perf hit to doing a substring lookup by position instead of getting an item in an array by index, this should help. So let's get rid of the second split
let sarcasticFont = (text) => {
let lower = text.toLowerCase().split("")
let upper = text.toUpperCase()
return lower.map((c,i) => i % 2 == 0 ? c : upper[i]).join("")
}
We also don't need the first .split("")
in order to access characters by position. We can get the character from either lower / upper case string by position, but we still do need to loop over something. So if we can find a cheaper way to create an array of N items besides text..split("")
we'll get some perf. Unfortunately, we can't just use native Array methods like .forEach
or .map
directly on a string. We can create a brand new array of size N, but we also have to hydrate it in order to be able to loop over the collection of elements. We can do that by creating a new array and populating it using the spread operator
let sarcasticFont = (text) => {
let lower = text.toLowerCase()
let upper = text.toUpperCase()
let arr = [...Array(text.length)]
return arr.map((_,i) => i % 2 == 0 ? lower[i] : upper[i]).join("")
}
Right now, .map
is responsible for mutating the array and then .join
combines the array after the fact, so we're building a whole extra array to store in memory and later be joined. We can condense them into a single step by using .reduce
let sarcasticFont = (text) => {
let lower = text.toLowerCase()
let upper = text.toUpperCase()
let arr = [...Array(text.length)]
return arr.reduce((acc, cur, idx) => {
return acc + (idx % 2 == 0 ? lower[idx] : upper[idx])
}, '')
}
In head to head testing, if we just need to build a string from an array, a simple for loop comes out slightly on top of reduce
. So we can replace our accumulator with just a plain old variable that will be incremented each loop.
var sarcasticFont = (text) => {
let lower = text.toLowerCase()
let upper = text.toUpperCase()
var arr = [...Array(text.length)]
let output = ''
for (var i = 0; i < arr.length; i++) {
output += i % 2 == 0 ? lower[i] : upper[i]
}
return output
}
But now, of course, we don't need to generate an entire empty array just so we can iterate over it's .length
. We can just iterate over the text length directly and continue to append to our own output
var sarcasticFont = (text) => {
let lower = text.toLowerCase()
let upper = text.toUpperCase()
let output = ''
for (var i = 0; i < text.length; i++) {
output += i % 2 == 0 ? lower[i] : upper[i]
}
return output
}
Purely in the name of performance (and that's why we're here), there are some funky tricks to iterating over a loop. Some involve working backwards from a while
loop, but then we can't use the +=
operator, so the gains would be offset by additional string concatenations. We could use a forward while loop, but it's about as a good as a regular for loop with length caching, so we'll just do that. Sometimes the compiler can figure this out automatically, but some browsers need the hint.
var sarcasticFont = (text) => {
let lower = text.toLowerCase()
let upper = text.toUpperCase()
let output = ''
for (var i = 0, len = text.length; i < len; i++) {
output += i % 2 == 0 ? lower[i] : upper[i]
}
return output
}
But don't take my word on any of this. Here's each of the previous test cases in js Perf
https://jsperf.com/sarcasm-casing/1
The guiding force in having the authority to author many of the performance optimizations was in isolating out two different syntaxes head-to-head and figure out which worked better. Overwhelmingly, my guesses were usually wrong as to what the most performant syntax might be.
But take all this with a grain of salt as well. The setup conditions can have a huge impact when preferring one syntax over another for arrays with hundreds of elements or just a couple. Also, browsers are all over the place and some have optimizations in their JS engine for particular methods, and some don't, so performance perferences on one device might not be globally felt.
Loop through an array jsPerf
let arr = ["a","b","c","d"]
for (var i = 0, len = arr.length; i < len; i++) {
}
SO - What's the fastest way to loop through an array in JavaScript?
Fill Empty Array jsPerf
let output = [...Array(10)].map( (_,i) => i )
Hydrate an Array of length N
SO - JavaScript “new Array(n)” and “Array.prototype.map” weirdness
SO - forEach on array of undefined created by Array constructor
SO - Using a for each loop on an empty array in JavaScript
Create array for range of values 1...N
SO - Create a JavaScript array containing 1…N
SO - Does JavaScript have a method like “range()” to generate a range within the supplied bounds?
SO - Create array of all integers between two numbers, inclusive, in Javascript
Convert String to Array jsPerf
let output = [..."abcd"]
SO - How do you get a string to a character array in JavaScript?
Get Character at Position in String jsPerf
let output = "string"[3]
SO - string.charAt(x) or string[x]?
Convert Array to String jsPerf
let arr = ["a","b","c","d"]
let output = "";
for (var i = 0; i < arr.length; i++) {
output += arr[i];
}
SO - Convert javascript array to string
Prepend text on a string jsPerf
let output = "prefix" + "suffix"