I recently had to solve a problem that I already faced before: I need an app to
keep track of my daily expenses, it has to run on my phone (so I can enter
expenses on the run before I forget), and pretty much nothing else.
Last time I scratched this itch I built an Android app that did exactly that,
and it worked fine for what I wanted. But time has passed, I have forgotten
almost everything I knew about the Android SDK, and I wanted to get this version
of the app done from scratch in one hour - for a single CRUD application that
doesn't do the UD part, this seemed more than reasonable. So this time I decided
to go for good old HTML and plain JavaScript.
Building the interface
I decided to build the app in pure HTML - the requirements of my project are
modest, and I didn't want to deal with setting up a server anyway (more on this
later). I did want my app to look good or at least okay but, while I do have
some art delusions, I was never particularly
good at web design. Therefore, I am a fan of fast web design frameworks that
take care of that for me, and this project is not the exception. I am weirdly
fond of Pure.css but for this project I wanted something
new and therefore I settled for good old Bootstrap.
If you have never used either of these frameworks, the concept is simple: you
include their code in the header of your HTML file, you copy the template that
better fits the look you're going for, and that's it. In my case that means that
my empty template looked like this:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spendings</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
rel="stylesheet" crossorigin="anonymous"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN">
</head>
<body>
<main>
<div class="my-4"></div>
<div class="container">
<h2>New expense</h2>
<form id="main_form">
<!-- Here comes the form fields -->
</form>
</div>
<div class="my-4"></div>
<div class="container">
<h2>Export expenses</h1>
<div class="row">
<div class="col text-center">
<form>
<button class="btn btn-primary" onClick="export_csv(); return false;";>
Export data to .CSV
</button>
</form>
</div>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
</body>
</html>
Once this was up and running it was time to add form elements. In my case that
meant fields for the following information:
- Expense: what it is that I actually bought.
- Amount: how much I paid for it.
- Date and Time: when did I buy it.
- Category: under which category it should be stored.
- Currency: in which currency is the expense - useful when I'm traveling back
home.
- Recurrent: this is a checkbox that I use for expenses that I'll have to
re-enter every other month such as rent. I have not yet implemented this
functionality, but at least I can already enter this into the database.
- Two buttons: "Add expense" which would save this data, and "Export to CSV"
to download the data saved so far.
The first three fields (expense, amount, and currency) look like this. Note that
most of this code is a slightly edited version from the examples I copy-pasted
from the official Bootstrap documentation:
<form id="main_form">
<div class="mb-3">
<label for="inputDesc" class="form-label" required>Description</label>
<input type="text" class="form-control" id="inputDesc" required>
<div class="invalid-feedback">Enter a description</div>
</div>
<div class="mb-3 row">
<div class="col-sm-8 input-group" style="width: 66%;">
<span class="input-group-text" id="descAmount">Amount</span>
<input type="number" step=".01" class="form-control" id="inputAmount" required>
<div class="invalid-feedback">Amount missing</div>
</div>
<div class="col-sm-4 input-group" style="width: 33%;">
<select class="form-select" id="inputCurrency">
<option selected value="eur">EUR</option>
<option value="usd">USD</option>
<option value="ars">ARS</option>
</select>
</div>
</div>
<!-- A lot of other fields -->
<div class="col text-center">
<button type="submit" class="btn btn-primary"
onClick="add_expense(event); return false;">
Add expense
</button>
</div>
And with that we are done with the interface.
Storing the data
Web browsers have become stupidly powerful in the last years, meaning that they
not only include a full programming language (JavaScript) but also at least three
mechanisms
for saving data in your device: Cookies, Web Storage, and IndexedDB.
This mostly works for my project: having a database in my phone saves me the
trouble of having to set a database in a server somewhere, but there's still a
risk of data loss if and when I clean my device's cache. The solution, as you
may have seen above, is an "Export to CSV" button that I can use to save my
data manually once in a while - I could have chosen to e-mail myself the data
instead, but that's the kind of feature I'll implement once I have a need for
it.
We now need to delve into JavaScript. Most of the code I ended up using came
from this tutorial,
with the only exception that my code doesn't really need a primary index so
instead of writing:
const objectStore = db.createObjectStore("name", { keyPath: "myKey" });
I went for:
const objectStore = db.createObjectStore("name", { autoincrement: true });
Once you have implemented most of that code, you are ready to insert data into
your database. In my case that's what the function add_expense
is for and
it looks like this:
function add_expense(event)
{
// Validate the expense using the mechanism provided by Bootstrap
// See https://getbootstrap.com/docs/5.3/forms/validation/
form = document.getElementById("main_form");
form.classList.add('was-validated');
// These are the only two fields that need proper validation
expense = document.getElementById("inputDesc").value;
amount = document.getElementById("inputAmount").value;
if (expense.length == 0 || amount.length == 0)
{
event.preventDefault();
event.stopPropagation();
}
// Open a connection to the database
const transaction = db.transaction(["spendings"], "readwrite");
const objectStore = transaction.objectStore("spendings");
// Collect the data from the form into a single record
var data = {spending: expense,
date: document.getElementById("inputDate").value,
time: document.getElementById("inputTime").value,
category: document.getElementById("inputCateg").value,
amount: Number(amount),
currency: document.getElementById("inputCurrency").value,
recurrent: document.getElementById("checkRecurrent").checked};
// Store this record in the database
const request = objectStore.add(data)
transaction.onerror = (sub_event) => {
// This function is called if the insertion fails
console.log("Error in inserting record!");
alert("Could not save record");
// Displays a (currently hidden) error message
document.getElementById("warning_sign").style.display = 'block';
event.preventDefault();
event.stopPropagation();
}
transaction.oncomplete = (sub_event) => {
// This function is called if the insertion succeeds
console.log("Record added correctly");
// Ensures that the hidden error message remains hidden, or hides it
// if an insertion had previously failed and now succeeded
document.getElementById("warning_sign").style.display = 'none';
}
}
The function for creating the output CSV file is a simple combination of a
call to retrieve every record in the database and this
StackOverflow answer
on how to generate a CSV file on the fly:
function export_csv(event)
{
// Open a connection to the database
const transaction = db.transaction(["spendings"], "readonly");
const objectStore = transaction.objectStore("spendings");
// Request all records
const request = objectStore.getAll();
transaction.oncomplete = (event) => {
// Header for the CSV file
let csvContent = "data:text/csv;charset=utf-8,";
csvContent += "date,time,expense,amount,category,currency,recurrent\r\n";
// Add every record in a comma-separated format.
// Can you spot how many bugs this code fragment has?
for (const row of request.result)
{
fields = [row['date'], row['time'], row['spending'],
row['amount'], row['category'],
row['currency'], row['recurrent']];
csvContent += fields.join(",") + "\r\n";
}
var encodedUri = encodeURI(csvContent);
window.open(encodedUri);
}
}
The above-mentioned code has a fatal bug: I am building a CSV file by simply
concatenating fields together with a "," in between, and that's likely to break
under many circumstances: if my description has a "," in it, if my locale uses
"," as the decimal separator, if my description has a newline character for
whatever reason, and many more.
I could solve this by escaping/replacing every "," in the input fields with
something else, but let's instead learn the proper lesson here: do not build
CSVs by hand in production!
Conclusion
And just like that we are done. I set out to get it done in an hour, and I
almost made it - I had some issues with the database not initializing properly,
which I ended up solving by bumping up the database version and forcing the
database to rebuild itself. The glorious final version can be found following
this link. This app saves all of your data in your
local device, meaning that you can start using it right now. But you don't have
to take my word for it. If you don't trust some random dude with a blog you can
always check the source code yourself and make sure that the version I'm
linking to is the same I described above. Isn't open source code great?
I can imagine a couple reasons why one would want to develop an app like this.
Perhaps you need a working prototype that you want to put in the hands of alpha
testers right now. Perhaps you want an app that has to work in exactly one
device. Or maybe you have to develop in an environment where all you have is a
text editor, a web browser, and nothing more. This would be unacceptable for a
professional environment, but sometimes the problems you're trying to solve are
so simple that scaling up in resources and costs doesn't make
sense. Or maybe you would
like to go down to the basics once in a while and realize that not every app
needs React, a web server, and a cloud deployment.
I didn't manage to do all I set up to do. Some upgrades that I'm planning to add
gradually are:
- A fix to the CSV bug mentioned above.
- A message letting you know that you expense was inserted successfully.
- Implement the "Recuring expense" feature - I'm thinking of checking whenever
you open the app whether the current month has any recurring expenses.
If not, then the app asks you whether you want to insert them, one at a time
(in case one of the expenses is no longer valid).
- A hint of what the conversion rate for that day is, in case I don't want to
save "I bought this in US Dollars" but rather "I bought this in US Dollars
and that converts to this many Euros".
And finally, I should point out that this project was originally conceived as a
proof of concept for streaming and coding at the same time. I have video of the
whole experience but, given that the camera died on me halfway, you will have to
rely on my word when I say that you aren't missing much.
As a software developer I constantly have to fight the urge to say "this is
nonsense" whenever something new comes up. Or, to be more precise, as a
software developer who would like to still find a job 10 years from now.
I may still prefer virtual machines over clouds, but I also realize that keeping
up to date with bad ideas is part of my job.
I've given around turns around the Sun by now to see the rise of technologies
that we take for granted. Today's article is a short compilation of a couple
of them.
Seat belts
When I was a child, 2-point seat belts were present in all cars but mostly as
decoration. In fact, wearing your seat belt was seen as disrespectful
because it signaled that you didn't trust the driver, or even worse,
that you disapproved of their current driving. And they were not wrong: since
most people didn't use their seat belts in general, the only reason anyone would
reach out to use theirs was when they were actively worried about their safety.
Drivers would typically respond with a half-joke about how they weren't that
bad of a driver.
It took a lot of time and public safety campaigns to instill
the idea that accidents are not always the driver's fault and that you should
always use your seat belt.
Note that plenty of taxi drivers in Argentina refuse to this day to use theirs,
arguing that wearing a seat belt all day makes them feel strangled.
Whenever they get close to the city center (where police checks are)
they pull their seat belt across their chest but, crucially, they do not buckle
it in. That way it looks as if they are wearing one, sparing them a fine.
Typewriters and printers
My dad decided to buy our first computer after visiting a lawyer's office.
The lawyer showed my dad this new machine and explained how he no longer had
to re-type every contract from scratch on a typewriter: he could now
type it once, keep a copy of it (unheard of), modify two or three things,
run a spell checker (even more unheard of!), and send it to the printer.
No carbon paper and no corrections needed.
A couple years later my older brother went to high school at an institution
oriented towards economics which included a class on
shorthand and
touch typing on a typewriter.
Homework for the later consisted on typing five to ten lines of the same
sentence over and over. As expected, the printer investment paid off.
... until one day my brother came home with bad news: he was no longer
allowed to use the computer for his homework.
As it turns out, schools were not super keen on the fact that you could correct
your mistakes instantaneously, but electric typewriters could do that too so
they let it slide. But once they found out that you could copy/paste text
(something none of us knew at the time) and reduce those ten lines of exercise
to a single one, that was the last straw. As a result printed pages were no
longer accepted and, as reward for being early adopters, my parents had to go
out and buy a typewriter for my brother to do his homework.
About two years later we changed schools, and the typewriter has been kept
as a museum piece ever since.
Neighborhoods
My parents used to drink mate
every day on our front yard, while my friends
and I would do the same with a sweeter version (tereré).
My neighbors used to do the same, meaning that we would all see each other in
the afternoon. At that point kids would play with each other, adults would talk,
and everyone got to know each other.
This is one of those things that got killed by modern life. In order to
dedicate the evening to drink mate you need to come back from work early,
something that's not a given. You also need free time meaning, on a way, that
you needed a stay-at-home parent (this being the 90s that would
disproportionately mean the mom). The streets were not also expected to be
safe but also perceived to be safe, a feeling that went away with the
advent of 24hs news cycles. Smartphones didn't help either, but
this tradition was long extinct in my life by the time cellphones started
playing anything more complex than snake.
Unlike other items in this list, I harbor hope that these traditions still
live in smaller towns. But if you wonder why modern life is so isolated, I
suggest you to make yourself a tea and go sit outside for a while.
Computers
Since this could be a book by itself, I'll limit myself to a couple anecdotes.
When Windows 3.11 came up, those who were using the command line had to learn
how to use the new interface. Computer courses at the time would dedicate its
first class to playing Solitaire, seeing as it was a good exercise to get your
hand-eye coordination rolling. Every time I think about it I realize what a
brilliant idea this was.
My first computer course consisted on me visiting a place full of computers,
picking one of the floppy disks, and figure out how to run whatever that disk
had. If it was a game (and I really hoped it would be a game) I then had to
figure out how to run it, what the keys did, and what the purpose of the game
was. There were only two I couldn't conquer at the time: a submarine game
that was immune to every key I tried (it could have been 688 Attack Sub)
and the first Monkey Island in English (which I didn't speak at the time).
I had trouble with Maniac Mansion too until my teacher got me a copy of the codes for the locked door.
This course was the most educational experience of my life, and I've struggled
to find a way to replicate it. Letting me explore at my own pace, do whatever
I wanted and asking for help whenever I got stuck got me in an exploration
path that I follow to this day. The hardest part is that this strategy only
works with highly motivated students, which is not an easy requirement to
fulfill.
Today I'll take you on a small trip down the rabbit hole of verifying
whether something reported on the media is actually true.
Let's start with this article from
NPR
about customers suing food companies for misleading companies. Let's take a
look at this bit:
"Red Bull announced it would pay more than $13 million to settle a lawsuit
brought by buyers who said the energy drink didn't — as the marketing materials
promised — 'give you wings'".
What a stupid lawsuit, right? Of course drinking an energy drink won't give you
actual wings!
Well, hold on. If we follow that link we reach an article on Business
Insider
with the click-bait title "Red Bull Will Pay $10 To Customers Disappointed The
Drink Didn’t Actually Give Them 'Wings'". The text, however, explains that the
wings are figurative, that Red Bull claimed that "the drink can improve
concentration and reaction speeds", and that there was no scientific support for
any of that.
However! Red Bull has caffeine, which is supposed to actually do all that.
So what gives?
We need to go one level deeper. After plenty of searching through articles
just copying each other we eventually reach
this website with the text of the actual lawsuit.
According to the complaint, Red Bull...
"During the class period, [Red Bull] have made various representations to
consumers about the purported superior nature of Red Bull, over simpler and less
expensive caffeine only products, such as caffeine tablets or a cup of coffee.
To bolster those claims [Red Bull] post "scientific studies" on the Red Bull
website which they say 'prove' Red Bull's superiority.
However, no competent,
credible and reliable scientific evidence exists to support [Red Bull's]
claims about the product".
Instead of fighting it in court, Red Bull settled for $13 million
and retired the slogan.
So let's recap: we started with a perfectly reasonable complaint about false
advertising, namely, that Red Bull's claims about being superior to plain coffee
were false. This complaint ended up on a $13M settlement. It was then wrongly
reported as "the drink does not improve concentration" (it might, just not any
better than plain coffee), then was further reported as "customer angry that
Red Bull doesn't give you (metaphorical) wings", and presented with click-bait
titles that lead you to believe that the complaint was about actual, literal wings.
Remember, kids: when social media tells you something that sounds too good to
be true, it usually is. Always check your sources! Otherwise you may end up
backpacking through Europe
convinced that touristic places are not full of tourists simply because
Instagram told you so.
Oh boy, where do I even start with this...?
Someone posted yesterday to Hacker News their new project:
Meoweler, a cat-themed travel website that's almost entirely auto-generated.
I have written here before about
the realistic dangers of AI, predicting that we would see "garbage in any
medium that offers even the slightest of rewards, financial or not". And boy
was I right...
Here's a short, non-exhaustive list of problems I found with this website:
- Offensive stereotypes. About half the Argentinean cities I've checked
are based on stereotypes about the country, regardless of whether
they are even remotely applicable.
- Offensive pictures. I grew up in not-so-popular cities across the country
and I can tell you: I wasn't born in a war-torn city, I didn't go to school
in Tatooine, I didn't study in a
mirror-like desert, and I didn't go to University on a mountain city.
I just lived in perfectly normal, standard, slightly boring cities.
But you wouldn't know any of that by looking at the Midjourney-generated
illustrations for each one of them.
- Bad information. I don't care that much that the AI suggests that Potsdam's
Brandenburg Gate
is a "mini version" of the one with the same name in
Berlin (which AFAIK is
not true), nor that it talks about getting lost "in the maze-like streets
of the Old Town" for the squarest blocks you'll find outside of Manhattan.
But when your list of attractions includes sightseeing points in cities
more than 200km away... and in a different Province... I hope you remember
to bring your good walking shoes.
- Dangerous information. The site includes an "LGBTQ safety" rating which is
auto-generated and not checked against any official source. The guide will
also gladly include locations "off the beaten path" that are plain unsafe
for a tourist. One such advice suggests "lounging in sunbathed plazas" in
a city that Travel Safe - Abroad
rates as having a high risk of pickpockets and medium risk for muggings.
And while I'm not here to tell you how to live your life, I think "don't"
is a solid advice for this situation.
The source code for the website is open, and the license seems to be
"do whatever you want with it, no restrictions". And you know what?
People will do whatever they want with it: they will spin God-knows how many
clones (dog themed, child themed, circus themed, ...), slap ads on it, and
the internet will drown a little more in auto-generated, unchecked, ad-ridden
SEO garbage.
But hey, at least the website has the tiny footer "Beware, most of the content
was generated by AI". So if you end up being robbed at gunpoint because you
went "off the beaten path" it will actually be your fault.
One final digression: I decided to check some of the cities listed as "Do not
travel"
in the US State website and I landed on Kabul, Afghanistan, which the US warns you against visiting due to armed conflict,
civil unrest, crime, terrorism, and kidnapping.
The AI description reads "Kabul is a friendly city: despite the violence,
Kabul residents are extremely hospitable and friendly towards visitors", which
to me as a (computational) linguist is fascinating: if the locals are hospitable
and friendly, then who is perpetrating "the violence"? Is this sentence blaming
foreigners for the situation in Kabul? Can someone kidnap you and be
hospitable at the same time? And since we know that a guest who kills their
hosts in their own house loses privileged status, does robbing,
kidnapping, and maybe murdering your visitors mean that you lose your status
as a friendly city?
I would say that I'm maybe overthinking it, but let's be honest: given that no
human thought of any kind went into this entire website, literally any attempt
at understanding it is, by definition, overthinking.
(I have changed the title of this article to add a "(I)" at the end. I get the
feeling this is far from the last entry in this series...)