Open Source Adventures: Episode 14: Timecalc
Tomasz Wegrzanowski
Posted on March 11, 2022
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
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.
#!/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.
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
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
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.
Posted on March 11, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.