Skip to content

Instantly share code, notes, and snippets.

@KyleMit
Last active June 9, 2021 03:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KyleMit/aa2f68f5694af4ed8449045ba2eec0ef to your computer and use it in GitHub Desktop.
Save KyleMit/aa2f68f5694af4ed8449045ba2eec0ef to your computer and use it in GitHub Desktop.
sArCasM cAsInG

Here's the challenge

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:

  1. 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.
  2. 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

1. Initial

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"

Red. Green. Refactor.

Let's keep tweaking this to get it better

2. Batch Casing

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("")
}

3. Batch Case Both

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("")
}

4. Use CharAt

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("")
}

5. Use CharAt for Both

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("")
}

6. Use Reduce

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])
   }, '')
}

7. Use For Loop

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
}

8. Remove Unused Array

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
}

9. For Loop w/ Len Cache

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
}

Show Me the Money

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

Perf Metrics

Performance Questions

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.

Most efficient way to ...

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"

SO - Prepend text to beginning of string

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment