Keeping track of bills can be hard. They come from so many places for so many things and come at different times of the year. In an effort to make the problem of keeping abreast of them, I wanted to build a program to do this for me.
Simply stated, I wanted a program that could do a few key things. For each account that gives me a bill statement, I wanted it to:
- check which statements I had downloaded,
- tell me if I was missing any statements that I would need to go download, and
- tell me when the next statement for each account was coming.
I’m new to Rust and wanted to try out the language for some pet projects.
And as I’ve heard from multiple sources, the best play to start learning something is by scratching your own itch.
I’ve made a tool to do this, called
You can download it from my GitHub repo.
If you want to know more about how I built it, this post is a breakdown of how I went about solving this problem.
The anatomy of a financial statement
At its most basic, a bill statement for an account is released on a certain day and pertains to a period of time.
The following bill for the same account will cover the next period of time, whatever it happens to be. You’ll get a bill sometime in the future for that one, too. Bills are usually released in some predictable pattern, like “the first Monday of every month” or “the last weekday of every three months”.
But not everything follows nice tidy rules. When you first open an account, your first bill usually covers a shorter period of time than what it normally is.
To add more confusion, many financial institutions use the weekends and holidays for backing up databases and veryfing information, so they don’t send out bill statements until the next weekday. So your phone bill that comes every four weeks isn’t necessarily going to come every 28 days if it happens to fall on Easter weekend.
Finally, each account that you have may have different rules. One account may release statments on the 19th of each month. Another may release on the 15th of each month. Another still might release on the 1st day of each quarter. Each account looks different, so you’ll need to keep track of them with some identifier and store the important information for each one.
With these observations, we can start to build a program.
Building a data model
At the very least, we have the following patterns we need to allow for:
- statements are released according to some rule like “every N days starting on this date” or “the Nth day every M months”
- the period of time can be in days, months, years, or some multiple of them
- statements will start on a certain day, but that may not match up with the statement cycle
- statements may need to shift backwards or forwards a few days to account for holidays or weekends
- statements will be downloaded in different folders on your machine
From this we can list a few key pieces of information:
- account name
- statement period
- date of the first statement
- where the statements are located locally
To define an account, we can make a struct as follows:
Everything here is standard Rust except for the
We’ll get to that later.
Some accounts may have a similar name or purpose (e.g. chequing accounts from multiple banks), so I’ve added an
institution to each account and defined a simple struct for them, too.
To tie them together, we’ll make use of a configuration file.
We don’t know ahead of time how big each
Institution object will be (names, being strings, are not a fixed size in memory), so we’ll want to make use of pointers.
We can store all this in a struct using a
This defines our data model, so now we need to define the logic behind the scenes.
Working with complex time sequences
Working with dates is always difficult, if only for the reason that days don’t divide months or years evenly or equally.
Relying on external libraries if often easiest for this type of problem.
The library we’re going to use in this case is
This crate has different time granularities, or
Grains, which handles our need for working with days, months, and years.
Grains, we can define various objects that implement the
TimeSequence trait is the powerhouse of the crate and makes use of Rust’s efficient
Iterator with some clever stepping forward and backward functions.
statement_period is going to make use of the
The flexibility of
NthOf with the
step_by function will cover all the period we need.
These are very general structs with different internals, so to keep the compiler happy we need to store the pointers in a particular way.
Shim struct inside the
kronos crate can handle this for us, and be a smart pointer with all the traits that we need.
I told you I’d get to it eventually.
The main logic of Quill itself comes from the
The first section find when the next statement should be released, given the period of the statement and the starting point for the account. The second section adjust for weekends, as is necessary. I don’t account for local holidays since that would require a lot of work hooking into a locale-based calendar database, and is simply too complex for the scope of this project. Approximate dates will be good enough for checking if we have a statement or not.
Looking for previous statements looks very similar except we use the
past() method instead of
Matching dates to statements
So now we have a method to get all the expected statements from a given account. We can also use some simple code to get a list of all the statement PDFs from a given folder. The last thing needed to get this working is to match statements to the bill dates.
There are some approaches I considered but avoided. Exact matching on statement dates and file names isn’t ideal because it can give some false positives if things aren’t exactly correct. Another approach would be to measure the time that a bill covers, build a contiguous series of time, and check that each interval is represented by a statement. But that would add more complexities to how to keep track of the service cover for a particular bill, separately from the sequence of statement release dates.
The final approach I settled on made use of a simple fact. If we consider the expected statement dates and the dates of the downloaded statements for a single account, as we move forward in time the two sequences should always alternate.
This doesn’t require matching a bill to the period of time it covers or keeping track of multiple complex time sequences with a mapping between them.
It also allows for some flexibile matching, which makes it a bit more robust in some ways.
You can efficiently store this information in a single
Vec, if you wanted to, and compare consecutive elements.
But using this simultaneous iteration through both
Vecs, we can compare and identify missing statements.
The function that runs this logic is below.
The decoration on top
The rest of the code is all about parsing a configuration file using
serde, and rendering the results in a nice way throught an intuitive CLI.
I use the
prettytable-rs crates to accomplish this.
Our example config file looks like this.
And we currently have 3 options for
quill: check for missing statements, show me the config, or tell me which statements are coming up next.
Conclusions & caveats
That’s it, the finished product. There are lots of ways to expand the scope of this tool, but it’s functional as is and satisfies the points I stated earlier.
But there are some caveats and limitations, of course. Some bill statements require you to act on them, but others, like statements from bank accounts, are just for your reference. To keep the scope of the problem contained, I ignored the problem of paying bills in the future. I just keep track of whether we have received the statement or not.
I also mentioned the no-holiday-tracking issue above. That’s not a big deal, in my opinion, and more work than it’s worth.
Finally, I’m not totally satisfied with how matching statements to expected dates works. I’ve encountered some unstable behaviour if you change the starting date slightly. But this is rare, so maybe it’ll be a bugfix in the future.
Overall, this was a fun project and I feel much more confident coding in Rust. It’s a good, well-designed language with a helpful compiler, even if you spend lots of time bashing your head against a wall trying to figure out why your pointers and traits aren’t exactly what you need them to be.
Quill is available on GitHub. Give it a try, if you like.