Friday, 31 May 2024

Some use cases for JavaScript's Spread Syntax

There's a new JavaScript syntax in town!

I'm just kidding; it's not that new. It's been around a couple years and I've gotten to use it sporadically. But it can come in really handy when you're dealing with arrays, strings or other iterables.

Like bullets from a
machine gun.

It looks like a machine gun blast, which is what gives it its name... I think.

The Syntax

The Spread Syntax is basically three periods followed by the object name to be deconstructed.

var obj = [100, 200, 3, 4, 5];
...obj //gives you 100, 200, 3, 4, 5

var str = "hello";
...str //gives you "h", "e", "l", "l", "0"


The syntax can't be used by itself as a value. It's actually a series of comma-separated values. So think of the Spread Syntax as a representation of those values, which can then be used in a construction of an array, or to fill in a function call.

Thus, if you have this...
var arr = [100, 200, 3, 4, 5];


...and you want to make an equivalent array, you could do this.
var arr = [100, 200, 3, 4, 5];
var arr2 = ...arr; //var arr2 = [100, 200, 3, 4, 5];


If you have an array of arguments...
var args = [true, 'test', 100];

function x(a, b, c)
{
    //code here
}


...you could do this.
var args = [true, 'test', 100];

x(...args); //x(true, 'test', 100)

function x(a, b, c)
{
    //code here
}


Copying an array

In one of the previous examples, we copied an array. What is the difference from doing, say, this?
var arr = [100, 200, 3, 4, 5];
var arr2 = arr;


Well, if you pushed a value to arr, arr2 would be affected as well.
var arr = [100, 200, 3, 4, 5];
var arr2 = arr;

arr.push(1000);
console.log(arr2); //gives you [100, 200, 3, 4, 5, 1000]


If you pushed a value to arr2, arr would similarly be affected.
var arr = [100, 200, 3, 4, 5];
var arr2 = arr;

arr.push(1000);
console.log(arr2); //gives you [100, 200, 3, 4, 5, 1000]

arr2.push(2000);
console.log(arr); //gives you [100, 200, 3, 4, 5, 1000, 2000]


Sometimes, that's just not what you want! So if we used the Spread Syntax to make array copies, those arrays would be independent of each other despite sharing the initial same values.
var arr = [100, 200, 3, 4, 5];
//var arr2 = arr;
var arr2 = [...arr]

arr.push(1000);
console.log(arr2); //gives you [100, 200, 3, 4, 5]

arr2.push(2000);
console.log(arr); //gives you [100, 200, 3, 4, 5, 1000]



Concatenating arrays, and other splicing

If you had these arrays, how would you concatenate them? Probably using some version of a structural loop (tedious) or the concat() method (better).
var arr1 = [1, 2, 3, 4, 5];
var arr2 = [6, 7, 8, 9];


Or use the Spread Syntax!
var arr1 = [1, 2, 3, 4, 5];
var arr2 = [6, 7, 8, 9];

varr arr3 = [...arr1, ...arr2]; //gives you [1, 2, 3, 4, 5, 6, 7, 8, 9]


This also works if you want to place the contents of an array within another array, or, as some of us like to call it, splicing.
var arr1 = [1, 2, 3, 4, 5];

varr arr2 = ["a", "b", "c", ...arr1, "d", "e", "f"]; //gives you ["a", "b", "c", 1, 2, 3, 4, 5, "d", "e", "f"]


Splitting a string

Most programming languages would already have a function to convert a string into an array of its characters. For JavaScript, it's the split() method.
var str = "test";
var arr = str.split(); //gives you ["t", "e", "s", "t"]


This pretty much does the same thing, but a lot more simply. Though this wouldn't work if you had "t e s t" and wanted to get ["t", "e", "s", "t"]. In a case like this, you would be better served using the split() method with a space passed in as an argument.
var str = "test";
var arr = [...str]; //gives you ["t", "e", "s", "t"]


Function calls

I mentioned function calls earlier. Some functions have an infinite number of parameters, such as the max() and min() methods of the Math object.
console.log(Math.max(1, 200, 4, 5, 6)); //gives you 200
console.log(Math.min(1, 200, 4, 5, 6)); //gives you 1


If you already had the values in an array, like so, you could do this.
var arr = [1, 200, 4, 5, 6];
console.log(Math.max(...arr)); //gives you 200
console.log(Math.min(...arr)); //gives you 1


That's it!

Thanks for reading. Just wanted to share this little tidbit.

And also some of the stuff I read about the Spread syntax confused the hell out of me, so I figured I would try to explain it my way. There's actually more fancy stuff you could do with the Spread Syntax. Maybe another time, for another day.


Spread the love, baby
T___T



Saturday, 25 May 2024

Googlers Fired For Protesting Project Nimbus

Google is in the news again for firing people, and this time it's a little different. Just last month, Google dismissed 50 staff members for being involved in protests against Project Nimbus - Amazon and Google's cloud computing services deal with the nation of Israel. 

For those who've been living under a rock, Israel and Palestine are at it again. The how and when are going to need more time than you have to read this blogpost, so let's move on. Suffice to say, the concern is that Project Nimbus will have military applications, even if this is not the expressed intent of Google.

Mixed feelings about the Googlers

I'll begin by saying that yes, I agree with CEO Sundar Pichai, politics should be kept out of the workplace. If you don't like what your company is doing and can't square it with your conscience, just leave. Don't be a drama queen about it. Especially, don't use the time that the company is paying you for, to work against the company itself. Amazon employees protested Amazon's involvement as well, but at least that was only in the form of a signed petition

On the other hand, Project Nimbus sounds all kinds of dangerous. A.I to profile people in Gaza? Possible military application? Huge potential for abuse. I don't blame the Googlers for not wanting any part of it. That goes double for if these Googlers are Muslim, or Palestinian. Seriously, why would Google even think it would be OK for a Muslim to aid Israel? There's only so far you can push the concept of "professional obligation".

An ominous cloud.

All that said, while their former staff might have been behaving in a manner that was less than professional, there's this part of me that thinks Google absolutely asked for it.

All these years, Google has been painting itself as this ultra-progressive place where their work shapes the world. They hired young idealistic types with self-righteous mantras such as "Don't be Evil" and "Do The Right Thing". The very kind of people who would be expected to dig their heels in if Google strayed from their perceived path of not being evil.

And now they object to this behavior? Come on, that's bullshit. Google made that bed, and now they get to lie in it.

About this whole Israel-Palestine thing...

I've had more than one person telling me that this Israel-Palestine issue should not be characterized as a "conflict" or a "war", and that doing so is disingenuous and downplays the fact that Israel is committing "genocide". OK, let's say that this is true. Let's say that the level of fuckery going on in that part of the world is off the scale.

But the Russia-Ukraine war is still going on. Pretty sure that hasn't been settled yet. People are still dying.

There's also whatever is allegedly going on in Xinjiang, China. Western media tells us it's a genocide, though at this point I trust Western media about as much as I trust China's. But again, let's assume it's true.

And Myanmar. After that coup back in 2021, the injustices are still happening, aren't they? Myanmar hasn't been somehow liberated while we weren't looking.

