Wednesday, 19 November 2025

Web Tutorial: Color Troll Meme

Time for a little fun. There's a meme I encountered years back, where it claims that certain illnesses affect the eyes, and there's a test for it. If you can see what you're reading, you probably don't have that condition. So you'll be tested for serious stuff like "tuberculosis", "stress" or what-have-you, and then there will be one you can't quite make out. When you finally do, it says something embarrassing like "erectile dysfunction".

Something like this!

So yeah, let's do something like that today. Totally randomize the text and colors. We'll use VueJS for this because... why the hell not, eh?

We're going to have some boilerplate HTML to start with. We have a div with the id ctmApp that will be Vue's placeholder. There's also a script remote link to VueJS.
<!DOCTYPE html>
<html>
  <head>
    <title>Color Troll Meme</title>

    <style>

    </style>
  </head>

  <body>
    <div id="ctmApp">

    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.18/vue.min.js"></script>

    <script>
  
    </script>
  </body>
</html>


Inside the script tag, We declare app as a call to Vue(). In it, we pass an object. In the classic VueJS pattern, the object will have el, data, methods and createdel will be set to ctmApp.
<script>
var app = new Vue
(
  {
    el: "#ctmApp",
    data:
    {

    },
    methods:
    {

    },
    created: function()
    {
   
    }
  }
);
</script>


In data, we want two arrays - ctms, inUse and the object wordTypes.
data:
{
  ctms:
  [
                          
  ],
  inUse:
  [

  ],
  wordTypes:
  {  

  }

},


ctms is an array of objects. You can have as many as you want, though I'd go with 9. Each object has name, color and textColor properties.
data:
{
  ctms:
  [
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    }    
                        
  ],
  inUse:
  [

  ],
  wordTypes:
  {  

  }
},


inUse is a placeholder array which defaults to empty. We won't need to worry about it yet. wordTypes is an objects that has area, condition, condition_prefix and condition_suffix arrays as properties. These arrays contain strings.
data:
{
  ctms:
  [
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    }                            
  ],
  inUse:
  [

  ],
  wordTypes:
  {  
    area:
    [

    ],
    condition:
    [

    ],
    condition_prefix:
    [

    ],
    condition_suffix:
    [

    ]

  }
},


I've filled these in. area is supposed to be embarrassing areas of the human body to develop medical conditions in. condition is an array of medical conditions that range from mild to nasty. condition_prefix and condition_suffix are descriptors that are supposed to go with the words in area.
data:
{
  ctms:
  [
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    }                            
  ],
  inUse:
  [

  ],
  wordTypes:
  {  
    area:
    [
      "testicle",
      "anus",
      "vagina",
      "nipple",
      "groin",
      "penis",
      "labia",
      "cleavage",
      "rectum"

    ],
    condition:
    [
      "dehydration",
      "constipation",
      "arthritis",
      "bronchitis",
      "myopia",
      "baldness",
      "pinkeye",
      "narcolepsy",
      "epilepsy",
      "insomnia",
      "dementia",
      "psychosis",
      "indigestion",
      "heartburn"

    ],
    condition_prefix:
    [
      "detached",
      "bruised",
      "inverted",
      "ingrown",
      "misaligned",
      "ruptured"

    ],
    condition_suffix:
    [
      "lacerations",
      "tumor",
      "ulcers",
      "blisters",
      "blockage",
      "fracture",
      "ulcers",
      "inflammation",
      "necrosis",
      "discharge",
      "dysfunction",
      "fungus"

    ]
  }
},


For the methods object, we will put in five methods - fillInfo(), getColor(), getCondition(), getOne() and getRandomNo(). In the created method, we will run the fillInfo() method.
methods:
{
  fillInfo: function()
  {
                          
  },
  getColor: function()
  {

  },
  getCondition: function()
  {

  },
  getOne: function()
  {

  },
  getRandomNo:function()
  {

  }

},
created: function()
{
  this.fillInfo();
}


In the HTML, add this in the ctmApp div. We use v-for to dictate that we want a div for each element in the ctms array, styled using the CSS class ctm.
<div id="ctmApp">
  <div class="ctm" v-for="ctm in ctms">

  </div>

</div>


In the CSS, add the CSS class for ctm. It's basically a deep grey circle.
<style>
  .ctm
  {
    width: 300px;
    height: 300px;
    float: left; 
    margin-left: 10%;
    margin-top: 10%;
    border-radius: 50%;
    background-color: rgb(10, 10, 10);
  }

