How I Made a MAS T-Bill Google Calendar
I finally had enough of manually checking the Monetary Authority of Singapore (MAS) T-Bill calendar from the official site. Just miss the buying window? Great! now I'll have to wait for another week or so.
Sometimes life gets in the way, then you forget, then you miss again. All that cash just sitting there doing nothing. I mean, there must be a better way to handle this, right?
The Calendar
Here are the links to the individual calendars:
Calendar | Link |
---|---|
SGS Bonds Calendar | Add calendar |
6-Month T-Bill Calendar | Add calendar |
1-Year T-Bill Calendar | Add calendar |
Savings Bond Calendar | Add calendar |
Idea
Existing Solution
Before diving into any project, I always like to see if there are existing solutions that might fit the bill. In my search, I stumbled upon ilovessb.com, which supports email notifications.
That's a step in the right direction, but there's still the possibility of missing those email notifications. Plus, I don’t really feel like giving away my email.
Google Calendar
Google Calendar notifications on the other hand are pretty neat. It supports device notifications, emails, or both. So yeah, it’s a real winner!
Anyway, I generally start my week by looking at my Google Calendar to see what's ahead for the week. Naturally, it occurred to me that having the MAS T-bill issuance calendar sync to my Google Calendar would be a neat solution. Why not automate this process?
The idea is pretty straightforward:
- Get the necessary data (bonds/bills issuance information and dates)
- Generate calendars and events using that data
- Run this on a regular schedule (monthly/yearly)
This should be an easy one. After all, I've already dabbled in a similar project before.
Goal
The ultimate objective here is to create a calendar that I can easily share with anyone by simply providing them a link. The idea is to offer people the benefits of using this calendar without requiring them to give up their email or name.
The only potential downside to this is that if I accidentally mess things up and delete these calendars, users might have to subscribe to a new calendar with a new link. But as long as I don't touch it, it should be smooth sailing (famous last words I guess). Anyway…
Data Source
The first step involves figuring out how to obtain the data we need to create events for the calendar. There are 2 approaches:
- Inspecting the network requests made when visiting the issuance calendar site
- Scraping the site’s HTML
Option 1 is always my preferred method due to its more structured and reliable nature.
MAS API
After some digging around the network tabs, I was able to identify the API calls made, which opened the door for us to fetch the data.
Then, I created a simple class that wraps around the respective MAS API requests. This file contains a class that wraps around the API requests. This makes it possible for us to interact with MAS API and gather the data required for our calendars later on.
If we want to fetch other data from other endpoints in the future, we can easily add support for those endpoints. No sweat!
Google Apps Script (GAS)
The core logic of Cron jobs (i.e. time-driven triggers), creating calendars, and events is written in TypeScript and deployed as a GAS project.
You can check out the code here.
Why Google Apps Script?
Well, there are a few reasons:
- It's free to run. This is an important point, especially since I plan to maintain this calendar for public use. Ensuring it's free is essential for its sustainability.
- Its Google Calendar API is super easy to use. No need to handle authentication like you would with its other languages SDKs. Dealing with that kind of stuff is an extra annoyance.
- It can be written in JavaScript/TypeScript, which is a language I'm already familiar with.
Also, since I haven't written any GAS project using TypeScript, I saw this as an opportunity to experiment with it. (The developer experience turned out great!)
Challenges
Oddly enough, I find the most challenging part of this project is to write tests. Well, more specifically when it comes to writing tests for GAS projects.
Wait, why bother writing tests for a pet project?
Well, the number of “live” tiny projects that I currently maintain is slowly getting out of hand. I’d imagine coming back to this project a year later, trying to refactor, add a small feature, or fix a bug, and thinking, "Will I break this..?".
For me, writing tests reduces that mental toil. It provides a certain “safety net”, assuring me that the changes won't break existing functionality. It's an oddly comforting feeling.
That being said, I always try to strike a balance between testing and maintenance. I believe in writing enough tests to check for regressions, but I also think that testing can become a maintenance burden if taken to an extreme. I don’t want to end up in a scenario where I spend more time wrestling with these darn unit tests than actually fixing the bug or adding a feature!
Why is it hard?
Testing a GAS project can be tricky. When you run code locally (e.g. using Jest) with Node.js, it uses the Node.js runtime. However, when you deploy that code as a GAS, it runs in a V8 runtime. This difference can cause issues when it comes to testing.
Let’s look at an example. First, make sure you have jest
and ts-jest
installed. If not, you can install them using npm
or yarn
:
npm install --save-dev jest ts-jest @types/jest
Now, imagine you've written a function called createMonthlyTrigger
, which, as the name suggests, is used to create a monthly time-driven using the GAS built-in ScriptApp
library.
Here's the code example:
export function createMonthlyTrigger(): GoogleAppsScript.Script.Trigger {
const triggers = ScriptApp.getProjectTriggers();
for (const trigger of triggers) {
const triggerExist = trigger.getHandlerFunction() === main.name;
if (triggerExist) {
Logger.log(`Trigger "${main.name}" already exists`);
return trigger;
}
}
Logger.log(`Creating a new monthly trigger`);
return ScriptApp.newTrigger(main.name).timeBased().onMonthDay(1).atHour(1).create();
}
The catch here is that you can't simply run jest
and expect it to work smoothly.
Instead, we need to mock the ScriptApp
module since it's a GAS-specific library and not available in a typical Node.js environment. To achieve this, you can use a library like ts-jest
with jest
to mock ScriptApp
.
Without mock, we will run into errors like ReferenceError: Logger is not defined
.
Here's an example of how to do it:
DevOps
These are the stuff that I always aim to automate as much as possible.
Continuous Integration (CI)
Setting up CI for your GAS project is pretty much the same as you'd do for any other Node.js project. There's nothing special or unconventional about it.
Take a look at the ci.yaml
file.
Manual Deployment
I highly recommend using clasp
(GitHub) when working on your GAS project. It's a game-changer, making your life a whole lot easier. It streamlines the development and deployment process, making it a more enjoyable experience.
I've been thinking about adding a Continuous Deployment (CD) part in the future, which would automate the deployment process. Though, I'm not sure how often I'll be deploying updates anytime soon. This project feels quite "completed" for now, but who knows?
Release
I'm using semantic-release to automate my releases. However, in this project, I've disabled the part where it automatically publishes the codebase as an NPM package (I mean it’s not a library where you would import).
You can take a look at the release.yml
and package.json
if you're curious.
Dependency Update
Just like many of my other projects, I've set up automatic dependency updates using Renovate. It's great for keeping the project's dependencies fresh and up-to-date with minimal effort on my part.
The best part? With CI setup and tests, it adds another layer of confidence to make sure that any automated updates don't pose a big risk to the project's stability.
Closing Thoughts
So, there you have it! This project is short and sweet, but it has been on my to-do list for a while. It's one of those small, nagging issues that bother me a bit every day, and I'm happy to finally find a solution.
Thanks for reading!