Open Source Adventures: Episode 14: Timecalc
Tags: ruby
Published: 2022-03-11 16:57:34

So it turns out someone [already created Evil Wordle](https://qntm.org/absurdle), 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](https://github.com/taw/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
```

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
```

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.

```ruby
#!/usr/bin/env ruby

require_relative "../lib/timecalc"

ARGV.each do |expr|
  puts Timecalc.new.call(expr)
end
```

By the way, I don't love any of the getopt-like libraries, as they tend to be overcomplicated and unflexible. [`optimist` is OK](https://www.manageiq.org/optimist/).

### `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:

```ruby
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
```

### Specs

Here are specs, for library only:

```ruby
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
```

### 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](https://github.com/taw/timecalc).

### Coming next

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