</style>


You'll see 9 deep grey circles. Because I put 9 elements inside the ctms array. Depending on your screen size, you may need to scroll.


Let's work on the methods now. This is getRandomNo(). Give it a parameter, max. It should return a random number from 0 to max minus 1.
getRandomNo:function(max)
{
  return Math.floor(Math.random() * max);
}


For getOne(), we have the parameter, arr, which is an array. We return one of the elements of arr.
getOne: function(arr)
{
  return arr[];
},


Which one? Well, we let getRandomNo() decide.
getOne: function(arr)
{
  return arr[this.getRandomNo()];
},


But of course, we need to pass in the size of arr so that the random number generated is not out of bounds for arr.
getOne: function(arr)
{
  return arr[this.getRandomNo(arr.length)];
},


For getColor(), there is the parameter maxRGB. maxRGB is used to restrict how bright each color component is. In here, we declare r, g and b, and each one is set by using getRandomNo() to grab any number from 0 to maxRGB minus 1.
getColor: function(maxRGB)
{
  var r = this.getRandomNo(maxRGB);
  var b = this.getRandomNo(maxRGB);
  var g = this.getRandomNo(maxRGB);

},


Then we return an array containing these three values.
getColor: function(maxRGB)
{
  var r = this.getRandomNo(maxRGB);
  var b = this.getRandomNo(maxRGB);
  var g = this.getRandomNo(maxRGB);
  return [r, g, b];
},


And for the last small method, we have getCondition(). For this, we have the parameters area and prefix. Here, we assume that area has already been taken from the area array, and prefix is a Boolean that is true by default.
getCondition: function(area, prefix = true)
{

},


We declare cond as an empty string, then use an If block on prefix.
getCondition: function(area, prefix = true)
{
  var cond = "";

  if (prefix)
  {

  }
  else
  {

  }

},


If prefix is true, we set cond to a random element in condition_prefix using getOne(), and return a string of cond and area. If not, we set cond to a random element in condition_suffix, and return a string of area and cond.
getCondition: function(area, prefix = true)
{
  var cond = "";

  if (prefix)
  {
    cond = this.getOne(this.wordTypes.condition_prefix);
    return cond + " " + area;

  }
  else
  {
    cond = this.getOne(this.wordTypes.condition_suffix);
    return area + " " + cond;

  }
},


Now, time to work on fillInfo()! It will use all of the methods we've worked on up to now, either directly or otherwise. We begin by iterating through the ctms array.
fillInfo: function()
{
  this.ctms.forEach(
    (ctm, index) =>
    {

    }
  );  
                          
},


We declare isLast as a Boolean. If the current value of index is exactly the length of ctms minus 1, that means it's the last element. Then we have an If block based on isLast.
fillInfo: function()
{
  this.ctms.forEach(
    (ctm, index) =>
    {
      var isLast = (index === this.ctms.length - 1);

      if (isLast)
      {

      }
      else
      {

      }

    }
  );                            
},


We'll leave the first case along for now. For all other ctm objects in the ctms array, we set the name property to a random string from the condition array.
fillInfo: function()
{
  this.ctms.forEach(
    (ctm, index) =>
    {
      var isLast = (index === this.ctms.length - 1);

      if (isLast)
      {

      }
      else
      {
        ctm.name = this.getOne(this.wordTypes.condition);
      }
    }
  );                            
},


Now, here in the HTML, we have a h1 tag with the value of name.
<div class="ctm" v-for="ctm in ctms">
  <h1>{{ ctm.name }}</h1>
</div>


In the HTML, let's just set the h1 tag to have a certain size, center it, and so on. Most importantly, we set the color to a light grey so we can see the contrast.
<style>
  .ctm
  {
    width: 300px;
    height: 300px;
    float: left;
    margin-left: 10%;
    margin-top: 10%;
    border-radius: 50%;
    background-color: rgb(10, 10, 10);
  }

  .ctm h1
  {
    margin-top: 40%;
    font-size: 30px;
    text-align: center;
    color: rgb(100, 100, 100);
  }

</style>


Look at this! Conditions are populated, but "pinkeye" is repeated.


To fix this, check if the value of name is in the inUse array. And keep reassigning the name property until you get a value that isn't in the inUse array.
if (isLast)
{

}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }

}


Once that's done, push the value into the inUse array, so that this value can be one of the values compared against further down.
if (isLast)
{

}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }
  
  this.inUse.push(ctm.name);
}


