Saying stuff about stuff.

My Prettier preferences and why

Over the years my own JavaScript code-formatting preferences have evolved but they don’t match Prettier’s defaults. I wondered whether I’d notice a difference so I started a recent new project with Prettier’s – and the community’s – defaults so I could find out.

After a few weeks I’m surprised (and more than a little pleased) to find that I’ve really noticed the difference and have switched back to my old, better, preferences. Each of the settings makes a noticeable difference when I’m writing code so here they are and why.

Single quotes (--single-quote)

Prettier makes quotes consistent either way so what’s the difference whether you write ' or "? The thing is that in general it’s easier to type a single quote than a double quote – because the former doesn’t require holding shift – and in particular with Vim it’s easier to type ci' (change inside quote) than ci". Simple as that.

Trailing commas (--trailing-comma es5)

Having a trailing comma on the final line of a multi-line object means that adding, removing, or moving entries doesn’t turn me into a comma juggler and force my brain to parse and validate the code. It also results in a cleaner diff so that only a single line is changed.

No semicolon (--no-semi)

This one is surely the most controversial because it involves semicolons. Consider the following:

const fourLetterShoutyWords = aListOfWords
  .map(word => word.toUpperCase())
  .sort()
  .filter(word => word.length === 4);

If I want to move the filter() line above the sort() it’ll take the semicolon with it and I’ll get a file.js|4 col 3| Unexpected token error. But with no semicolons, much like with trailing commas, I’m able to move code around more freely and without having to visually parse and validate the code myself.

Where Prettier makes a huge difference to fans of no semicolons is that old foe of forgetting to put one where it’s definitely necessary. This issue used to be something to fear (though it’s been many years since I last encountered it):

entirelyContrived = 1

(() => entirelyContrived++)()

Oops, what’s the problem: TypeError: 1 is not a function?

With Prettier enforcing no semicolons the reformatted code makes it more visually obvious that you’re actually attempting to call a function 1():

entirelyContrived = 1(() => entirelyContrived++)()

Conclusion

I have no doubt others will disagree with my choices but I’m happy to have found that they work for me and aren’t just subjective.

Getting the last day of the month or year in JavaScript

It’s easy to get the first day of a month but what about the last day of a month? Looking on Stack Overflow this is quite a common ask but it’s not something I’d ever encountered and the solution is quite interesting so I thought I’d describe it here.

It turns out the way to get the last day of a month is to ask for the day before the first day of the following month! I particularly like that phrasing because it’s both a description for humans and an explanation of the code. Here’s an example:

const month = 5 // June.

const startOfMonth = new Date(2019, month, 1)
// Sat Jun 01 2019...

const endOfMonth = new Date(2019, month + 1, 0)
// Sun Jun 30 2019...

Obviously zero is the day before the first day of a month?! And zero isn’t a special case, you can continue to count back:

new Date(2019, 6, 1)
// Mon Jul 01 2019...

new Date(2019, 6, 0)
// Sun Jun 30 2019...

new Date(2019, 6, -1)
// Sat Jun 29 2019...

// Keep going...

new Date(2019, 6, -364)
// Sun Jul 01 2018...

It works with months too, and across years:

new Date(2019, 11)
// Sun Dec 01 2019...

new Date(2019, 12) // ¿December + 1?
// Wed Jan 01 2020...

The two can be combined to get the last day of the year by asking for the zeroeth day of the thirteenth month (remembering it’s zero-indexed, so month number 12):

new Date(2019, 12, 0)
// Tue Dec 31 2019...

Or the final second before the new year:

new Date(2019, 12, 0, 24, 0, -1)
// Tue Dec 31 2019 23:59:59 GMT+0000 (Greenwich Mean Time)

And it’s the same behaviour when you mutate a date object – it does the cleverness on write so it reads back normally.

const date = new Date(2019, 6)
// Mon Jul 01 2019...

date.setMonth(12)
// Wed Jan 01 2020...

date.getFullYear()
// 2020

date.getMonth()
// 0

It seems a bit weird at first but behaves entirely consistently – as described in the specs (even the first one from 1997) – and is unusually developer friendly (particularly compared to the zero-indexed month debacle), I can’t believe I didn’t know this already.

Ruby’s identity method

When Ruby 2.2 added #itself I couldn’t think of anything I’d previously encountered where it would have been useful but I’ve finally used it in the wild. I had an array of integers and wanted to count their occurrences. Initially reaching for each_with_object I was hoping for something more meaningful, intention-revealing, and succinct.

