I've recently been trying to improve my rather disorganized personal finances. While I previously "managed" by leaving the bulk of the money in my account and hoping that I'd calculated my expenses versus salary roughly correctly, this is suboptimal for answering various questions like "Do I have enough money to go on holiday?" or "Have I forgotten to pay the electricity bill for the last year so that I now have to pay it back?"
Clearly there is a better way. Checking off expected payments, knowing what's due in the next week/month/etc., and siphoning off the excess into a linked savings account for example. But this requires being alert and accurate... Failing that, I could always use a computer ;-)
Spreadsheets
Surprisingly perhaps, the humble spreadsheet is an instrument of frustration for this kind of task. I'm currently using Google Docs, which is underpowered, but not really much more hateful than Excel or OpenOffice.org.
It's particularly bad at being a working document. Moving an item (to reschedule the date, for example) involves:
- inserting rows
- cutting and pasting
- deleting the original rows
- changing the date
- "filling" down the subtotal column (because spreadsheets don't have the concept of a calculated column).
Worse yet, because spreadsheets don't know about dates, adding regular payments (bills, weekly spends etc.) is an exercise in manually creating them, and then visually auditing the sheet to make sure you put them in the right places.
Other apps
Of course dedicated apps for personal finances do exist. I looked at the Linux ones, Gnucash, Kash, etc. Mostly they crashed. Sometimes they ran but utterly confused me. And then crashed.
There are also web based ones. My initial survey of these suggests that they are prettier and easier to use, but perhaps clunkier, or tied into specific bank systems.
Beans means...?
So, being a programmer, the obvious answer is to attempt to roll my own. As a learning exercise ;-) Though I'll attempt various sketches of this in Haskell, I'll start off by trying to implement it in
Modern Perl: specifically the
Moose OO framework, using various niceties such as the pretty declarative syntax of
MooseX::Declare.
Every project needs a good name! But this is just a learning exercise, so for now I'll call it "Beans". I'm hoping this will end up an ongoing series of posts, please feel free to follow along at the Beans git repo if you like, or to suggest improvements, for example:
- a better project name
- shinier ways to do this in Moose and Modern Perl
- how this would be more elegant in Haskell/Lisp/etc...
As always, these posts will be fairly rough drafts, I'm hoping to tidy this up into a more finished article soon, so any comments are really welcome to help me polish it up!
Declarative code
In this installment, I'll just define a data type for a line item in Beans. You can see the
current code as of this evening, but let's look at it in detail here. We'll start off by using Moose, and declaring our class:
use MooseX::Declare;
class Beans::Item {
...
}
Now we'll want to declare some fields for our class. For example, every payment made or expected will have a value:
has value => (
isa => Num,
is => 'rw',
required => 1,
);
What's that? Perl has types?! Yes indeed, Moose gives us types, and automatically validates against them. So when we try to create our object, we'd find that:
my $obj = Beans::Item->new( ); # fails, value is required!
my $obj = Beans::Item->new( value => "foo" ); # fails, "foo" is a string
my $obj = Beans::Item->new( value => 10.00 ); # OK! 10.00 is a number
I've cheekily missed out a few details though...
Num isn't a Perl builtin keyword, so we'll need to import it. In fact, while we're at it, let's pull in
Str too, so that we can add a couple of string fields at the same time:
use MooseX::Types::Moose qw/ Num Str /;
has name => ( isa => Str,
is => 'rw',
required => 1,
);
has comment => ( isa => Str,
is => 'rw',
);
Now we also want to be able to add the date of the expected payment:
use MooseX::Types::DateTime qw/ DateTime /;
has due_date => ( isa => DateTime,
is => 'rw',
required => 1,
);
This means we could do
my $obj = Beans::Item->new(
value => 10.00,
due_date => DateTime->new(
year => 2010,
month => 6,
day => 30,
),
);
# but I'd prefer to be able to do
due_date => '2010-06-30',
And we can indeed make this prettier using
coercions!
use DateTime::Format::Natural;
my $dt = DateTime::Format::Natural->new( prefer_future => 1 );
coerce 'DateTime'
=> from 'Str'
=> via { $dt->parse_datetime($_[0]) };
# We'll need to tweak the due_date declaration too...
# (coercions don't run unless you ask for them)
has due_date => ( isa => DateTime,
is => 'rw',
required => 1,
coerce => 1,
);
While every line item will have an expected due_date, we'll also want to keep track of what date the item was actually paid,
if it has been paid. As this is uncertain, we use the Haskell-inspired
Maybe type. (As far as I can see, this isn't actually as powerful as Haskell's Maybe monad, it's more like a nullable type).
use MooseX::Types::Moose qw/ Num Str Maybe /;
has paid_date => ( isa => Maybe[DateTime],
is => 'rw',
);
Once we've got this type, we can calculate a boolean field based on it. The
paid field will be true if paid_date contains anything, and false otherwise. Let's try defining it like this:
use MooseX::Types::Moose qw/ Num Str Maybe Bool/;
has paid => ( isa => Bool,
is => 'rw',
lazy => 1,
default => sub {
my $self = shift;
defined $self->paid_date
},
);
The
default code block gets called the first time we ask for the field. There are a couple of problems with this of course - once it's set, if we change the paid_date, this field won't change. We'll come back to this later, but here are some ideas:
- change it into a method
- use immutable objects, so that setting this lazily on first request is always the right thing to do
- use triggers to update paid when paid_date is set (and vice versa! setting the boolean flag could set the payment date to today's date, for example).
Now, though we specified that the name should be a string, nothing is preventing us from passing the empty string
""... but Moose allows us to define a better type constraint here too!
use MooseX::Types -declare => [qw/ NonEmptyStr / ];
subtype NonEmptyStr
=> as Str
=> where { length $_ };
# so now we can modify this definition to:
has name => ( isa => NonEmptyStr,
is => 'rw',
required => 1,
);
> Finally, let's add a list of tags. Just for now, we'll define these as an array of non-empty strings:
use MooseX::Types::Moose qw/ Num Str Bool Maybe ArrayRef /;
has tags => ( isa => ArrayRef[NonEmptyStr],
is => 'rw',
default => sub { [] },
);
Notice how we're using
default again to set the default value to an empty array.
That's it for now! Next time around we'll maybe look at parsing and displaying line items, and start to do useful things with them.