Now you'll see no repeats! Bear in mind that this only works as long as you have less elements in the ctms array (OK, not counting the final element) than there are elements in the condition array. Otherwise you're going to get an infinite loop.


Now to handle the condition for the last element in ctms. Declare prefix as a Boolean, and use getRandomNo() with a argument of 2, to get a random number between 0 and 1. Then declare area, and use getOne() to obtain a random value from the area array, as its value.
if (isLast)
{
  var prefix = (this.getRandomNo(2) === 0);
  var area = this.getOne(this.wordTypes.area);

}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }
  
  this.inUse.push(ctm.name);
}


Then set the name property by using getCondition() and passing in area and prefix as arguments.
if (isLast)
{
  var prefix = (this.getRandomNo(2) === 0);
  var area = this.getOne(this.wordTypes.area);
  ctm.name = this.getCondition(area, prefix);
}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }
  
  this.inUse.push(ctm.name);
}


See? The very last circle at the bottom right now reads "misaligned rectum"!


Let's handle colors now. After the If block, declare colorArr as the array returned from calling the getColor() method. Pass in 100 as an argument so that the RGB values won't be greater than 100. That means it'll be a somewhat dark color. After that, formulate a CSS rgb() string based on colorArr's elements, and assign the value to the color property of the current element of ctms.
if (isLast)
{
  var prefix = (this.getRandomNo(2) === 0);
  var area = this.getOne(this.wordTypes.area);
  ctm.name = this.getCondition(area, prefix);
}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }
  
  this.inUse.push(ctm.name);
}

var colorArr = this.getColor(100);
ctm.color = "rgb(" + colorArr[0] + "," + colorArr[1] + "," + colorArr[2] + ")";


Bake this value into the div's style attribute.
<div class="ctm" v-for="ctm in ctms" style="background-color:{{ ctm.color }}">
  <h1>{{ ctm.name }}</h1>
</div>


You can see that now the circles are colored different. Each one is a different color.


Now for the text! Each circle's text should have the same color as its div, but several shades brighter. Remember we capped the RGB values at 100 earlier? That was deliberate so we could have a buffer between 100 and 255, the actual maximim RGB value. Now declare opacity and set it to 1. Then set the textColor property to what we previously set color as, except that we use rgba() instead and use opacity as the last argument.
var colorArr = this.getColor(100);
ctm.color = "rgb(" + colorArr[0] + "," + colorArr[1] + "," + colorArr[2] + ")";

var opacity = 1;
ctm.textColor = "rgba(" + colorArr[0] + "," + colorArr[1] + "," + colorArr[2] + "," + opacity + ")";


For each value (except opacity), we add 150. This makes the text color much brighter, but keeps them at the same proportions.
var colorArr = this.getColor(100);
ctm.color = "rgb(" + colorArr[0] + "," + colorArr[1] + "," + colorArr[2] + ")";

var opacity = 1;
ctm.textColor = "rgba(" + (colorArr[0] + 150) + "," + (colorArr[1] + 150) + "," + (colorArr[2] + 150) + "," + opacity + ")";


Now bake the color of the text into the h1 tag.
<div class="ctm" v-for="ctm in ctms" style="background-color:{{ ctm.color }}">
  <h1 style="color:{{ ctm.textColor }}">{{ ctm.name }}</h1>
</div>


There you go. You can see all the text colors are basically brighter versions of their backgrounds.


Finally, the opacity!

Remember the variable opacity? It's supposed to be very low for the final div, and varying higher levels for the others. The idea here is that the more clearly you can see the text, the less likely that you have that condition. So for the last medical condition, it's super unclear and when the user can finally read it, it says something embarrassing. That's the joke!

So here, if isLast is true, we set opacity at a really low value, such as 0.05.
var opacity = (isLast ? 0.05 : );


And if not, we get a random number from 0 to 7.
var opacity = (isLast ? 0.05 : this.getRandomNo(8));


We add 2, so we get a number from 2 to 9.
var opacity = (isLast ? 0.05 : this.getRandomNo(8) + 2);


Then we divide by 10, so the final result is between 0.2 to 0.9.
var opacity = (isLast ? 0.05 : (this.getRandomNo(8) + 2) / 10);


See that everything else is relatively easy to read, but the last one is almost invisible! It says "ingrown rectum", which sounds painful.


Have fun with this. Refresh. See what results you get. Add more to the arrays!

Better go see a doctor for that testicle necrosis!
T___T

