DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Open Source Adventures: Episode 14: Timecalc

So it turns out someone already created Evil Wordle, using the exact same algorithm I wanted to use. Well, it's a fairly obvious idea.

Before I pick up a new project, I want to do a few episodes about random things I created before, starting with a very small thing - Timecalc.

The problem

Some things like haircuts or cleaning up kitty boxes need to be done over and over, every N weeks or so. I don't want to put such tasks as a recurring events in the calendar, because if I do it late, I want it to start the new count down from when it was actually done, not from when it was supposed to be done.

So when I do something like that, I need a quick way to know what date is today + N days, weeks, or months.

This can be done in Ruby without that much difficulty, but it's still not perfect:

$ ruby -rdate -e 'puts Date.today + 7 * 4'
2022-04-08
Enter fullscreen mode Exit fullscreen mode

But I thought that maybe it would be more useful to create a custom command for it.

Timecalc command

The command works like this:

$ timecalc '10.days'
2022-03-21
$ timecalc '4.weeks'
2022-04-08
$ ./bin/timecalc '4.weeks + 2.days'
2022-04-10
$ ./bin/timecalc 'week - day'
2022-03-17
Enter fullscreen mode Exit fullscreen mode

The string passed to timecalc is just arbitrary Ruby code with some DSL preloaded, and automatically prints out the value. If the value is a Duration, it's added to Date.today before printing.

bin/timecalc

The binary doesn't do anything special, it doesn't even support --help or such. It just calls the library.

#!/usr/bin/env ruby

require_relative "../lib/timecalc"

ARGV.each do |expr|
  puts Timecalc.new.call(expr)
end
Enter fullscreen mode Exit fullscreen mode

By the way, I don't love any of the getopt-like libraries, as they tend to be overcomplicated and unflexible. optimist is OK.

lib/timecalc.rb

The library just loads Date and ActiveSupport, and then has some logic how to format the result. As you can see I thought about making it working with hours, minutes, and seconds as well, but I never actually used that functionality:

require "date"
gem "activesupport", ">=5"
require "active_support/core_ext/numeric/time"
require "active_support/core_ext/time/calculations"
require "active_support/core_ext/date/calculations"

class Timecalc
  def initialize(today=Date.today)
    @today = Date.today
  end

  attr_reader :today

  %i[
    second seconds
    minute minutes
    hour hours
    day days
    week weeks
  ].each do |unit|
    define_method(unit) { 1.send(unit) }
  end

  def call(expr)
    format_output eval(expr)
  end

  def format_output(result)
    case result
    when ActiveSupport::Duration
      (@today + result).to_s
    else
      result.to_s
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Specs

Here are specs, for library only:

describe Timecalc do
  examples = {
    "today" => "2019-07-20",
    "today + 3.days" => "2019-07-23",
    "today + week" => "2019-07-27",
    "today + 1.week" => "2019-07-27",
    "7.days" => "2019-07-27",
    "7.day" => "2019-07-27",
  }

  examples.each do |expr, expected_output|
    describe expr do
      let(:today) { Date.parse("2019-07-20") }
      let(:timecalc) { Timecalc.new(today) }
      let(:output) { timecalc.call(expr) }
      it do
        expect(output).to eq expected_output
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Was it successful?

Timecalc was a case of me thinking this use case might be big enough, and worth creating a fancy tool for it (something like Unix units command), then prototyping a super simple version, and discovering that the prototype does everything I want, and I never needed anything fancier.

And in retrospect, even that is only a modest improvement over what Ruby one-liners like ruby -rdate -e 'puts Date.today + 7 * 4', so I didn't even mention timecalc until now.

I still use timecalc occasionally, so in the end it sort of worked out. And it was definitely worth doing a quick prototype before starting with parsers etc.

If you're interested, you can get the code here.

Coming next

In the next few episodes I want to showcase some of my other tiny projects, and how they went.

Top comments (0)