Writing a Rust library crate for time calculating (1)
In this blog post, which might turn into a short series, I want to plan a rust library crate and write notes down how to implement it.
This article was yet another one that I wrote while being on my trip through Iceland. As you can see – my head does never stop thinking about problems.
Usecase
So first of all, I want to write down the use case of this library. I had this idea when thinking about how to design a user frontend for imag (of course) and came to the conclusion that rust lacks a library for such a thing. So why not writing one?
I want to design the user interface of this library crate approximately like Rails did with their implementation of the same functionality for Ruby (bear with me, I'm not that involved in the Ruby world anymore so I don't know whether this is actually Rails or just another gem that comes with it).
So what I want to be able to do is something like this:
let event_date = today() – days(2) + weeks(10);
for example. I'm not yet entirely sure whether it is possible to actually do
this without returning Result<_, _>
instead of real types (and because I'm
in Iceland without internet connection, I cannot check). If results need to
be returned, I would design the API in a way so that these functions and
calls only create a AST-like object tree which then can be called with a
function to calculate the final result:
let eventdate = today() – days(2) + weeks(10); let eventdate = try!(event_date.calc());
But even more ideas come to mind when thinking about functionality this library may provide:
// Creating iterators today().repeat_every(days(4)) // –> Endless iterator
// Convenience functions (today() + weeks(8)).endofmonth() // The end of the month of the day in 8 weeks
today().endofyear().day_name() // name of the day at end of the current year
today().until(weeks(4)) // Range of time from now until in 4 weeks
// more ...
Later on, a convenient parser could be put in front of this, so a user can actually provide strings which are then parsed and be calculated.
calculate(“now – 4h + 1day”)
Which then could of course be used to face a user as well.
Core Data types
As the foundation of this library would be the awesome “chrono” crate, we do not have to reimplement all the time-related things. This eases everything quite a lot and also ensures that I do not double work which others have done way better than I could have.
So at the core of the library, we need to encapsulate chrono types. But there are many user-facing types in chrono and we cannot assume we know which of them our users need. So we have to be generic over these types, too. This is where the fun starts.
At the very base level we have three kinds of types: Amounts (like seconds, minutes, etc, fixed points in time as well as time ranges:
pub enum TimeType { Seconds(usize), Minutes(usize), //... Years(usize), Point©, Range(A, B) } // A, B and C being chrono types which are wrapped
As I assume right now, we cannot simply subtract and add our types (and thus chronos types) without possible errors, so we have to handle them and return them to the user. Hence, we will create intermediate types which represent what is about to be calculated, so we can add and subtract (etc) them without error:
enum OpArg { TT(TimeType), Add(AddOp), Sub(SubOp) }
pub struct AddOp(OpArg, OpArg); pub struct SubOp(OpArg, OpArg);
trait CalculateableTime { calc(self) –> Result; }
with the trait implemented on the former types – also the enum maybe as I explain in a few words.
To explain why the CalculateableTime::calc()
function returns a TimeType
rather than a chrono::NaiveDateTime
for example, consider this:
(minutes(15) – seconds(12)).calc()
and now you can see that this actually needs the function to return our own type instead of some chrono type here.
The OpArg
type needs to be introduced to be able to build a tree of
operations. In the calc
implementation for the types, we can then recursively
call the function itself to calculate what has to be calculated. As the trait is
implemented on TimeType
itself, which just returns Self
then, we
automatically have the abort-condition for the recursive call. To note: This is
not tail-recursive.
Optimize the types
After handing this article over to two friends for some review, I got told that the data structures can be minified into one data structure. So no traits required, no private data structures, just one enum and all functions implemented directly on it:
enum TimeType { Seconds(usize), Minutes(usize), //... Years(usize), Point©, Range(A, B),
Subtraction(TimeType, TimeType), Addition(TimeType, TimeType) }
and as you can see, also almost no generics.
After thinking a bit more about this enum, I concluded that even things like
EndOfWeek
, EndOfMonth
and such have to go into it. Overall, we do not want a
single calculation when writing down the code, only lining up of types where the
calculate
function takes care of actually doing the work.
Helper functions
In the former I used some functions like seconds()
or minutes()
– these are
just helper functions for hiding more complex type signatures and can hopefully
be inlined by the compiler:
pub fn seconds(s: usize) –> TimeType { TimeType::Seconds(s) }
So there is not really much to say for these.
Special Functions, Ranges, Iterators
To get the end of the year of a date, we must hold the current date already, so
these functions need to be added to the TimeType
type. Ranges can also be
done this way:
now().until(tomorrow()) // –> TimeType::Range(_, _)
Well, now the real fun begins. When having a TimeType
object, one should be
able to construct an Iterator from it.
The iterator needs to be able to hold the value it should increase itself every
time as well as a copy of the base value. With this, one could think of an
iterator that holds a TimeType
object and every time the next()
function is
called, adds something to it and returns a copy of it.
Another way of implenting this would be to know how many times the iterator has been called, multiply this with the increase value and add this to the base.
I like the latter version more, as it does not increase the calculations needed
for getting the real value out of the TimeType
instance every time the
iterator is called.
This way, one can write the following code:
let v : Vec<_> = now() .every(days(7)) .map(TimeType::calculate) .take(5);
to retrieve five objects, starting from today, each separated by one week.
Next
What I think I'll do in the next iteration on this series is summarize how I want to develop this little crate. I guess test driven is the way to go here, after defining the type described above.
Please note: This article was written a long time ago. In the meantime, I
learned
from a nice redditor
that there is
chrono::Duration
which is partly what I need here. So I will base my work
(despite beeing already started into the direction I outlined in this article)
on the chrono::Duration
types and develop the API I have in mind with the
functionality provided by chrono
.
For the sake, I did not alter this article after learning of
chrono::Duration
, so my thoughts are lined up like I originally had them.
tags: #open-source #programming #software #tools #rust