Wednesday, 12 November 2025

Spot The Bug: The Underperforming Chart Scale

Hey, hey! Spot The Bug is back in town, and we're rolling out a new bug-fix story!

Can't wait to
squash another bug.

Working with D3 is one of my favorite pasttimes, especially when I have a new dataset to play with. However, there are some gotchas, and today's episode of Spot The Bug is a story of how one of these gotchas, well, got me.

I had some data that I wanted to plot a chart for. I truncated it a bit for brevity, but it will serve to illustrate the point. Note in particular, the column val.

datavals.csv
date,val
2024-11-01,8
2024-11-02,2
2024-11-03,17
2024-11-04,11
2024-11-05,25
2024-11-06,4
2024-11-07,7
2024-11-08,3
2024-11-09,8
2024-11-10,15


Here's the code I wrote to create an y-axis
<!DOCTYPE html>
<html>
  <head>
    <title>Chart</title>

    <style>
      svg
      {
        width: 800px;
        height: 500px;
      }
      
      .scaleTick, .scale
      {
        stroke: rgba(100, 100, 100, 1);
        stroke-width: 1px;
      }
      
      .scaleText
      {
        font: 8px verdana;
        fill: rgba(100, 100, 100, 1);
        text-anchor: end;
      }
    </style>

    <script src="https://d3js.org/d3.v4.min.js"></script>
  </head>

  <body>
    <svg>

    </svg>

    <script>
      d3.csv("datavals.csv", function(data)
      {
        var dataVals = [];

       for (var i = 0; i < data.length; i++)
       {
         dataVals.push(data[i].val);
       }

       var maxVal = d3.max(dataVals);

        var chart = d3.select("svg");
        chart.html("");

        chart
     .append("line")
     .attr("class", "scale")
     .attr("x1", "50px")
     .attr("y1", "50px")
     .attr("x2", "50px")
     .attr("y2", "450px");
    
        var pxPerUnit = Math.floor(400 / maxVal);
    
        var scale = [];
        for (var i = 450; i >= 50; i -= pxPerUnit)
        {
          scale.push(i);
        }
    
     chart.selectAll("line.scaleTick")
     .data(scale)
     .enter()
     .append("line")
     .attr("class", "scaleTick")
     .attr("x1", "40px")
     .attr("y1", function(d)
     {
     return d + "px";
     })
     .attr("x2", "50px")
     .attr("y2", function(d)
     {
     return d + "px";
     });
    
     chart.selectAll("text.scaleText")
     .data(scale)
     .enter()
     .append("text")
     .attr("class", "scaleText")
     .attr("x", "30px")
     .attr("y", function(d)
     {
     return d + "px";
     })
     .text(
     function(d, i)
     {
     return i;
     });     
      });
    </script>
  </body>
</html>

Here, I got a scale. However, something was wrong. The largest value in my dataset was 25. Why was the largest value here 8?


What Went Wrong

My first clue was here, near the top of the script where I declared maxVal. Because according to this line, max() was returning 8 as the largest value.
var maxVal = d3.max(dataVals);


This would explain why the scale started with the value 8 and worked its way down from there.

Why It Went Wrong

If the largest value in the dataVals array was 25 and the script thought the largest value was 8, there was one explanation for that. The script was comparing alphabetically rather than numerically. As text, "8" is always larger than "25". Basically, the dataVals array was being filled with strings rather than integers from the CSV file.
var dataVals = [];

for (var i = 0; i < data.length; i++)
{
  dataVals.push(data[i].val);
}

How I Fixed It

I used the parseInt() function to convert the data before pushing it into the dataVals array.
var dataVals = [];

for (var i = 0; i < data.length; i++)
{
  dataVals.push(parseInt(data[i].val));
}


And now it took 25 as the largest value! Because now dataVals was filled with integers rather than text, and it was comparing accordingly.


Moral of the Story

You'd think I would have learned to sanitize these things by now, especially in JavaScript, or any loosely-typed language. Apparently not!

Stay int-tellectual,
T___T

Thursday, 6 November 2025

Why Your Database Needs Audit Fields

In most relational database schemas, there exists a convention where timestamps and text strings are stored to record when a row was last inserted or updated. These are created in columns, and these columns are commonly referred to as Audit Fields.

Time for an audit!

