The Virus

I had read a lot about plagues and pandemics years back wondering how and why these viruses spring up, wreak havoc and then just vanish into thin air! I realised these outbreaks were periodic, spanning several months if not years. Little did I know that my generation would be living through one.

Another question I pondered was if these pandemics are cyclic, why haven’t we in the 21st century, with all the advancements in technology and medicine, learned anything about how to prevent these epidemics and pandemics, most of which are endemic? Perhaps we aren’t the most intelligent species in the universe after all.

I’m deeply saddened and brokenhearted by how lives are being lost, and how society has been repressed to react to combat the virus with all the social distancing measures and lockdowns, a looming deep recession and all the detrimental side effects this virus has triggered. Besides people losing loved ones, jobs are being lost, making it even tougher for some of us to find new projects to work on.

Back in 2015, Bill Gates gave a TED talk that I’m sure nobody knew about until now. Had our politicians and leaders paid heed and played germ games instead of war games, prepared and braced for the worse with viral outbreaks adequately, and had some governments not suppressed public health information, this probably wouldn’t have happened… not at this scale.

It’s not all bitter for some of us. In these hard times, we need something to distract us from the reality of what’s happening. For most, it’s our families, pets, games, watching all the Harry Potter series or anything you can hold on to indoors. For me, it’s Ruby.

I’ll be leaving the virus issue to the virologists. We’re Rubyists, let’s talk about Ruby, shall we?

The last fifteen days have passed very swiftly for me. Any day now, I hope to wake up and read a news headline: “Virus conquered, Everyone Go Out and Play!” (That’d be a horrible headline if I were a journalist). I’ve spent 99.8% of all this time indoors reading and learning, mostly about Ruby and astrophysics. To keep me from going insane, I decided to build something.

What I Built

I built a command-line interface (CLI). To me, this sort of stuff takes more time than if I were to build a web app. If anything can occupy my time, I’d beg for it at this time. Of course, I’d want to use my time meaningfully.

It was supposed to be a straightforward CLI app, 500 lines of code maximum, and it was during this process that I learned first hand that the complexity of building software increases exponentially with time and added features. If you create two conditions and they have branches, and those branches happen to have more branches, you’re in a for a treat.

Currently, the CLI that was meant to be a simple one has about 2500 lines of code, and it’s not even halfway through what I envisioned. That’s good because it means I get to learn even more about Ruby while:

  • Building something “useful”.
  • Not getting bored and going crazy.
  • Keeping my mind off the current situation.

I named the CLI kovid. So much for creativity. You should bear with me on this one if you know the hard problems of computer science.

kovid just does one thing: it lets you filter and compare the data we have on the coronavirus. I intend to add another feature so it returns data as JSON that others can use in their apps, bringing the tally of things kovid can do to two. For now, you can fetch and compare statistics on any given country or state, or get aggregated data on entire continents. It’s an easy way to access information: it saves you the clicks, scrolls and swipes of navigating a traditional web app.

With kovid, comparing data on countries is as straightforward as typing kovid compare poland ghana usa.

You can also query individual countries with a command like kovid check croatia.

Because I spend a lot of time in the terminal, I think this is easier than having to leave the terminal, launching a browser and clicking around. The data comes from the Center for Systems Science and Engineering at Johns Hopkins University and worldometers.info.

How I Built It

When the thought of building a CLI sprang to mind, I started looking for free APIs that’d provide the data I needed. To my dismay, I discovered many people are making money off this crisis, potentially at the cost of human lives. I started thinking of ways to build a free API myself. The first idea was to scrape Wikipedia, but I ruled that out when I noticed Wikipedia is rather slow at publishing data, so I got back to searching until I came across disease.sh. A group of awesome developers had already noticed the problem and built a scraper for data from worldometers.info and Johns Hopkins University.

Let’s get building!

I grabbed a few gems that I’m already used to, like terminal-table and thor, to start with. I wanted then to cache requests and started looking for caching solutions for a CLI. Shannon Skipper, from whom I’ve learned a lot over the years, then recommended typhoeus. I looked it up and decided it’s a good pick, so I went for it.

Primarily, kovid is built by connecting these tools and navigating NovelCOVID/API to mould the data I need out of it. There isn’t much to it. The process of building the CLI, the little hurdles and the joy of working with Ruby and passing the time, is what mattered to me the most. I’m still building on top of kovid and enjoying the ride with new knowledge and tricks. This process keeps me sane as much as it is rewarding and relaxing at the same time.

The code is available on GitHub.

What I Learned

The plot twist of building a seemingly useless CLI is not the product itself. It’s learning, passing time meaningfully. For such a tiny CLI. I’ve learned a lot through Shannon and another Ruby Jedi, Lee. The funny thing about learning, not just about Ruby but any subject, is: the more you learn, the more you realise how much you don’t know.

For this reason, I try to stay very humble to the point that sometimes, people perceive this as stupidity. Playing with kovid has taught me countless things. I’ll list a few.

Hash#merge only accepts multiple args in Ruby >=2.6.0.

If you’re merging a hash you’d expect something like this to work:

head, *tail = country_array
  data = head.merge(*tail) do |key, left, right|
    # ...
    left + right unless %w[country countryInfo].include?(key)
  end.compact

It does indeed work, but only if you’re using a Ruby version >=2.6.0. Any Ruby version below 2.6.0 would result in complaints because it wasn’t until Ruby 2.6.0 that Hash#merge got support for multiple arguments.

There were multiple fixes for this. One was to use the backports gem, but grabbing a whole gem for this would be silly. In the end, we reached for a more sensible solution to rewrite the above as:

  data = country_array.inject do |base, other|
            base.merge(other) do |key, left, right|
              # ...
              left + right unless %w[country countryInfo].include?(key)
            end
          end.compact