War is everywhere.

My question is; why do these people talk like the Israel-Palestine thing should matter to me the most? I get that if someone is Muslim or Jewish, without question, this should matter to them. And even if they're not Muslim or Jewish, they should be able to care about this issue as much as they wish.

But why me? Why do people insist that I care about this over everything else that's happening?

Human Rights violations? It's the humane thing to do? Children are dying? Shit, one could make the same argument about Russia-Ukraine, or Xinjiang, or Myanmar. I realize that this sounds horribly cold, but what makes Israel-Palestine special? Or is this just a case of "I care because I'm a good person, and if you were a good person, you'd care too"?

And if the answer is, no, I don't have to choose, I should care about all this equally? Sorry guys, I don't have the kind of bandwidth required to study the history of all these conflicts. I actually have a job, unlike those former Googlers (low blow, couldn't resist, heh heh) and shit I need to handle. At some point, we all need to choose what to spend our time and energy on. Yes, this sounds like a cop-out, but that doesn't make it any less true.

If they want people to prioritize this, a compelling reason is needed. And no, "be on the right side of history" (whatever that dumb shit even means) does not count. You guys realize that there is no "right" side of history, right? No matter what you do, or don't do, a hundred years from now, some snot-nosed kid is going to read a history book and judge you for it. The same way youngsters are now judging the likes of Abraham Lincoln, Lee Kuan Yew, and so on.

Conclusion

No two ways about it - in this story, everyone's an asshole.

Google, not for their business dealings with Israel. Project Nimbus is pretty disturbing, yes, but they're a company and they don't owe anyone an explanation as long as it's legal. But for hiring woke and passionate people who will absolutely put their passion over common sense, almost encouraging them to act that way, and then having the unmitigated gall to take exception when they inevitably do so? Total dick move.

The people who got fired, also did their best to make it happen. Shit, how did they think it was going to go down? Did they think Google was going to cave in to the pressure and beg for forgiveness? Google's been laying people off; they were probably grateful for the excuse.

No silver lining in this cloud!
T___T

Sunday, 19 May 2024

The Fuss Over Flexible Work Arrangements

By the end of 2024, companies in Singapore will have to provide a channel for employees to request for Flexible Work Arrangements. This includes Flexi-place, Flexi-time and Flexi-load.

This was announced by Singapore's Ministry of Manpower some time last month. The reactions to this, were not, as one might have expected, greeted with much fanfare. Employers, of course, weren't overjoyed by the prospect. As for employees, reactions have been mixed, but much of it negative. This being Singapore, I probably shouldn't be too surprised.

Reactions

Predictably, some employers are upset that now it's going to be a recommendation, and they might even be (gasp!) counselled for not adhering to guidelines. I can almost see these employers obstinately digging their heels in and declaring that they should be allowed to run their company how they see fit, oversight be damned.

Employees, on the other hand, have had the most varied reactions. There are some who have gone through decades of the traditional work model, commuting and all, and are decrying what they see as the entitlement of the modern-day employee. The sentiment seems to be: I put up with all this, it's not such a big deal, why can't you? What makes you so special?

There's, of course, the people who welcome this new development, and have been working for companies that already impleement some form of Flexible Work Arrangements. I count myself among these. Some people need Flexible Work Arrangements due to things going on with their lives - children, caretaker responsibilities, disabilities, among other things - and such arrangements would be a boon. I have no such responsibilities or disabilities, but my lifestyle has been a lot healthier after being allowed to work from home. There's a greater degree of control over what I eat and when I exercise and sleep, for instance. I've made it work for me really well.

More wayang?

And then there's the people who would welcome Flexible Work Arrangements, but view this move as lip service, nothing more. Just another cheap way to get popular support ahead of the next Singapore General Elections. After all, if it's "just" a recommendation and not legislation. If employers still have the final say as to whether or not they will allow Flexible Work Arrangements, this new development has no teeth. It is, as they say, wayang.

These people, are, however, just thinking of themselves, and stamping their proverbial feet crying that they want change and they want it now, dammit. That's not how things work right now, and that's certainly not how things have ever worked. Real, sustainable change, takes place over time. And it tends to begin with conversations like these. Change is lasting when all parties concerned benefit, and there's just no way to ascertain that without actually trying it.

The main issue here is trust. Trust between employers and employees, to be exact. Employers don't trust employees not to abuse what they see as an unearned privilege, and employees don't trust employees not to use things like Flexible Work Arrangements against them, where promotions and pay raises are concerned. I'm going to attempt to be fair here - both sets of concern are valid. There are employees who will abuse Flexible Work Arrangements shamelessly, and there are employers who will decide that your contributions aren't contributions if they can't actually see you working.

This is why we can't have nice things.

Is workplace flexibility a good thing?

Well, that really depends on whether we can make it work. The answer is going to be vary wildly with different business models.

As the only software developer in the department, with my own boss shuttling to and from different locations islandwide daily, in-office presence does not appreciably add to my work. Given that my own residence is at opposite ends of the island from my workplace, the daily commute to the office and back would leave me in no real shape or inclination to entertain any work communications outside of working hours. Working from home eliminates that commute, and also eliminates the need for me to watch the clock in order to catch the company bus, which can be a real hassle if I'm in the middle of a delicate software operation. Working from home allows me to work uninterrupted, if need be.

Working without
being disturbed.

Not having to share the internet connection with everyone else in the office is a real plus, too.

It has not been all smooth sailing, however. There's been times when colleagues have called me and not gotten a swift response. Most of it is because they try to contact me on Microsoft Teams outside of working hours, and on WhatsApp during work hours. (This is really strange to me. If employers don't want to encourage us to use our personal mobile phones while we're working, why contact us on WhatsApp?). And sometimes I resist strenuously when I'm called into the office with only a day's notice. My lifestyle is currently so regimented that something like this needs to be scheduled.

Employers (and some Managers) need to understand that not a single one of your employees sees the company the way you do. The company is your domain - you are ultimately in charge of people, profits and culture. Whether or not you have different departments handling these things is irrelevant; the buck stops with you. This is your kingdom.

For employees, this is just a place where they work, get paid and go home. Especially if they are mere grunts. Things like love and devotion are (and rightfully so!) reserved for family and friends. Do you, for example, imagine that any of them would work for you if they weren't being paid? If the answer is yes, you're delusional and this blog has nothing to offer you that a trained psychiatrist could. If not, you can probably see why your employees don't view their presence in the office the same way.

Fairness has also been named as a concern. Employers worry that allowing Flexible Work Arrangements for some would cause resentment among those who don't benefit. And indeed, there are some employers who would feel that way.

As excuses go, however, fairness probably ranks among the most disingenuous. What's "fair"? Do all employees benefit the same from the business? Do they all get paid exactly the same? I would wager my immortal soul that this isn't the case. Software developers, receptionists, drivers - all can be part of a company while delivering different value, and therefore get paid differently. Even two software developers doing the exact same job won't get paid the exact same wages - that would depend on market factors, perceived value and whatever you allow employers to get away with paying.

Some employees get paid more simply because they negotiated harder when it counted. Some employees get more benefits because they're prettier. It's obvious that not everyone gets the exact same treatment anyway, with or without Flexible Work Arrangements, so what's this "fairness" they speak of?

Why not legislate?

Why not just back up the words with action and force companies to do this? Sounds good, doesn't it? Unfortunately, kids, this is the real world. When you legislate, there's a lot that goes with it. Legislation is an administrative nightmare waiting to happen because legislation requires enforcement. When legislation occurs, you can no longer choose when to enforce it and when not to enforce it. Enforcement, or lack thereof, has to be consistent. You either always enforce it or never enforce it.

Companies come in several models, sizes and shapes. Not every company has a five-day workweek, dental benefits, or a ping-pong table. All for good reason - because every company has their own limitations and constraints.

Some companies need Singapore less than Singapore needs them. Those are the ones that pay well, and antangonizing them makes no bloomin' sense.

Some companies already implementing some form of channel to request Flexible Work Arrangements, which may not may not conform exactly with the guidelines. Getting on their cases with legislation is a lot of work for very little value. Why quibble over little details with someone who's already in agreement with you in principle?

It's not simply a matter of enshrining Flexible Work Arrangements in law. That is a child's point of view. Adult professionals need to be better than that.

Making it a law?

The Singapore Government has motives for doing these things. That's not to say that they're incapable of bad decisions; far from it. But they generally don't even take these steps without some kind of reason, even if that reason is sometimes completely stupid. In this case, employee welfare is only part of the picture.

Ultimately, Singapore needs to be seen as a first-world country. God knows we've done plenty towards that end - low crime, high GDP, stable and dependable political leadership, an award-winning airport. And workplace flexibility is one of the things needed to attract foreign talent to our shores.

What Flexible Work Arrangements companies want to provide, should ultimately be down to how badly they want to compete for talent. The Singapore Government shouldn't have to be involved at all, legislatively that is. If these companies just want wage slaves who will work for cheap, why begrudge them that? Some people literally have nothing but hard work to offer. They will work for little or no benefits. Should they starve instead, for the lofty principles of Flexible Work Arrangements? That's not my call to make at all, but my answer would be a resolute no.

When I was just a simple web developer, there were only tiny companies that had very few benefits who would hire me. Better positions were closed off due to a limited skillset and experience. Today, I can look back in disdain and say that these companies are now beneath my capabilities. But the fact remains that tf these companies had not existed to employ me and help me level up, I wouldn't have made it halfway this far.

You can say that they exploited me and that's true... but it's equally true that I exploited them right back.

In conclusion

This move annoys many employers and a perplexing number of employees. There is no winning with this one.

If this is indeed some cynical ploy of the Singapore Government to score political points ahead of the General Elections, this would be the dumbest move ever.

Stay flexible,
T___T

Wednesday, 15 May 2024

Web Tutorial: Python Matplotlib Bar Chart (Part 2/2)

Welcome back!

Before we proceed any further, we'll write a function that's going to be used later in multiple parts. You know how, so far, we have represented the seasons using year names? Well, that's not good enough. Mostly the English Premier League season starts around July and ends in May the following year. So the "2018" season would more accorately be the "2018/2019" season.

Let's write a function, seasonName(), to accept the season year as an argument and spit out the appropriate string.
def barChart(labels, vals, season, stat):
    plt.figure(figsize = (10, 5))

    plt.bar(labels, vals, color=(1, 0.2, 0.2))

    for index, value in enumerate(vals):
        plt.text(index - (len(str(value)) * 0.02), value + 1, str(value))

    plt.ylim(0, max(vals) + 5)
    plt.xticks(rotation=90)
    plt.axhline(y=np.nanmean(vals))

    plt.xlabel("Players")
    plt.ylabel("No. of " + stat)
    plt.title("Liverpool FC Player Stats for " + season)
    plt.show()
    
def seasonName(year):

data = {
    2017: {
        "Mohamed Salah": {"goals": 44, "appearances": 52},
        "Roberto Firminho": {"goals": 27, "appearances": 54},
        "Sadio Mane": {"goals": 20, "appearances": 44},
        "Alex Oxlade-Chamberlain": {"goals": 5, "appearances": 42}
    },


So we take season and convert it to a string using str(). We then concatenate "/" to it.
def seasonName(year):
    return str(year) + "/"


Finally, we concatenate the following year, also converted to a string via str().
def seasonName(year):
    return str(year) + "/" + str(year + 1)


Let's try this by changing the labels in barChart().
plt.xlabel("Players")
plt.ylabel("No. of " + stat)
plt.title("Liverpool FC Player Stats for " + seasonName(season))
plt.show()


We'll also need to slightly alter the function call to use the number 2018 instead of the string "2018".
barChart(players, stats, 2018, "goals")


So simple!


But the main reason we want this, is so that we can populate the user menu. We want a list of all seasons. For this, we use the keys() method on data and put the results in a list using the list() function. Then we assign the result to seasons. While we're at it, we prepare for the next operation by declaring ans1 and ans2 as empty strings.
    2022: {
        "Mohamed Salah": {"goals": 30, "appearances": 51},
        "Roberto Firminho": {"goals": 13, "appearances": 35},
        "Alex Oxlade-Chamberlain": {"goals": 1, "appearances": 13},
        "Diogo Jota": {"goals": 7, "appearances": 28},
        "Luis Diaz": {"goals": 5, "appearances": 21}
    }
}

seasons = list(data.keys())
    
ans1 = ""
ans2 = ""


players = list(data[2018].keys())
values = list(data[2018].values())

stats = [];
for v in values:
    stats.append(v["goals"])

barChart(players, stats, 2018, "goals")


Then use enumerate() on seasons so we can run it through a For loop with an index, i. We want to print i, but of course we have to convert it to a string via the str() function.
seasons = list(data.keys())
    
ans1 = ""
ans2 = ""

for i, s in enumerate(seasons):
    print (str(i + 1))

        
players = list(data[2018].keys())
values = list(data[2018].values())


Then concatenate ":" and a space, and the season.
for i, s in enumerate(seasons):
    print (str(i + 1) + ": " + seasonName(s))

players = list(data[2018].keys())
values = list(data[2018].values())


Cap it off with one last option, 0.
for i, s in enumerate(seasons):
    print (str(i + 1) + ": " + seasonName(s))
    
print ("0: Exit")
        
players = list(data[2018].keys())
values = list(data[2018].values())


There, you see the entire list appears on the menu!


We want the user to pick a season. Thus, we use the input() function, passing in a user-friendly prompt as an argument. We then set the result to ans1.
for i, s in enumerate(seasons):
    print (str(i + 1) + ": " + seasonName(s))
    
print ("0: Exit")

ans1 = input("Select a season")

players = list(data[2018].keys())
values = list(data[2018].values())


There you see the prompt appear. The bar chart will not appear until a value is entered.


But hold on... we want a number to be entered. How do we enforce this? We can force convert the value using the int() function. However, this is going to throw an error if someone enters something like "test".
for i, s in enumerate(seasons):
    print (str(i + 1) + ": " + seasonName(s))
    
print ("0: Exit")

ans1 = int(input("Select a season"))
        
players = list(data[2018].keys())
values = list(data[2018].values())


To forestall that, we use a Try-catch block. So if getting that input triggers an exception, an error message is printed.
print ("0: Exit")

try:
    ans1 = int(input("Select a season"))
except:
    print("Invalid option. Please try again.")

        
players = list(data[2018].keys())
values = list(data[2018].values())


We wrap this entire segment up in an infinite While loop, and add a break statement after running input(). Thus, if the input is invalid, the program keeps prompting the user until it gets an acceptable answer. Once the answer is valid, the break statement exits the While loop.
print ("0: Exit")

while True:
    try:
        ans1 = int(input("Select a season"))
        break
    except:
        print("Invalid option. Please try again.")    
        
players = list(data[2018].keys())
values = list(data[2018].values())


Try it. Enter an invalid value.


An error message will be printed, and you'll get the prompt again until you enter in a number.


From here, ans1, minus 1, will be used as an index referencing a value in the seasons array, and the result is season. And instead of 2018, we make sure the players and values now uses season.
while True:
    try:
        ans1 = int(input("Select a season"))
        break
    except:
        print("Invalid option. Please try again.")    
        
season = seasons[ans1 - 1]

players = list(data[season].keys())
values = list(data[season].values())


Finally, we make sure that season is passed into barChart() instead of 2018.
players = list(data[season].keys())
values = list(data[season].values())

stats = [];
for v in values:
    stats.append(v["goals"])

barChart(players, stats, season, "goals")


This is what happens when you enter in "4". It will look for the the element in seasons array at index position 3, resulting in the value for Season 2020/2021. And then the bar chart is generated!


We still need to tidy this up a little, though. What happens when the user enters a 0? What if they enter a number, but it's out of the desired range? For this, we encase the entire thing in yet another While loop that will run as long as ans1 or ans2 is not 0.
while (ans1 != 0 and ans2 != 0):    
    for i, s in enumerate(seasons):
        print (str(i + 1) + ": " + seasonName(s))
        
    print ("0: Exit")

    while True:
        try:
            ans1 = int(input("Select a season"))
            break
        except:
            print("Invalid option. Please try again.")    
        
    season = seasons[ans1 - 1]
    
    players = list(data[season].keys())
    values = list(data[season].values())

    stats = [];
    for v in values:
        stats.append(v["goals"])

    barChart(players, stats, season, "goals")


Then we handle the case for if the user enters a 0. In this case, we break out of the While loop using a break statement.
while (ans1 != 0 and ans2 != 0):    
    for i, s in enumerate(seasons):
        print (str(i + 1) + ": " + seasonName(s))
        
    print ("0: Exit")

    while True:
        try:
            ans1 = int(input("Select a season"))
            break
        except:
            print("Invalid option. Please try again.")    

    if (ans1 == 0): break
        
    season = seasons[ans1 - 1]
    
    players = list(data[season].keys())
    values = list(data[season].values())

    stats = [];
    for v in values:
        stats.append(v["goals"])

    barChart(players, stats, season, "goals")


If the numeric value of ans1 is out of the valid range of values, we use a continue statement to "restart" the While loop.
while (ans1 != 0 and ans2 != 0):    
    for i, s in enumerate(seasons):
        print (str(i + 1) + ": " + seasonName(s))
        
    print ("0: Exit")

    while True:
        try:
            ans1 = int(input("Select a season"))
            break
        except:
            print("Invalid option. Please try again.")    

    if (ans1 == 0): break
    if (ans1 > len(seasons) or ans1 < 0): continue
        
    season = seasons[ans1 - 1]
    
    players = list(data[season].keys())
    values = list(data[season].values())

    stats = [];
    for v in values:
        stats.append(v["goals"])

    barChart(players, stats, season, "goals")


See this? I first entered -6, which is out of the valid range, and it re-prompted me to make a choice. Same thing happened when I entered 8. But once I entered 0, the program stopped running.


Let's go ahead and do some user input for the stat type. So far we've been hard-coding "goals" as the statistic. Well, that's gonna change. Print the following lines.
if (ans1 == 0): break
if (ans1 > len(seasons) or ans1 < 0): continue
    
season = seasons[ans1 - 1]

print ("1: goals")
print ("2: appearances")
print ("0: Exit")


players = list(data[season].keys())
values = list(data[season].values())


Then repeat what we did for ans1, except this time we check ans2, and use the input() function to request for a stat.
if (ans1 == 0): break
if (ans1 > len(seasons) or ans1 < 0): continue
    
season = seasons[ans1 - 1]

print ("1: goals")
print ("2: appearances")
print ("0: Exit")

while True:
    try:
        ans2 = int(input("Select a stat"))
        break
    except:
        print("Invalid option. Please try again.")


players = list(data[season].keys())
values = list(data[season].values())


Again, we repeat what we did for ans1 for ans2. The same logic applies here.
if (ans1 == 0): break
if (ans1 > len(seasons) or ans1 < 0): continue
    
season = seasons[ans1 - 1]

print ("1: goals")
print ("2: appearances")
print ("0: Exit")

while True:
    try:
        ans2 = int(input("Select a stat"))
        break
    except:
        print("Invalid option. Please try again.")

if (ans2 == 0): break
if (ans2 > 2 or ans2 < 0): continue


players = list(data[season].keys())
values = list(data[season].values())


Then we handle the other options. If the user has entered 1, we set the variable stat to "goals". If the user has entered 2, we set stat to appearances. We won't have to bother declaring stat beforehand because in any other case, the program either ends or "restarts" the While loop.
if (ans1 == 0): break
if (ans1 > len(seasons) or ans1 < 0): continue
    
season = seasons[ans1 - 1]

print ("1: goals")
print ("2: appearances")
print ("0: Exit")

while True:
    try:
        ans2 = int(input("Select a stat"))
        break
    except:
        print("Invalid option. Please try again.")

if (ans2 == 1): stat = "goals"
if (ans2 == 2): stat = "appearances"

if (ans2 == 0): break
if (ans2 > 2 or ans2 < 0): continue

players = list(data[season].keys())
values = list(data[season].values())


In the For loop, we replace the hard-coded value of "goals" with stat.
players = list(data[season].keys())
values = list(data[season].values())

stats = [];
for v in values:
    stats.append(v[stat])

barChart(players, stats, season, "goals")


And also in the barChart() function call.
players = list(data[season].keys())
values = list(data[season].values())

stats = [];
for v in values:
    stats.append(v[stat])

barChart(players, stats, season, stat)


We repeat the tests. If we enter "hello" for the stat, it repeats the prompt.


If we enter an invalid range, it "repeats" the While loop.

Here, we enter 2 for "appearances"... and the chart generates! You can see that the label has changed from "goals" to "appearances" as well.


That's it...

This is but the tip of the iceberg. Python's matplotlib library offers a whole lot of functionality for customizing bar charts. Go check it out!

Luis Diaz, Mohamed Salah and Diogo Jota walk into a bar...
T___T

Monday, 13 May 2024

Web Tutorial: Python Matplotlib Bar Chart (Part 1/2)

Let's do some charting!

It will be football staistics again of Liverpool Football Club, and we will use Python's matplotlib library. Python is among the top choices for Data Analytics, and its charting capabilities are just the tip of the iceberg.

We start off by importing some libraries. We want numpy, and we want the pyplot functionality of matplotlib.
import numpy as np
import matplotlib.pyplot as plt


Next, we create a dictionary, data.
import numpy as np
import matplotlib.pyplot as plt

data = {

}


Data will have statistics split into seasons.
data = {
    2017: {

    },
    2018: {

    },
    2019: {

    },
    2020: {

    },
    2021: {

    },
    2022: {

    }

}


In 2017, we have statistics for these players.
data = {
    2017: {
        "Mohamed Salah": {},
        "Roberto Firminho": {},
        "Sadio Mane": {},
        "Alex Oxlade-Chamberlain": {}

    },
    2018: {

    },
    2019: {

    },
    2020: {

    },
    2021: {

    },
    2022: {

    }
}


Each player, in turn, has numerical properties goals and appearances.
data = {
    2017: {
        "Mohamed Salah": {"goals": 44, "appearances": 52},
        "Roberto Firminho": {"goals": 27, "appearances": 54},
        "Sadio Mane": {"goals": 20, "appearances": 44},
        "Alex Oxlade-Chamberlain": {"goals": 5, "appearances": 42}
    },
    2018: {

    },
    2019: {

    },
    2020: {

    },
    2021: {

    },
    2022: {

    }
}


I filled up the statistics for the rest of the seasons.
data = {
    2017: {
        "Mohamed Salah": {"goals": 44, "appearances": 52},
        "Roberto Firminho": {"goals": 27, "appearances": 54},
        "Sadio Mane": {"goals": 20, "appearances": 44},
        "Alex Oxlade-Chamberlain": {"goals": 5, "appearances": 42}
    },
    2018: {
        "Mohamed Salah": {"goals": 27, "appearances": 52},
        "Roberto Firminho": {"goals": 16, "appearances": 48},
        "Sadio Mane": {"goals": 26, "appearances": 50},
        "Alex Oxlade-Chamberlain": {"goals": 0, "appearances": 2}

    },
    2019: {
        "Mohamed Salah": {"goals": 23, "appearances": 48},
        "Roberto Firminho": {"goals": 12, "appearances": 52},
        "Sadio Mane": {"goals": 22, "appearances": 47},
        "Alex Oxlade-Chamberlain": {"goals": 8, "appearances": 43}

    },
    2020: {
        "Mohamed Salah": {"goals": 31, "appearances": 51},
        "Roberto Firminho": {"goals": 9, "appearances": 48},
        "Sadio Mane": {"goals": 16, "appearances": 48},
        "Alex Oxlade-Chamberlain": {"goals": 1, "appearances": 17},
        "Diogo Jota": {"goals": 13, "appearances": 30}

    },
    2021: {
        "Mohamed Salah": {"goals": 31, "appearances": 51},
        "Roberto Firminho": {"goals": 11, "appearances": 35},
        "Sadio Mane": {"goals": 23, "appearances": 51},
        "Alex Oxlade-Chamberlain": {"goals": 3, "appearances": 29},
        "Diogo Jota": {"goals": 21, "appearances": 55},
        "Luis Diaz": {"goals": 6, "appearances": 26}

    },
    2022: {
        "Mohamed Salah": {"goals": 30, "appearances": 51},
        "Roberto Firminho": {"goals": 13, "appearances": 35},
        "Alex Oxlade-Chamberlain": {"goals": 1, "appearances": 13},
        "Diogo Jota": {"goals": 7, "appearances": 28},
        "Luis Diaz": {"goals": 5, "appearances": 21}

    }
}


And now we're going to call this function, barChart().
    2022: {
        "Mohamed Salah": {"goals": 30, "appearances": 51},
        "Roberto Firminho": {"goals": 13, "appearances": 35},
        "Alex Oxlade-Chamberlain": {"goals": 1, "appearances": 13},
        "Diogo Jota": {"goals": 7, "appearances": 28},
        "Luis Diaz": {"goals": 5, "appearances": 21}
    }
}