Take for example this schema in MySQL, for the table Members.
CREATE TABLE Members (
    "id" INT NOT NULL AUTO_INCREMENT,
    "name" VARCHAR(100) NOT NULL,
    "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "createdBy" VARCHAR(50) NOT NULL DEFAULT "system",
    "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    "updatedBy" VARCHAR(50) NOT NULL DEFAULT "system"
);


The last four fields are Audit Fields:
- createdAt: this is a timestamp that is set to the current date and time when the record is created, and never changed.
- createdBy: this is a string that is set to the user's login when the record is created, and never changed.
- updatedAt: this is a timestamp that is set to the current date and time when the record is created, and changed to the current date and time every time the record is updated.
- updatedBy: this is a string that is set to the user's login when the record is created, and changed to the current user login every time the record is updated.

How do Audit Fields work?

"Created" fields. These are less useful because they should never be updated, and serve as a static record of when the record was created, and by who. However, it's good to have because it provides an initial reference point to troubleshoot if needed.
INSERT INTO Members (name)
VALUES ("Ally Gator");


id name createdAt createdBy updatedAt updatedBy
1 Ally Gator 2024-05-05 16:05:42 admin1 2024-05-05 16:05:42 admin1


"Updated" fields. These are updated when the record is created and every time the record is updated. While it's not as useful as a full audit log, it at least lets you know when the last update was. Let's say you run this query.
UPDATE Members SET name = "Allie Gator" WHERE id = 1;


id name createdAt createdBy updatedAt updatedBy
1 Allie Gator 2024-05-05 16:05:42 admin1 2024-05-16 12:27:21 admin1


Why Audit fields?

At the risk of stating the obvious, these are useful when you need to perform an audit (hence the name) on a database as to what records were created or updated, when, and by who. The importance of this only becomes increasingly obvious the larger the database grows.

Audit Fields can be a pain to set up, especially if you're not used to it. Once you've done it enough, though, you may find yourself wondering how you ever managed without them. Yes, they take up space. Yes, you have to bear them in mind during INSERT and UPDATE operations.

But man, they're awfully useful.

Examining data.

Imagine you need to do some forensics on some data that got added out of nowhere. You can check the timestamps and the ids of whoever created them. If they don't match any user on record, you know you have a problem. Scratch that - there's (almost) always a problem, but at least you have a better idea where it's coming from.

Or even if it's not a security breach, perhaps there's a dispute as to who was responsible for a certain data update? If the timestamps and user ids are right there in the database, there's instant accountability.

And all this is not even considering the fact that depending on the prevailing laws of the land, the presence of Audit Fields may even be mandatory.

Ultimately...

Get comfortable with the concept of Audit Fields. It's not new. It's pretty much timeless, in fact. It always represents some extra work at the start, but will save you so much hassle in the long run.

See you later, Allie Gator!
T___T

Saturday, 1 November 2025

The year I finally achieved that JLPT certification

Some say "better late than never". I got a huge dose of that this year as I obtained my Japanese Language Proficiency Test (JLPT) N5 Certificate.

Why was this overdue? Well, you see, because I actually began on this journey at the tender age of 15. I was a Polytechnic student at the time, and this was a supplementary module. At first, things went well. Having received education in Chinese, learning the basic sounds of Japanese was a breeze. Learning the Hiragana and Katakana writing systems wasn't much of a stretch as well; in fact it was significantly easier than reading and writing Chinese characters. Until we got to Kanji, which essentially was Chinese characters.

Ugh, more Chinese.

At which point I lost all interest.

Also, at this time, I had begun an obsession with writing code, and building stuff. Japanese was a pretty language, but it just didn't fit into my world the same way C++, SQL or even QBasic, did.

And the rest, as they say, is history. Even after graduation, not only did I neglect whatever little Japanese I had learned, the next few decades were a disastrous collection of bad career choices, even worse lifestyle choices, and sobering life lessons.

COVID-19 and Clubhouse

Fast-forward to 2021. COVID-19 had the world in its loathsome grip and I, like many others, sought human interaction online. That was when I discovered the Clubhouse app. Through it, I encountered numerous communities, not all of which spoke English, Mandarin, or even Cantonese.

A few of these were Japanese speakers. And some were even offering Japanese lessons. I dove right in, relearning all the basics I had forgotten. Many times, I would simply listen in, try to make sense of the conversation, and Google words that came up. Then I started engaging, hesitantly, in the conversations. I even participated in Mandarin-Japanese exchanges!

Little cultural exchanges.

It was slow going. I wrote an app to facilitate relearning Hiragana and Katakana. I practiced writing every day, religiously. But there was this feeling that I could do more.