Hacking codepoints to create flag emoji.

If you look at the kovid screenshots above, you’ll notice they have flags attached to the countries. My initial idea on adding emoji to a terminal table was to build a gem that, given an ISO 3166-1 alpha-2 code, would spit out a flag emoji – This is somewhat naive but naive only if you know of a better solution.

Back in the day, I discovered that flag emoji used “two-letter” ISO alpha-2 codes of the country they represented. I never thought about how these grapheme clusters could be programmatically manipulated to produce the flags that I wanted, though.

To see the individual graphemes you could do

  '🇭🇹'.codepoints.map { |codepoint| codepoint.chr 'utf-8' }

which returns

  ["🇭", "🇹"]

and to get the codepoints of this same flag '🇭🇹'.codepoints would be enough to return [127469, 127481].

With this new-found knowledge, let’s see how we can easily exploit Unicode, so we don’t have to create a gem that just spits out flag emoji.

Here’s a method that does that:

  COUNTRY_LETTERS = 'A'.upto('Z').with_index(127_462).to_h.freeze

  def country_emoji(iso)
    COUNTRY_LETTERS.values_at(*iso.chars).pack('U*')
  end

Step-by-step, the COUNTRY_LETTERS constants maps values of ASCII A starting with 127_462 through to Z. The returned hash looks like this:

  {
    'A' => 127_462,
    'B' => 127_463,
    'C' => 127_464,
    'D' => 127_465,
    'E' => 127_466,
    'F' => 127_467,
    'G' => 127_468,
    'H' => 127_469,
    'I' => 127_470,
    'J' => 127_471,
    'K' => 127_472,
    'L' => 127_473,
    'M' => 127_474,
    'N' => 127_475,
    'O' => 127_476,
    'P' => 127_477,
    'Q' => 127_478,
    'R' => 127_479,
    'S' => 127_480,
    'T' => 127_481,
    'U' => 127_482,
    'V' => 127_483,
    'W' => 127_484,
    'X' => 127_485,
    'Y' => 127_486,
    'Z' => 127_487
  }

The values are the codepoints of the graphemes that we’ll combine (pack) to get our emoji flag. So for this method, given a string of say "GH", it’d split the string into an array and #pack them into a binary sequence with the U* directive. The U* directive means “UTF-8 character”.

With this p country_emoji("PL") would return 🇵🇱. Nifty right?

Zero-width space (ZWSP).

Take a peek at the screenshot of kovid above and compare that to this screenshot:

You might notice that the flag emoji huddles against the text and the | character at the end on that row draws in. It appears the terminal-table gem only cares about ASCII characters. There was no easy way to fix this unless the country_emoji(iso) method is rewritten as:

  def country_emoji(iso)
    COUNTRY_LETTERS.values_at(*iso.chars).pack('U*') + \
      8203.chr(Encoding::UTF_8)
  end

The 8203.chr(Encoding::UTF_8) adds a zero-width space.

The zero-width space is a code point (a number) in a character set, that does not represent a written symbol used in computerised typesetting to indicate word boundaries to text processing systems when using scripts that do not use explicit spacing. They are used as in-band signalling to cause effects other than the addition of a symbol to the text. – Wikipedia

There are a few of them: [8203, 8204, 8205, 8288, 65279].

Hash#reject! returns nil when it rejects nothing.

In kovid the command to check historical data looks like this: kovid history usa. The corresponding code is:

  def history(country, last)
    # ...
    stats = if last
              transpose(country).last(last.to_i)
            else
              transpose(country)
            end

    dates = if last
              country['timeline']['cases'].keys.last(last.to_i)
            else
              country['timeline']['cases'].keys
            end

    unless last
      stats = stats.reject! { |stat| stat[0].to_i.zero? && stat[1].to_i.zero? }
      dates = dates.last(stats.count)
    end

    # ...
  end

I would run kovid history usa and get undefined method 'count' for nil:NilClass (NoMethodError) without immediately knowing why because binding.irb told me stats defined earlier is not nil. How is it nil in the unless block then? I pushed this aside to work on other features, until a PR came in from a contributor. When I saw that, I went to the documentation only to find:

reject!() public Equivalent to #delete_if, but returns nil if no changes were made.

Why did I think a banged method on an Enumerable worked like Array#map!? This made me more attentive when it comes to Ruby methods with a !. It’s unsafe to assume that a method you’re used to behaves similarly to another method that works on an Enumerable.

Adiós binding.pry. Hola binding.irb.

Ruby 2.4.0 introduced binding.irb – This is an excellent addition. While I’m very appreciative of Pry, I must say, the added hassle of having to install something extra and require it sometimes gets in the way. With Ruby versions higher than 2.4.0, you can add binding.irb anywhere without additional steps to debug your code. This, though seemingly small, is a boost to productivity. While building kovid, I added binding.irb to my toolchain, and that’s what I’ll be using henceforth.

Conclusion

We don’t know how long this pandemic will last, or when a vaccine will come out to help us fight it, there isn’t much we can do other than follow procedures and be extra vigilant about hygiene. I only hope this doesn’t last as long as previous pandemics, and that we can succeed in flattening the curve.

I don’t know about you, but for me, this situation is more than depressing. I’m happy I have Ruby. I find solace in Ruby. Building toy applications takes my mind off the devastation we’re experiencing in these times. Whatever you find relief in, stick to it as long as it’s a safe routine. I’m sure, like with all other adversities that humanity has suffered, this too shall pass.

Last Update: January 07, 2024