barChart()


And then we create this function at the beginning of the code, after the part where we import the libraries. It has four parameters - labels, vals, season and stat. The first two are arrays and the latter two are strings.
import numpy as np
import matplotlib.pyplot as plt

def barChart(labels, vals, season, stat):

data = {
    2017: {
        "Mohamed Salah": {"goals": 44, "appearances": 52},
        "Roberto Firminho": {"goals": 27, "appearances": 54},
        "Sadio Mane": {"goals": 20, "appearances": 44},
        "Alex Oxlade-Chamberlain": {"goals": 5, "appearances": 42}
    },


So now that these are in place, let's prepare some values to pass into barChart() as arguments. For now, we assume that we want season 2018's goals. We obtain a list of player names by using the list() function.
    2022: {
        "Mohamed Salah": {"goals": 30, "appearances": 51},
        "Roberto Firminho": {"goals": 13, "appearances": 35},
        "Alex Oxlade-Chamberlain": {"goals": 1, "appearances": 13},
        "Diogo Jota": {"goals": 7, "appearances": 28},
        "Luis Diaz": {"goals": 5, "appearances": 21}
    }
}

players = list()

barChart()


We then pass in the element of data pointed to by 2018, and get its keys using the keys() method.
players = list(data[2018].keys())

barChart()