That was when someone suggested that I take the JLPT. And from that day forth, I took things up a notch.

Training in earnest

My next step was to install Duolingo on my phone. For the next few years, I faithfully partook of the exercises daily. At the same time, I went onto YouTube to search for JLPT Listening exercises. I didn't really have a sense of how far I had progressed; I just knew I needed to keep going. I've never been brilliant; but what I am good at is being consistent.

One year later, I downloaded the Migii app, and paid the subscription fees for accessing sample JLPT exam questions. With Duolingo, the returns were diminishing. I needed to train a different set of linguistic muscles - the kind used for passing the exam. Duolingo had brought me to a certain point where I could read the Japanese exam questions without too much difficulty... now I had to practice answering them.

The next milestone was hit another year later when, while watching a Japanese TV show, with English subtitles, I noticed something odd. Almost constantly, the subtitles would read "It's OK". Even though "It's OK" was a perfectly reasonable thing to say in the context of the current point of the story, it was a pretty poor translation.

Phrase Literal Meaning English Translation
daijobu It's OK. "It's OK."
dou itashimashite You're welcome. "It's OK."
shinpai suna Don't worry. "It's OK."
anshin shite Relax. "It's OK."
ki ni shinai de Pay it no mind. "It's OK."


At this point, I found myself thinking - wow, these translators are so goddamn lazy! They just use "It's OK" for everything!

And my next thought was - hey, if I know this much, shouldn't I be taking the JLPT already?!

The JLPT Test Voucher

That was when I registered for the JLPT on the Japanese Cultural Society of Singapore website. and paid a hundred Singapore dollars for the privilege. A month later, the JLPT test voucher arrived in the mail. The train was in motion; there was no going back.

Meanwhile, I continued training religiously. With Listening practice, especially. Online chatter had it that the recordings would be played only once, so I trained myself similarly, by not rewinding and replaying.

The JLPT N5 Exam

It was July when the day arrived. I head for the Singapore Management Institute where the exam was held, and stood in line with what looked like mostly Burmese folks. Interesting.

The invigilator who took charge of the exam room I had been assigned to, seemed to be Japanese going by the way she spoke English.

Shading answers with a pencil.

The proceedings were charmingly old-school. We were given question and answer sheets. It was all multiple-choice, and the correct answer had to be shaded with a pencil for feeding into a scanning machine. Despite my numerous certificates, I hadn't done this since... 2015? This was because my last couple certifications were earned from doing the coursework and presentations, rather than standardized tests.

The toughest moment in the exam came from the Choukai, which was the Listening portion. Despite my best efforts, my concentration slipped at various points. It was with considerable relief that I handed in my question and answer sheets, and headed off.

In August, I logged on to the JLPT portal to check my results. I had passed, and my results were more than decent. I actually scored higher on Listening than other sections!

The real value of all this

Last September, I got my actual physical certification through the mail. It was a foregone conclusion by that time, but I still felt that little thrill of pride. Job well done, bro, I told my reflection. Not such a big deal in the larger picture, but we've got to celebrate our wins even if they're small. Especially if said win took thirty-three friggin' years to achieve.

My results.

The JLPT N5 Certification isn't going to change my life. As far as professional cred goes, it's barely a blip. Achieving the JLPT N5 Certification probably puts me on par with the average Japanese toddler where the language is concerned. As for the value of understanding multiple languages? That's not much of a flex. This is Southeast Asia; just about everyone and their dog is multilingual.

No; at my age, the act of learning is arguably more important than what's being learned. It helps stave off dementia.

The real value is knowing and affirming, that with sufficient motivation and putting in the time and effort, I can learn pretty much whatever I choose to learn. And that is powerful stuff. In an age where things are constantly and rapidly evolving, the ability to learn shit has become vital - not just in the tech industry, but for life itself.

In hindsight, I should have realized this. How did I learn ASP? PHP? Ruby? VueJS? D3? All by picking up a book, watching videos, visiting websites, and constant practice. Most of the time, it really is that simple. The methods vary, but at the end of the day, it's about the willingness to put in the hours.

The linguistic journey continues!

I do want to see if I can achieve the next rung, the JLPT N4 Certification. Again, I'm not really sure why. Just for the hell of it, I guess. It's not like I plan to visit Japan. It's not like I realistically have anything to prove to anyone.

On the other hand, Korean does seem pretty interesting.

Decisions, decisions...

Ganbarimashoo!
T___T