[3, 1, 2, 1, 5].each_with_object(Hash.new(0)) { |i, memo| memo[i] += 1 }
# => {3=>1, 1=>2, 2=>1, 5=>1}

I remembered something about an identity method – #itself – and by using it you get the following (which is definitely more succinct):

[3, 1, 2, 1, 5].group_by(&:itself)
# => {3=>[3], 1=>[1, 1], 2=>[2], 5=>[5]}

So now I could write a method to extract pairs:

def pairs(array)
  array
    .group_by(&:itself)
    .select { |_, v| v.size == 2 }
end

pairs([3, 1, 2, 1, 5])
# => {1=>[1, 1]}

pairs([9, 1, 2, 2, 9])
# => {9=>[9, 9], 2=>[2, 2]}

In particular I wanted to check whether the array contained two groups each containing two items:

def two_pairs?(array)
  pairs(array).size == 2
end

two_pairs?([3, 1, 2, 1, 5])
# => false

two_pairs?([9, 1, 2, 2, 9])
# => true

Pre-#itself hack

Looking around the internet it turns out that people were already getting around the lack of an identity method by calling a no-op method that returns self:

[3, 1, 2, 1, 5].group_by(&:to_i)
# => {3=>[3], 1=>[1, 1], 2=>[2], 5=>[5]}

self and #itself, what’s the difference?

To be honest although I expected it to fail I had my fingers crossed trying to use self like so:

# This doesn’t work.
[3, 1, 2, 1, 5].group_by(&:self)
# => NoMethodError (undefined method `self' for 3:Integer)

It doesn’t work but I didn’t know why. It turns out that self isn’t a method, it’s a special keyword whereas #itself is a method defined on Object (although the Ruby 2.2 release notes say it’s defined on Kernel).

Caching Yarn workspaces on CircleCI

Yarn workspaces make it easy to share packages within the same monorepo but I found that something wasn’t quite working with CircleCI’s caching – the correct cache was found and used but it didn’t seem to be enough because yarn install continued for 30 seconds.

Yarn workspaces use a single yarn.lock at the root of the project along with a node_modules directory. However, even though the individual workspace node_modules directories may look empty you’ll probably see that they contain a .bin directory if you list hidden files – and this was the clue to fix my caching issue.

To ensure the cache is properly populated you must specify that each workspace’s node_modules directory is included in the cache along with the main one:

- save_cache:
    key: yarn-{{ checksum "yarn.lock" }}
    paths:
      - node_modules
      - workspace-a/node_modules
      - workspace-b/node_modules

Better Ruby Gem caching on CircleCI

I was grateful to Nick Charlton for his blog post on setting up CircleCI 2.0 for Rails as understanding the new v2 configuration options was something I’d been avoiding (v1 has since been deprecated so this is now a must). However, after using, loving, and getting so much value from Dependabot I noticed that with any change to my Gems they were all being installed from scratch – meaning that the cache wasn’t being used and minutes were being added to the build time. I took a little time to understand more about CircleCI’s caching and discovered that they actually already have this covered.

The restore_cache step can take an array of cache keys which it will look up in the order declared. So the seemingly erroneous final bundler- key in the following is actually really important as it allows CircleCI to fallback to the last cache that has the prefix bundler- (I had seen it in a couple of examples but wrongly assumed it would pointlessly look for a cache named literally bundler-).

steps:
  - restore_cache:
    keys:
      - bundler-{{ checksum "Gemfile.lock" }}
      - bundler-

Now after adding this the restore_cache will always fall back to the latest cache entry with the prefix bundler- and the bundle install step will only have to install missing gems. You can verify that it’s working by expanding the “Restoring Cache” step in the CircleCI UI, when there’s a cache miss you’ll see something like “Found a cache from build 513 at bundler-“:

No cache is found for key: bundler-7cHA+e+3dMj5o8KeEXzZWm_pWslivYO08S8xulWZ4gw=
Found a cache from build 513 at bundler-
Size: 66 MB
Cached paths:
  * /home/circleci/app/vendor/bundle

The next problem you’ll encounter is the cache growing with each change to your Gems. This is also easy to fix by running bundle install with the --clean option.

My full restore/install/cache section looks like this:

- restore_cache:
    keys:
      - bundler-{{ checksum "Gemfile.lock" }}
      - bundler-

- run: bundle install --clean --path vendor/bundle

- save_cache:
    key: bundler-{{ checksum "Gemfile.lock" }}
    paths:
      - vendor/bundle

Adding this caching has reduced my build time by a whopping 2 minutes – even if the change was bumping a patch version of a single Gem – and has helped reduce feedback time and CI utilisation. It’s also generally satisfying.