We do something similar for values, which is another list. Except we don't want the keys - we want the values.
players = list(data[2018].keys())
values = list(data[2018].values())

barChart()


Now, each element in values will be a dictionary with goals and appearances properties. We only want the goals. So declare the list, stats, and iterate through values using a For loop.
players = list(data[2018].keys())
values = list(data[2018].values())

stats = [];
for v in values:


barChart()


And then we use the append() function to add the goals property of the current element, v, into stats.
players = list(data[2018].keys())
values = list(data[2018].values())

stats = [];
for v in values:
    stats.append(v["goals"])

barChart()


And we finish up by putting in these arguments into barChart().
players = list(data[2018].keys())
values = list(data[2018].values())

stats = [];
for v in values:
    stats.append(v["goals"])

barChart(players, stats, "2018", "goals")


Now back to the barChart() function. Start by running the figure() method of plt, and setting the figsize argument to a value pair. This defines the width and height of the plot in inches.
def barChart(labels, vals, season, stat):
    plt.figure(figsize = (10, 5))
    
data = {
    2017: {
        "Mohamed Salah": {"goals": 44, "appearances": 52},
        "Roberto Firminho": {"goals": 27, "appearances": 54},
        "Sadio Mane": {"goals": 20, "appearances": 44},
        "Alex Oxlade-Chamberlain": {"goals": 5, "appearances": 42}
    },


Next, we use labels and vals in the bar() method. The last argument, color, is an RGB value. This is Liverpool FC, so we'll go with a brilliant red.
def barChart(labels, vals, season, stat):
    plt.figure(figsize = (10, 5))

    plt.bar(labels, vals, color=(1, 0.2, 0.2))


Finally, we use the show() method to display the chart.
def barChart(labels, vals, season, stat):
    plt.figure(figsize = (10, 5))

    plt.bar(labels, vals, color=(1, 0.2, 0.2))

    plt.show()


And there's the bar chart! With lovely red bars.


We haven't added titles for the bar chart. This is where the parameters season and stat come in. We use the xlabel() method and pass in the string "Players". Then we call the ylabel() method to use the stat parameter value. And finally, we call the title() method and use the season parameter value.
def barChart(labels, vals, season, stat):
    plt.figure(figsize = (10, 5))

    plt.bar(labels, vals, color=(1, 0.2, 0.2))

    plt.xlabel("Players")
    plt.ylabel("No. of " + stat)
    plt.title("Liverpool FC Player Stats for " + season)

    plt.show()


You can see where on the chart the text appears.


I want the values to appear at the top of the bars. For this, I have to iterate through vals using a For loop. And because I want the index value, I have to first use enumerate() on vals.
def barChart(labels, vals, season, stat):
    plt.figure(figsize = (10, 5))

    plt.bar(labels, vals, color=(1, 0.2, 0.2))

    for index, value in enumerate(vals):

    plt.xlabel("Players")
    plt.ylabel("No. of " + stat)
    plt.title("Liverpool FC Player Stats for " + season)
    plt.show()


In the For loop, we use the text() method to put the values in the chart. The first argument is the x value, the second is the y value, and the last is the value of the current element of vals, which we use the str() function on, to convert it to a string. For the first argument, index is reasonable because that's how the position of the bars are determined. For the second value, how high the text appears also depends on how high the bar is (because we want it on top) which in turn depends on the value of the current element of vals.
def barChart(labels, vals, season, stat):
    plt.figure(figsize = (10, 5))

    plt.bar(labels, vals, color=(1, 0.2, 0.2))

    for index, value in enumerate(vals):
        plt.text(index, value, str(value))

    plt.xlabel("Players")
    plt.ylabel("No. of " + stat)
    plt.title("Liverpool FC Player Stats for " + season)
    plt.show()


This needs cleaning up.


We want the text to be a bit higher than the bar, leave a bit of space between the text and the top of the bar.
for index, value in enumerate(vals):
    plt.text(index, value + 1, str(value))


Then we want the text to move left horizontally so that it appears right in the middle of the bar. This is tricky, but basically I use a formula to subtract from the value of index, based on the length of the string value. Therefore, we have to use the str() and len() functions here.
for index, value in enumerate(vals):
    plt.text(index - (len(str(value)) * 0.02), value + 1, str(value))


Much better. But now we have a problem at the top where the text overlaps the top.


Here, we use the ylim() method. We pass in 0 for the first argument, to tell Python that the lowest value to be displayed on the scale is 0. The next value defines the upper limit, and we want this to be a fair bit higher than the maximum value, so that there's space for the numbers. We use the max() function and pass in the vals array as an argument to get the biggest number, then add 5 to it.
def barChart(labels, vals, season, stat):
    plt.figure(figsize = (10, 5))

    plt.bar(labels, vals, color=(1, 0.2, 0.2))

    for index, value in enumerate(vals):
        plt.text(index - (len(str(value)) * 0.02), value + 1, str(value))

    plt.ylim(0, max(vals) + 5)

    plt.xlabel("Players")
    plt.ylabel("No. of " + stat)
    plt.title("Liverpool FC Player Stats for " + season)
    plt.show()


Looking good!


Next, we want to show the average value for this.. To that end, we use the axline() method. We define the y parameter using Numpy's nanmean() method, passing in the vals array as an argument. This will take the average of vals, excluding values that aren't numbers. Since we've pretty much made sure that vals is all numbers, this is a bit of overkill, but let's go with it.
plt.ylim(0, max(vals) + 5)
plt.axhline(y=np.nanmean(vals))

plt.xlabel("Players")
plt.ylabel("No. of " + stat)
plt.title("Liverpool FC Player Stats for " + season)
plt.show()


There it is. The default color is a deep blue, but you can simply pass in the color argument like we did for the bars, to customize this.


One final thing. You know how the player names can be a bit long. Well, to mitigate the problem, let's rotate the labels. We use the xticks() method and pass in the value of 90 to define the rotation.
plt.ylim(0, max(vals) + 5)
plt.xticks(rotation=90)
plt.axhline(y=np.nanmean(vals))


Well done!


There are more ways to customize this. Check out the documentation here.

Next

Querying the dataset and generating new charts.

Tuesday, 7 May 2024

Glassdoor has just become entirely too transparent

When looking to join an organization or a company, prospective jobseekers are supposed to do their due dilligence. This might involve asking around, searching on the internet, or simply consulting the web portal known as Glassdoor.

What, you may ask, is Glassdoor? In brief, it's a site that provides information about companies such as geographical locations, job roles, general remuneration and history. But what people tend to go to Glassdoor for, are reviews. Testimonials by employees both current and former.


Recently, Glassdoor made waves when they announced that reviewers would now have to fill in actual names on their profiles, whereas in the past they could submit reviews anonymously. They assured users that those names would not appear on reviews. 

Previously, reviews could be posted anonymously because user names were not added to profiles and only a working email address was required to create one. However, new users now need to provide real names in addition to email addresses, and existing users need to verify their accounts with real names.

Reactions from users

Users who had been providing anonymous reviews on customers, now found themselves in a position where their identities could be exposed. Now they were potentially open to retaliation if any of the companies found their reviews objectionable.

Glassdoor has reiterated its commitment to privacy, stating that names will not be exposed except with the express permission of the user. However, from the user's point of view, the best way to not have their identities exposed, is not to have those identities on file in the first place.

No longer anonymous!

After all, databases can be breached. Companies can be legally forced to provide information, regardless of any promises made to users. The only way for Glassdoor to guarantee that it will not divulge those names, would be for Glassdoor to not have the ability to divulge those names.

Companies that have suffered from anonymous scathing reviews of their employment practices, are presumably glad to no longer be vulnerable to anonymous criticism, either deserving or otherwise.

While it's true that the ability to review companies anonymously enables frank and candid critique, it's also sadly true that such anonymity can be abused to perpetuate vendettas. Thus, having that anonymity taken away, forcing reviewers to be accountable, can only be a good thing from a company's point of view. No more anonymous attacks by faceless cowards, dammit!

What I would do

Both employees and employers are barking up the wrong tree with this anonymity thing.

Obviously, I would never use Glassdoor reviews as a gauge as to whether or not I should join a company, the same way I would never read a movie review for the purpose of whether or not I should watch that movie. People vary wildly; a movie reviewer has different expectations about movies from myself. Similarly, other people reviewing these companies may view certain things very differently from myself.

For example, I once criticized my CTO for including profanities in the code base, which I found both juvenile and unprofessional. However, other techs I spoke to, told me they found it perfectly OK. Obviously, their thresholds of maturity and professionalism were significantly higher than mine.

Therefore, having a ton of glowing reviews on Glassdoor doesn't impress me. Conversely, neither do negative reviews. As far as I'm concerned, reading reviews on Glassdoor is a huge waste of time surpassed only by arguing with people on Social Media.

Cracking that glass door.

Also, if I wanted to get back at a company for treating me poorly, I wouldn't go to Glassdoor and write nasty anonymous reviews. If you want to retaliate using a platform like Glassdoor, writing nasty anonymous reviews is both unimaginative and in poor taste. It's been done to death.

I would go the other way. I would write overwhelmingly positive reviews, reviews so complimentary that they border on satire. I would highlight all the negative traits, and couch it in the most kiss-ass terms ever.

"This CTO drops F-bombs in his code base! He's not afraid of insulting his co-workers! That's so real! So ballsy!"

"The pay's a little low, but adversity builds character! The company treats you like family; what's a little more money compared to that?!"

"Even though I'm a software developer, this company continuously challenges me by asking me to fix faulty microwaves and screw in lightbulbs!"

No company can then accuse me of writing nasty reviews... and on the off-chance that any jobseekers actually use Glassdoor to help them make their decisions, this is going to make the company look like one of those losers that need to tell their interns to get onto Glassdoor and write positive reviews about them.

What if those users are dumb enough to be influenced by my fake overwhelmingly positive reviews? Oh, well. Some people need to learn the hard way, amirite? Also, just because I think a company is a shitshow doesn't mean I don't want anyone to join them. Au contraire, some companies exist for the express purpose of helping people experience how they don't want to be treated. Also, people who are this stupid should absolutely join companies that are this shitty. Consider it a public service.

That's just hypothetical, of course. If a company and I have parted ways, they no longer warrant that level of attention from myself, much less that amount of effort. Just saying; if you absolutely had to be petty, unbelievably positive reviews are the way to go.

Conclusion

Remember, Glassdoor is a business. Glassdoor needs to make money. If you're not paying Glassdoor, then Glassdoor needs to make money some other way. Your data is monetizable. Do the Math.

I wouldn't make a big deal of what Glassdoor have done. Glassdoor should never be that influential in your decision-making. Heck, just delete your Glassdoor profile. What are you losing, really?

Kiss my glass,
T___T

Friday, 3 May 2024

A Software Developer's Vacation in Penang

His Teochewness goes on vacation again!

This time, instead of the bustling Malaysian city of Kuala Lumpur, I visited some Clubhouse friends in the little city of George Town, Penang. How did this come to pass?

Street mural
in George Town.

Well, it all began when my sister-in-law came over to stay for the month of April. She shared the bed with my wife, while I crashed on the couch. On many levels, this was inconvenient. Even if you take away the fact that it was one of the hottest spells in Singapore weather, I work from home the majority of the time, and my work area just happens to be right beside the couch - which results in removing that one last (albeit artificial) barrier between personal and work life.

That's when I figured - hey, if I'm going to be sleeping on a couch, I might as well be sleeping in a hotel room. In another country. That's when I made the arrangements, bought the ticket and booked the hotel room. It happened to be a pretty busy time at work, but my boss was OK with it as long as I brought my MacBook along and remained contactable.

I would be going to George Town, a historic site in Penang. I envisioned myself taking long walks in the day and venturing into multiple museums and art galleries. Mrs TeochewThunder would probably hate this shit, but whatevs, she wasn't coming along. I could concentrate entirely on what I found fun, for a change.

April 14th, 2024

The day started off pretty well. I made it to Changi Airport early, had a nice breakfast and even found time to write some code I'd been tinkering with while waiting for my flight. The weather was blazing like Singapore's, just the way I like it.

I arrived at Penang International Airport ahead of schedule and soon after, I was picked up by a sweaty but dazzling young lady (she was pretty hot, in all senses of the word) in her car, and we greeted each other like friends who had only known each other online for the past couple years.

First meal at a
roadside coffeeshop.


Astonishingly fancy coffee.

After checking in to the run-down dodgy-looking hotel I'd picked out for my stay (the less said about this, the better), we headed off for some street food, where I got my mind blown by the Ice Yuen Yeung (coffee and tea, mixed) that turned out to be an ultra-fancy deconstruction of what I expected, served with a spiral glass stirrer. Holy shit.

Trust me, the other
stuff was weirder.

She then took me to George Town's Hin Bus Depot to attend what had to be the most macabre art exhibition I ever laid eyes on. The paintings looked like entries in some serial killer's fantasy. 

Bizarre.

At the same venue was a sculpture exhibition which was a nightmarish hellscape of squished body parts rendered in clay. Was I disturbed? That's an understatement. Was I fascinated? Also an understatement.

Something relatively normal.

We capped this off by attending a third exhibition which looked somewhat normal, and also easier on the eyes.

Nyonya dinner.

Dinner was at a Nyonya Restaurant where her mother joined us and we absolutely demolished a whole bunch of stuff. Her mom was excellent company, by the way. I took great pleasure in showing off the pictures of my papier-mache tree to her.

The night went well. The hotel itself wasn't much to shout about, but for the first time in weeks, I got to sleep in a proper bed!

April 15th, 2024

Got up bright and early. George Town exploration awaited! The sun was blazing overhead like it had something against humanity. No worse than the Singapore heat I'd left behind yesterday. In other words, business as usual for me. It was easy to tell that I was a tourist, despite my skin color fitting right in with the demographic - I was grinning like an idiot in the oppressive heat and obviously happy to be here.

Interior was
great, though.

I got some  breakfast at this place called English Hainan. Name sounded promising, but the Full English Breakfast I got was a bit of a disappointment. After breakfast, I wandered around, looking at various street art and the architecture of the buildings. It was like a replica of Singapore's Chinatown and Bugis Street, expanded by ten times to an entire district. Shophouses and temples abounded. I was getting a sense of how my grandparents lived in Singapore.

Food museum.

Pretty soon, I came upon this building, and decided that this was too good to pass up. A Food Museum! I've always loved miniature art, and this museum had it in spades. The amount of detail was incredible.

Great detail.


This was so interesting.

I could have spent another hour in here, but it was the first full day in George Town and there was plenty more to see. Further down the street was another tourist attraction advertised on Google Maps - Fort Cornwallis.

Gunpowder chamber.

Boom!

This was a gigantic bore, to be honest. It was nice and quiet, but this only served to highlight how few people seemed to really want to spend their time here.

Should've saved
my money.

A quick lunch later, I checked out an exhibition near the hotel that promised "Trick Art". This turned out to be a waste of time, possibly a bigger one than Fort Cornwallis, which is no mean feat. You've been warned.

Ssssss!

There was a pretty cool painting of a snake in a tunnel, but it was cool only compared to the rest of the uninspiring and insipid stuff in there. If anyone from the Trick Art Museum is reading this and feels triggered, tough titty. Do better.

That's a spread.

Dinner was great, though. I bought my friend and her mom a nice quiet seafood dinner in a pretty retro restaurant and had the place almost entirely to ourselves.

More food!

After that, we adjourned to another roadside joint where my friend introduced me to Penang Hokkien Mee and their local Otak-Otak. Mmmm!

Checking my email and work notifications that night, thankfully nothing seemed amiss. Would have been nicer if I didn't need to check, but oh well.

April 16th, 2024

More walking abounded! I got up bright and early, and had breakfast at one of those little roadside food courts.

Street art.

I especially liked
this one.

Then I made my way to Armenian Street, where a lot of the street art and art installations were congregated. Here, footfall was noticeably heavier as I was sharing the space with tourists and trishaw riders.

Why did I ever
go in there?!

One good exhibit.

Somewhere along the way, against my better judgement, I paid for a ticket for an entry into the Upside Down Museum. Much like the Trick Art Museum, this turned out to be pretty much a waste of time, with only a couple exhibits worth looking at. No more gimmick museums!

TeochewThunder
waz here.

I like to think I was a good sport about it, though. I paid for coffee and a postcard, and added my (fake) complimentary review to all the other (hopefully) genuine reviews stuck on their wall. I even stuck mine upside down! (hur hur) Might as well spread the love. Why should I be the only unfortunate soul to get suckered into parting with his money?

What followed was a slow saunter westwards to meet a couple Clubhouse friends at a cafe. As it turned out, I had severely overestimated the distance on Google Maps, and got there a full hour earlier than expected.

Much better.

It was lunch-time, so I stopped at White Chapel Cafe, and ordered their Full English Breakfast... for lunch. Decadent, eh?

A couple hours later, I met my friends at the other cafe. I remember the place being nice, but at this point I'd been in so many cafes that they were all starting to look the same.

The really exciting part of the day came when one of them gave me a ride to Komtar Tower, the tallest building in Penang. There, I could get to the topmost floor and view Penang from above. There was also a whole host of attractions in the theme park.

View was amazing...
and terrifying.

Up I went to the 65th storey, paying about RM 80 for the experience. This would include a trip outside of the perimeter of the top, tethered only by a safety harness. Honestly, just looking at the view from the glass floor made my stomach lurch. But I'd already paid, so what the hell...

...except that at this very moment, the ugly side of being the only software dev in the department kicked in. People from work called to tell me that one of our sites was down. And I just happened to be 65 storeys above ground, with my laptop in my hotel room. I made some calls and got people to patch some stuff until I could get to it proper. By the time I turned my attention back to the breathtaking view, the moment was well and truly lost. I went back down, and couldn't even muster up the enthusiasm to try out the attractions. Although, after having already been taken for a ride by Trick Art and Upside Down Museum, I wasn't really up for any more overpriced gimmicks anyway.

Tan Jetty, viewed
from the side.

The sun was going down in two hours. I was heading back to the hotel on foot, but this looked like a good time to visit the Clan Jetties of George Town. The air was thick with flies, but I made it out to the rickety planks that made up the Tan Jetty.

This was surreal, yo.

There was a little hut and what looked like an incense pot midway, then the rest pf the jetty led out to sea. It was surreal. I was on a flimsy piece of wood surrounded by the waves, and the odds of any stuff I had on me falling into the water (me included) were more than decent. With the evening sea breeze howling in my ear, even.

So this was how the Tans lived, in Penang, all those decades ago. My ancestral cousins.

Delicious, but
almost couldn't
finish it.

I didn't bother with the Chew or Lee Jetties. It was more of the same, and I had least shared a family name with the Tans. Instead, I headed back to the hotel and had a nice (and extremely filling) dinner at the Peranakan fusion restaurant nearby.

Cozy.

And that was it for the day. Somewhere before midnight, I got restless and took a little stroll around Little India and ended up at this cafe where the ultra-friendly staff served me cake and juice.

April 17th, 2024

The day started off with breakfast at a humble cofeeshop where I ordered a standard breakfast set - two soft-boiled eggs, kaya toast and coffee. To my pleasant and everlasting surprise, they not only broke the eggs for me and put them in a nice shallow cup, they even brought everything to my table! Well, damn.

Straits & Oriental Museum.

A trip to Penang Chinatown brought me to a Porcelain Museum. In contrast to the last two, this one was actually pretty educational.

Interesting pictures.

I totally didn't get
this one.

Just across the street was a small art gallery which seemed to be beckoning me. Not being one to resist a siren call like that, I ventured in and took some pictures.

Now, I'd been constantly walking all around George Town for the last few days, and the inner linings of my bootleg Converse shoes were worn out,. I thought it was time for a massage, so off I popped into this place called Revive Reflexology. I opted for the Thai package, hoping that these were "serious" masseurs. Unfortunately for me, the one I got turned out to be entirely too serious. I had underestimated the strain all that walking had put on my aging legs, and when she grasped hold of the ligament in my inner thigh near the left knee, I'm not ashamed to admit I screamed like a little bitch. And then she started on the right leg.

Suffice to say, I had walked in, and now I was crawling out. I took a lunch break at a nearby bar restaurant because I just didn't feel up to walking around to find something more interesting.

An elaborate
gateway.

My next stop was at the Pinang Peranakan Mansion which was nearby the hotel. I had passed by a couple times along with a whole host of big-ass Chinese temples. Through the past few days, I had come to realize that there were a lot of temples in George Town and I would need more than a couple days to cover them all, if I were even so inclined. But... there was only one Peranakan Mansion.

Check this out...

What followed was a feast for the eyes. Did I say feast? It was more I was pigging out on a visual buffet. It was a visual overload. There was so much detail in everything - elaborate carvings, designs, architectural features, old furniture and so on. Everything had a historical explanation.

My second favorite place was probably the kitchen, where I could pretty much imagine how a meal was cooked back in the day. But my absolute favorite was their Ancestral Hall. There was just so much going on there I can;t even begin to describe it. Check out the pictures!

Not quite a mansion, but
still interesting.

My next stop was, again, right next to the hotel where I visited The House of Yeap Chor Ee. Yeap was a Chinese immigrant who plied his trade in Penang as a barber (there were samples of his tools in the museum, quite a visual treat) and ended up being a very wealthy man despite not knowing how to read or write. Even his signature was written wrongly.

I was the only visitor in the place, and the guide obligingly took me on a tour of all three floors, explaining the history behind each exhibit. The tour cost RM 20. She didn't have change for my RM 50 bill, but I told her to keep it. It sounds like a sizeable tip, but honestly, after wasting time and money on Trick Art and Upside Down Museum, this was more than worth it.

The Last Supper... in Penang.

Later in the evening, I met up with a couple friends for a supper of some chicken wings, oyster omelette, fried dumplings and milo. And that was a wrap for my final night in Penang.

April 18th, 2024

Bittergourd with duck egg.

It was my final day here in George Town. I had some breakfast, wandered around, and checked out early. One of my friends from last night bought me lunch, and then dropped me off at the airport, where I headed home to Singapore... and back to the couch.

So long, Penang!

My little vacation there went pretty much as expected. A lot of walking in the hot weather, museums and art galleries. And food. And having to respond to work emergencies at the most awkward times possible. But hey, at least I got to sleep in a proper bed. And work on Python at night, along with some of my other little pet projects.

I suspect I've barely scratched the surface, though. At some point, I'll be back.

Your travelling dev,
T___T