Sunday 7 July 2024

Great Expectations and how to manage them

Ever have this problem with expectations? Unreasonable, illogical expectations, to be exact.

It's almost inevitable if you are a developer working for non-tech people. Having little to no experience with the process of software development, they have no idea what constitutes a reasonable expectation, and let their imagination fill in the blanks. With amusing, and sometimes nightmarish, results. An (admittedly extreme) example would be the employer assuming you can fix the microwave simply because you know how to write software.

Fix this!

That's not to say experienced tech people don't have unreasonable expectations either. Their expectations tend to be a little less fanciful, but no less impractical when it comes right down to it. An example would be the employer expecting you, as an employee, to match his devotion to the company he created.

Then there are the in-betweeners. The ones who know a bit of programming, just enough to be problematic, but aren't exactly technical. Because they know how to code, they have expectations of how quickly code can be churned out. Unfortunately, they do not have experience with the less fun aspects of software development, such as best practices, testing and optimization. Therefore those expectations, too, can be divorced from reality.

People are like animals, in the sense that they need to be trained. Some might argue that humans have intelligence superior to animals. Well, that's great. That just makes it easier to train them. Make no mistake; where you are concerned, how people treat you is ultimately how you train them to treat you.

This kind of training tends to take two forms.

Don't give them what they want

And by that, I also mean stop being so obliging. When you satisfy an unreasonable request, it sets a new bar for expectations. And then it becomes the norm. This ultimately means that you're setting yourself up for increasing and unnecessary hardship.

If you get texts at night long after you're supposed to be off for the day, do not respond until the next day. Or at least, don't respond every single time. Or make them wait. Either way, make them expect that if they contact you at a certain time, they're not going to get a response right away, if at all.

If you're tasked to do something that is radically out of your job scope, make them expect you to fail. Sometimes spectacularly.

I've been known to work on weekends and public holidays. However, if my employers explicitly tell me that they expect me to use my weekends for work, I make damn sure I'll be doing something else during the weekend. It's the principle of the thing. It's not that I have a problem with working on weekends; I just have a problem with it being my employer's expectation. And, as an employee, I consider it my ethical responsibility to disabuse my employers, either in word or deed, of such expectations.

Gone fishing.

If the expectation is impossible, don't bust your ass trying to do it. Make sure you do the job that you're paid to do, and then maybe work on the impossible requirement. This sets the tone; you fulfil your professional obligations - no two ways about it, that's your first and foremost priority - and prevents expectations from getting too high. Once people see that they're not going to get what they asked for, but they are absolutely getting what they paid for, expectations get realigned. This, of course, assumes that you're dealing with reasonable people. In the corporate world, you don't always have that luxury. An alarming percentage of the time, you might end up looking for a new job.

Also, can't stress this enough, communication. Don't just say "yes" and then not do it - that's passive-aggressive bullshit and you should hold yourself to higher standards. Let them know it can't be done, why it can't be done, and what would have to give in order for you to carry it out.

Give them exactly what they want

Or what they say they want. People tend to tell you what they want without thinking it through. In order to be helpful, we should at least give them fair warning that what they want comes with a whole lot of caveats. And if that doesn't work, give them a preview of what fulfilling their wishes looks like.

And if that's still not enough, let them have it.

Now, just to be clear, I'm not advocating Malicious Compliance. It's one of the top fantasies of the average disgruntled employee (and understandably so!) but we're professionals. And as professionals, it is our duty to point out the pitfalls and advise accordingly. However, while we may be professionals, we're not superheroes. We can't save people from themselves. Thus, if an ill-advised route is insisted upon, short of it being illegal, give them what they want, and let the cards fall where they may.

They insist on not encrypting passwords and storing them in cleartext? Hey, go for it. But make sure the decision is reversible.

They insist on adding a new software feature that would compromise the functionality of existing features? Knock yourself out. Again, make sure you have a backup.

But if you go this route, document your objections. Obsessively. There may come a day when you need it. Also, plan your exit. People generally don't like being reminded of their failure, and your continued presence may be just such a reminder.

Here's a personal example. There was a time Mom had this unfortunate habit of calling me, and when I was otherwise occupied and didn't pick up, she would leave a string of missed calls on my phone. And then when I finally called her back, I would get an earful for not calling back right away. The kicker was, the body of her message was never anything that required immediate attention, such as an emergency. Like the house being on fire, or someone being sent to the hospital. No, the message would be something alone the lines of reminding me that Dad's birthday was next week, or asking if my refrigerator had any space for some extra apples she had picked up.

Missed calls.

Now, I love apples, but that's neither here nor there. I had to get her off this expectation that I had to be instantly responsive at all times regardless of the situation. The next time she called, I was walking alongside a busy road, and I had an idea. I picked up, and let her hear the blaring horns of irate road-users. She asked what the hell was going on there, and I innocently replied that I'd been in the process of crossing the road, and inquired what was the matter. In effect, I had given her a preview of just what could happen if she got what she wanted.

I wouldn't go so far as to say she never called me on my phone again, but each time she called, she had to weigh the contents of her message against the possibility of me getting run over by traffic just because I mindlessly picked up the phone as she demanded. The result was that she got into the habit of leaving messages on WhatsApp instead. You may think I'm a real asshole for scaring my own mother like that, but there was no lie in anything I did. There was a real possibility of something horrible happening if she got her way, and this was not a message that could be effectively delivered by me just telling her, since Asian parents, y'know, tend not to take their kids seriously.

On the flip side...

All this only works if you're competent at what you do, and you deliver what you're actually paid to do. It doesn't mean that just because you're bad at what you do, unreasonable requests become any less unreasonable. But being good at your actual job would help your case a lot more.

Expecting your response,
T___T

Tuesday 2 July 2024

The time is now, or at least, soon

Procrastination can be a beautiful thing. It staves off burnout, promotes peace of mind, slows life down to a manageable pace. And yet, taken too far, it can lead to regrets. Unfulfilled aspirations. Because time marches on, and doesn't come back. Opportunities are like origami boats floating along the river.

Like origami boats
in the water.

One of the challenges in life, in or outside the workplace, is to identify what things can be reasonably put off for another time, and what things would better serve you being done now. As in, now, pronto. Or at least, given a concrete timeline to begin.

For example, if you want to make a lifestyle change (Diet? Exercise?) putting it off indefinitely is a poor choice. Putting it off to next Monday or even the start of next month, however, can be justified on the grounds that it makes progress easier to track.

On the other hand, such justifications don't always pan out because, again, time marches on, and opportunities are lost.

A couple years back, I received a pay raise. It wasn't much; I was left with just over a hundred dollars extra after deductibles. I had the idea of giving the extra to my mother; after all, my pay had been going up the past couple years and she hadn't really benefitted from it. Then I looked at the pitiful amount and felt embarassed. Wouldn't it make more sense to wait another year, and if my pay went up again, put it all together to give her a more hefty income boost?

Running out of
time.

No. No, it wouldn't. The thing is, I was in my mid-forties and Mom was almost seventy. What if I waited another year and she croaked in the meantime? Sure, that hundred bucks wasn't going to make much of a splash. But I had given her to understand that whatever I got, we would share it. And it was time to deliver. The time was now. Or possibly never.

Even further back, in the year 2019, I was doing my usual annual appraisal of my current work situation and seeing if there were other opportunities out there. Part of me wanted to kick back and extend my contract by another year, and the other part insisted on at least attending a few interviews to see what was what.

Then COVID-19 happened. People got laid off left and right - generally I mean, not the software industry specifically. I decided that terrible though the entire thing was, it was an excellent opportunity. If I accepted that new job offer and it didn't work out, no one was going to question me not having a job, when just about the entire island was facing pretty much the same problem (though for different reasons). But I had to make the decision now. Not next three days, not next week. Now.

When COVID-19 wrecked
the world.

Spoiler alert: the new job didn't work out, but the resultant scenario happened just as I thought it would. I attended job interviews, and no one batted an eyelash when they saw I had that gap in my resume. I had successfully identified an opportunity in the midst of a crisis and taken advantage of it.

There have been several examples of this throughout my life, where I had to go do something. Before, it was a matter of finding a reason to do it; but at some point, it became a matter of finding a reason not to.

Take this blog, for example. When I started it back in 2014, did I wait till I was older and wiser and more professionally experienced before airing my views on a tech blog, for fear of embarrassing myself? No, I got off my ass and did it, public opinion be damned. It's now the tenth year I've been doing this. Has this blog become a resounding success? Also no. But it's a heck of a lot more successful than the blogs that never got started. And I've learned so much from doing this.

Just went out
there and started
this blog.

The "Now or Never" philosphy has been the driving force behind every new programming language I've learned, every tech platform I've tried. Every post-graduate Diploma course I've signed up for. All because I knew there would come a day my body just couldn't handle the rigors of studying while working any more.

Next time? There might not be a next time. And if I want to put things off, I have to be OK with that possibility.

The Time Is Now!

Inaction is a choice like any other, but bear in mind that all choices have an associated cost.

There's no need to jump into anything, of course. However, before deciding to do nothing, you might want to consider what you're missing out on, and if it's really worth it.

No time like the present.
T___T

Friday 28 June 2024

Remote workers in Dell Technologies stand firm

It was with a fair amount of interest that I read this article during the week. Computer company Dell Technologies, like many companies in the USA, has been trying to enforce a return to office for their workers, with dismal results.



During the worldwide crisis that was COVID-19, Dell mandated a remote work policy which their employees resisted at first, but gradually grew accustomed to. And now that COVID-19 is no longer life-threatening (at least for the general public), Dell has tried an about-face on this by demanding that employees return to the office.

Sound familiar?

If you're thinking that you've heard all this before, you probably have. This is a story that has been repeated over and over since 2021, not just in the USA but all over the world. Employers like Elon Musk have even stamped their metaphorical feet and opined that chosing to work from home is "immoral", which only serves to enforce my belief that being worth millions of dollars doesn't mean you're less prone to saying stupid shit.

Going to remote work meant that many employees had made certain commitments in terms of housing location and lifestyle. A return to office would disrupt everything, and incur certain costs such as transport, food and makeup. Nice shoes. The works. Those were just financial costs; there were also costs with regard to time, such as the daily commute.

Traffic is crazy.

Can we reasonably blame workers for not wanting to go through yet another drastic lifestyle change after going all in the first time? Should we accuse them all of being lazy and unmotivated or worse, actively trying to take advantage of the company? Those bad actors do exist, but does their existence justify tarring all remote workers with the same brush? Or is that just a convenient excuse?

Also, a company known for making computers portable and providing network access to those computers so that they can be used from anywhere, is insisting on a return to office? How's that for an unsubtle dose of irony?

Standoff

What's interesting is that Dell stated that those who opted to remain remote would no longer be considered for promotion... and a full fifty percent of remote employees didn't blink. The sentiment appears to be that the threat of not being promoted was an empty one, anyway, as opportunities for promotion had been scarce even before this. Their defiance seemed to state that even if there was a genuine chance of getting promoted had they chosen to acquiesce, promotion just wasn't worth making all those changes. The carrot wasn't working.

A chance at promotion?

Of course, the stick would soon follow. Or, if you consider the aforementioned "carrot" actually more of a stick, an even bigger stick would inevitably follow. The implication being that in the case of layoffs or company restructuring, remote workers would be the first on the chopping block.

Who's winning?

Well, arguably, the fact that this story is out in the public in the first place, means that Dell as a company has lost. And if Management goes to war with its own employees over this, Dell's reputation as a workplace can only fall further.

Resentment at Dell's machinations has caused employees to not only remain remote, but also to actively seek out new employment regardless of what Dell eventually decides. There's also the sneaky feeling all around, that Dell Management knows exactly what they're doing. After all, if those opting to stay remote really do leave, this eliminates the cost of compensating them if indeed layoffs do occur. To be fair to Dell, this tactic is hardly illegal. If we're being honest, it's not even exactly original.

Dell's tactics at this point in time would seem to be directly contradictory to the stand articulated by CEO Michael Dell less than two years ago. This is an excerpt from his blog, where he waxed lyrical about Dell's inclusive work culture.
But from my experience, if you are counting on forced hours spent in a traditional office to create collaboration and provide a feeling of belonging within your organization, you’re doing it wrong.

Technology’s ability to create a do-anything-from-anywhere world, where work is an outcome rather than a place or time, also enables you to create a strong corporate culture anywhere, all the time.


And another...
Ultimately, we have committed to allow team members around the globe to choose the work style that best fits their lifestyle - whether that is remote or in an office or a blend of the two. We are redesigning office spaces for the purpose of bringing teams together for social connection and collaboration to enhance hybrid experiences.

These decisions are grounded in our culture and based on the facts of our internal data. It is a philosophy of flexibility, choice, and connection.


Companies making a u-turn from their previous stands isn't at all new, of course... but a u-turn after that extravagant song and dance about "flexibility, choice, and connection"? Unless shame is a completely foreign concept to you, that's nothing short of an utter embarrassment.

Abrupt reversal in direction.

If we give Dell the benefit of the doubt, Dell may have been all about remote work in the beginning, but perhaps that sentiment was genuine until the tides of reality hit and they realized that they simply weren't that well-equipped for it. Perhaps this was an honest mistake.

Or perhaps, more cynically, it was just corporate gaslighting. Perhaps those words by Michael Dell were merely meant to sway workers to adopt remote work due to circumstances caused by COVID-19... and now that the pandemic is over, there's no longer any need for that pretense.

All in all...

As a salaried worker and an economic digit, my heart is with the employees of Dell no matter what their ultimate decisions are. This stalemate cannot end well; eventually the pendulum will have to swing one way or another. Only time will tell if Dell Technologies, as a company, can survive.

I would like to conclude by saying that I've never been a big fan of the USA. There's precious little about American culture that I admire, that I would choose over Asian culture. But now here's one thing, at least. I appreciate the way these workers at Dell have managed to prioritize the quality of their lives over corporate promotions. That's something that we in Southeast Asia, with our obsessive hustling and pointless flexing of empty job titles, can emulate.

What the Dell?!
T___T

Monday 24 June 2024

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

It's time to render the line chart!

Begin by using the figure() method to determine how large you want the chart to be. I'm going to recommend these proportions.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))


Now, we use the plot() method for line charts. We are going to first plot goals. Remember vals carries information for both goals and appearances. We want only goals right now.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"])


The marker argument defines what the points will look like. We want a circle, so I've input "o".
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o")


Finally, for goals, I want a nice solid scarlet, and that's what we'll have for the color attribute.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))


Finally, we use the show() method to render the chart.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))

  plt.show()


I chose to show the stats for Roberto Firminho, one of the longest-serving players in the menu. Notice that the data is rendered in circular dots and it's a bright red.


Now, we do the same for appearances! Only this time, we'll use a deeper shade of red.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  plt.show()


As you can see here, the scale has changed because the maximum number of appearances is vastly greater than the number of goals. (Still a great goalscorer though!)


Let's add these values to the plot. First, for goals, we want to iterate through the values using a For loop. Since we want to use the index value as well, we will need to use enumerate() on the values.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
  
  plt.show()


Then we use the text() method within the For loop. index determines the horizontal positioning, while value determines the vertical positioning. We'll add 1 to value so that the text won't overlap the dots. The next argument is the value itself, and we need to use str() on it so convert it to a string. Finally, we use the same color we did for plotting the values of the goals.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))
  
  plt.show()


Looks good.


Now we do the same thing for appearances!
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))

  
  plt.show()


I like the look of how things are shaping up!


Next, let's add lines to show average values. Since we have two sets of values, we want them in different colors too. Let's start with the average line for goals. We use the axhline() method for this. We begin by specifying that the y argument is the average of the goals properties in vals, using the namnean() method from numpy.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]))

  plt.show()


And then we make the line a dashed line by passing in "-." as the linestyle argument.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]), linestyle="-.")

  plt.show()


The color argument is obvious - it's the same as the plot color for goals.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]), linestyle="-.", color=(1, 0, 0))

  plt.show()


Nice!


We want to label it, so let's use the text() method. It starts at 0 for the horizontal position. For the vertical position, we want it just about the dashed line, so we use the same value as the dashed line, plus 5. The text will be "GOALS", and of course, we use the same color.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]), linestyle="-.", color=(1, 0, 0))
  plt.text(0, np.nanmean(vals["goals"]) + 5, "GOALS", color=(1, 0, 0))
  
  plt.show()


There it is.


Now, if you need me to elaborate on what you should do for appearances, I'll be terribly disappointed.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]), linestyle="-.", color=(1, 0, 0))
  plt.text(0, np.nanmean(vals["goals"]) + 5, "GOALS", color=(1, 0, 0))
  
  plt.axhline(y=np.nanmean(vals["appearances"]), linestyle="-.", color=(0.5, 0, 0))
  plt.text(0, np.nanmean(vals["appearances"]) + 5, "APPEARANCES", color=(0.5, 0, 0))

  
  plt.show()


We'll be done soon. The hard parts are over.


Now all that's left to do is use xlabel() and title() methods for your chart!
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]), linestyle="-.", color=(1, 0, 0))
  plt.text(0, np.nanmean(vals["goals"]) + 5, "GOALS", color=(1, 0, 0))
  
  plt.axhline(y=np.nanmean(vals["appearances"]), linestyle="-.", color=(0.5, 0, 0))
  plt.text(0, np.nanmean(vals["appearances"]) + 5, "APPEARANCES", color=(0.5, 0, 0))
  
  plt.xlabel("Seasons")
  plt.title("Liverpool FC Player Stats for " + player)

  plt.show()


Let's try it with Salah this time! Uh-oh, looks like the top label overlaps! This can happen if the stats for appearances are fairly consistent, putting the average value near the maximum.


So what we need to do is add this line, to set the highest limit to the max value for appearances, plus a few. This ought to be safe, because, barring some freak statistic, the number of appearances for any player over the course of a season should be significantly higher than the number of goals. Unless it's the kind of player who makes maybe five appearances in one season but scores a hat-trick each time. But this is too much of an anomaly for me to bother with right now.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.ylim(0, max(vals["appearances"]) + 10)
  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]), linestyle="-.", color=(1, 0, 0))
  plt.text(0, np.nanmean(vals["goals"]) + 5, "GOALS", color=(1, 0, 0))
  
  plt.axhline(y=np.nanmean(vals["appearances"]), linestyle="-.", color=(0.5, 0, 0))
  plt.text(0, np.nanmean(vals["appearances"]) + 5, "APPEARANCES", color=(0.5, 0, 0))
  
  plt.xlabel("Seasons")
  plt.title("Liverpool FC Player Stats for " + player)
  plt.show()


Here we go.


Well, that was fun...

It's not all that often I do a web tutorial that doesn't involve coding for the web. But when I do, it's really rewarding. The matplotlib library seems pretty basic so far, but it has tons of features I'm itching to explore.

And that's the bottom line!
T___T

Friday 21 June 2024

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

Last month, we wrangled a bar chart in Python. This month, we will repeat the process, this time with a line chart.

Now, the considerations for a line chart are different from a bar chart. Line charts are primarily utilized to display progress over time. Therefore, the line chart will show both goals and appearances for particular players, over the course of several seasons.

There are some things we can reuse from the code, such as the dictionary from which we derive the data, and the helper function seasonName(). We will clear out the contents of the barChart() function, and rename it to lineChart(). This time, the function will not have season or stat as a parameter, because the line chart will show both goals and appearances across all seasons. We will, however, need the parameter player.

As for the rest of the code, we will discard it.
import numpy as np
import matplotlib.pyplot as plt

def lineChart(labels, vals, player):
  
def seasonName(year):
  return str(year) + "/" + str(year + 1)

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}
  }
}


We will require a variable, ans. That's the only input we will need.
  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}
  }
}
  
ans = ""


Therefore, when we create the While loop, this time we only need to do it as long as ans is not 0.
  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}
  }
}
  
ans = ""

while (ans != 0):


We will still need to have a menu. This time, we will have a menu of players. Create an empty list, players.
  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 = []
  
ans = ""

while (ans != 0):


Then iterate through the data object. For every element, grab the list of players by using the keys() method, and add it to players.
players = []
for season in data:
  players = players + list(data[season].keys())

  
ans = ""


Now, since players reappear in different seasons, we are going to have duplicates in the list players, and we'll need to remove them. If we create a dictionary from the keys of players. this automatically eliminates all duplicates (dictionaries cannot have duplicates).
players = []
for season in data:
  players = players + list(data[season].keys())
  
dict.fromkeys(players)
  
ans = ""


Then we put the result back in a list using the list() function...
players = []
for season in data:
  players = players + list(data[season].keys())
  
list(dict.fromkeys(players))
  
ans = ""


... and assign the value back to players. So now the list players has all unique values.
players = []
for season in data:
  players = players + list(data[season].keys())
  
players = list(dict.fromkeys(players))
  
ans = ""


And then use the sort() method on players.
players = []
for season in data:
  players = players + list(data[season].keys())
  
players = list(dict.fromkeys(players))
players.sort()
  
ans = ""


Now within the While loop, we iterate through the players list using a For loop. Since we want the index value, we will need to use the enumerate() function on players.
players = list(dict.fromkeys(players))
players.sort()
  
ans = ""

while (ans != 0):
  for i, p in enumerate(players):


Thrn we'll print out the options as we iterate.
while (ans != 0):
  for i, p in enumerate(players):
    print (str(i + 1) + ": " + p)


At the end of it, the option we print out is 0, to exit.
while (ans != 0):
  seasons = []
  
  for i, p in enumerate(players):
    print (str(i + 1) + ": " + p)
    
  print ("0: Exit")


And here, we ask the user to select from the list, using an input() function, and assign the value to ans.
while (ans != 0):
  seasons = []
  
  for i, p in enumerate(players):
    print (str(i + 1) + ": " + p)
    
  print ("0: Exit")

  ans = input("Select a player")


Before running the code, comment out the lineChart() function definition. We haven't written anything for it, so running the program will trigger an error.
#def lineChart(labels, vals, player):


There you go! A menu of players, nicely sorted in alphabetical order.


As before, we only want an integer value, so use the int() function here to force-convert the resultant value.
ans = int(input("Select a player"))


There's a possibility that the user will enter something non-numeric, so we encapsulate the entire thing in an infinite While loop and a Try-catch block.
while True:
  try:

    ans = int(input("Select a player"))
    break
  except:
    print("Invalid option. Please try again.")


After that, outside of the infinite While loop, we set the program to exit using the break statement if ans is 0. If the value of ans is numerical but invalid, we "restart" the While loop using the continue statement.
while True:
  try:
    ans = int(input("Select a player"))
    break
  except:
    print("Invalid option. Please try again.")  

if (ans == 0): break
if (ans > len(players) or ans < 0): continue


Let's try entering "hello". Obviously, it triggers the prompt ""Invalid option. Please try again."


Then we try a negative number. This "restarts" the While loop.


Then we try "0", and this quits the program altogether by exiting the outer While loop.


Now that we have a value, it's time to get other data. First, let's declare seasons as a list, within the first While loop.
while (ans != 0):
  seasons = []
  
  for i, p in enumerate(players):
    print (str(i + 1) + ": " + p)


Next, we define player by using the value of ans(minus 1 because counting starts from zero) as a reference to the players list. This will get you the name of the player chosen.
while (ans != 0):
  seasons = []
  
  for i, p in enumerate(players):
    print (str(i + 1) + ": " + p)
    
  print ("0: Exit")

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

  if (ans == 0): break
  if (ans > len(players) or ans < 0): continue
    
  player = players[ans - 1]


Then we declare stats as a dictionary object with goals and appearances as properties, both set to empty lists.
if (ans == 0): break
if (ans > len(players) or ans < 0): continue
  
player = players[ans - 1]
stats = { "goals": [], "appearances": []}


Now iterate through data using a For loop.
player = players[ans - 1]
stats = { "goals": [], "appearances": []}

for season in data:


We only want to operate on data that features the selected player. Thus we use an If block to check if player is in the list returned by running the keys() method on that current element in data.
for season in data:
  if player in data[season].keys():


If so, we append that particular season to seasons. But we'll want the proper season name instead, and that is where the seasonName() function comes in.
for season in data:
  if player in data[season].keys():
    seasons.append(seasonName(season))


We then add to stats. We want to append to the goals and appearances lists. Thus, if Luis Diaz was selected, the seasons 2021 and 2022 would be added to seasons. Then his goals and appearances for 2021 and 2022 would be appended to the goals and appearances properties of stats.
for season in data:
  if player in data[season].keys():
    seasons.append(seasonName(season))
    stats["goals"].append(data[season][player]["goals"])
    stats["appearances"].append(data[season][player]["appearances"])




Remember commenting out the call to lineChart()? Well, time to undo that action. We will next call lineChart() and use seasons, stats and player. seasons and player will be used to set labels and stats will provide values.
for season in data:
  if player in data[season].keys():
    seasons.append(seasonName(season))
    stats["goals"].append(data[season][player]["goals"])
    stats["appearances"].append(data[season][player]["appearances"])

lineChart(seasons, stats, player)


Next

Rendering the line chart with two sets of values.

Sunday 16 June 2024

Why Mastery of Programming is a Myth

Much has been made about mastering the craft, and the constant moaning that developers these days are sloppy and don't make the grade.

Some developers feel that way about themselves. They feel that they aren't masters of their craft (spoiler: many of them aren't) and this disqualifies them from being where they are right now. That they will be found out someday. That they should be grateful to be employed. I'm no stranger to that feeling, especially during my younger years.

On some level, I get it; we all want a certain standard of professionalism in the workplace. And not adhering strictly to standards, results in bug-ridden, subpar software. I've even said before in a previous blogpost that sometimes, taking the trouble to do the simple things right, is vital.

When does this become silly?

There's a limit to how much gatekeeping can take place before it becomes ridiculous.

The saying goes like this: If something's worth doing, it's worth doing well. Now, this isn't exactly untrue. If you're going to put in the effort to do something, you may as well try to do it properly.

However, here's something to consider. Trying to do something properly isn't the same thing as actually doing it well. Ever heard people butcher your favorite song in a karaoke session? Do you think they're singing badly on purpose?

Singing badly.

Let me counter the saying with this - before you learn to do something well, you first have to learn to do it half-assed. Kind of like learning to walk before you can learn to run.

Now, do programmers write bad code? Of course they do. It's not always due to the lack of giving a shit. Sometimes they just never figured out a better way. Even with the will to continuously improve, there are a million things a programmer can learn, all in different directions, how to code better.

Are we saying that programmers who don't currently write great beautiful watertight code, shouldn't have a job? And how, pray tell, are these programmers ever going to get their feet wet if they don't get to apply their code in the real world? Not all code sends people to Mars, or handles billions of dollars in financial transactions hourly. Some code just handles penny-ante stuff like displaying blog posts on a web page, and if these programmers want to do that job, why shouldn't they? Unless you're saying that you want that job?

The insistence on "Mastery"

Sometimes, the insistence on "Mastery" in programming is just ego talking. Not mastery. Masturbation. The mental kind.

You know those job ads that want their candidates to have a "mastery" or "best-in-class" or "first-rate" of whatever discipline? They're kidding themselves. Whatever project they think is going to change the world, chances are, it's just marketing talk and that role doesn't actually need anyone that good. They just want to feel like they're getting the best, even though common sense would tell them that the best would be already happily employed somewhere else. The best wouldn't likely be applying to their stupid job applications.

We only want
the best!

I mean, can you imagine the sheer level of delusion it would take to even imagine that your fabulous earth-shattering idea (which probably has already been done to death) requires that amount of expertise?

Or even the famous phrase "go big or go home", a favorite phrase among the ambitious. Really? So if you can't be the best, or even "go big", you're just going to pack up and leave? If you can't be a master of your craft, you're just going to stop embarrassing yourself by even trying to improve? That's not being a serial winner, that's just being a baby. So go suck on a pacifier, Junior, adults are working here.

I should also mention that in just the field of programming alone, there are way too many things to learn and get good at. Just statistically, what you learn tends to be what you need, at that moment. What you get good at, tends to be what you keep doing over and over. If you needed to use Python to, for example, clean up string values before committing them to a database, you would end up learning a lot of string manipulation functions, the gotchas of character encoding, and all that jazz. You certainly would not be learning about Python's web-scraping or chart-plotting utilities. Would that mean you didn't know Python at all, just because you didn't know everything about it? That's a depressingly binary way of looking at things.

"Mastery"? This is a joke, right?

And on the other side...

...we have programmers getting Imposter Syndrome and wondering if they're good enough to deserve their current position. Newsflash: where you are right now, is the product of not just your technical abilities, but also how much you impressed during interviews and how well you networked with the right people. Something about you made up for whatever you think you lack. Ergo, you pretty much deserve to be wherever you are. This Imposter Syndrome nonsense suggests that you think working as a programmer is only about programming. And that, my friend, is one the the first and biggest mistakes a programmer can make.

If you're going to make a living at coding, the first fact that you need to reconcile with, is that your first few attempts are going to be laughable. Your first hundred websites, your first hundred web forms, your first few web apps. They will be, despite your darnedest efforts to the contrary, horribly inefficient, a nightmare to maintain and result in a mountain of technical debt. Does that mean you should listen to be would-be gatekeepers of our sacred profession and never start until you have become a master?

Absurd.

Why stop there? Let's take this nonsense even further.

Unless you can do more than boil eggs and make sandwiches, unless you can do shit that would win you the approval of Gordon Ramsay himself, you shouldn't step in the kitchen.

Can't flaunt your figure
unless it's perfect?

Unless you're in great shape and the sight of your figure in tights isn't visually offensive, you should dress modestly.

Unless you can type in full sentences and perfect English and have full mastery of the language, you should stay off Social Media. (I would actually support that one)

This is why you get software developers who are scared to death to admit when they made a mistake, or that they don't know something. This harms the industry a whole lot more than having an entire bunch of supposed "masters" of their craft out there. If you can't admit that you don't know something, if your environment is full of factors that discourage that kind of disclosure, that adds self-created roadblocks to learning. And without learning, there is no growth.

Conclusion

Everything is a work in progress. You're a work in progress. Forget that at your own peril.

Why insist on perfection in an imperfect world? Exercise some humility; you're probably not destined for greatness. And unless you're willing to start small, you almost definitely never will be.

Master of none,
T___T

Monday 10 June 2024

When an absence of value doesn't equate to a NULL

In programming, it can be easy to confuse the NULL value some something similar, such as undefined or (with regard to string literals) empty. This is because people tend to classify values into two categories - either something has value, or it has no value.

But NULL is a value in itself... and that value is explicitly no value. Confusing? I can see how it would be.

NULL values vs undefined

Let's say I gave you a voting slip in an election, where you could choose only one candidate out of two choices.

Not voting is not the same
as voting for no one.

Did you choose the first, or second candidate? That's the value of your choice. If your choice was, say, a variable named choice, and the candidates were numbered 0 or 1, the value of choice would either be 0 or 1.

But what if you decided not to vote, or hadn't had a chance to vote yet? Then, if this was JavaScript, the default value for choice would be undefined, because a value has not yet been assigned to choice.

What if you decided to invalidate your vote by spoiling it or just not filling it in? Then you would have voted. A value would be assigned to choice. And since you invalidated the vote, that means you voted for neither. But your vote was still registered. Whereas undefined means that you hadn't voted yet, in this case you had voted and you had explicitly not voted for either candidate. This would be more akin to a NULL value; you explicitly choose not to make a choice. But that in itself is a choice, therefore the variable choice now has a value. And that value, of course, is no value. Or NULL.

NULL values vs empty strings

Similarly an empty string is not the same as a NULL. Both an empty string and a NULL have values. The NULL has, as before, a value of no value. And the empty string is simply a string with no characters, but that does not change the fact that it is a string.

A string without characters
is still a string, not a NULL.

Try running a string operation on an empty string...
var str = "";
console.log(str.length) //gives you 0.


...and on a NULL.
var str = null;
console.log(str.length) //gives you an error.


Trying to get the length property from str in the first example will give you a 0, because it is a string with zero characters. In the second example, you get an error because a NULL is not a string object, and therefore does not have a length property.

To expand on the above, a NULL is not only not a string, it is also not an integer, or a float, or a Boolean. A NULL is a NULL.

The Takeaway

The takeaway from this, of course, is that NULL values are NULL values... not empty strings or 0s. NULL values are values that basically mean "no value", which is not the same as actually having no value.

It's NULL or never,
T___T


Thursday 6 June 2024

Is it really ScarJo, and why does it matter?

If you're both a fan of Hollywood and tech, the tail end of May must have been eventful for you. A-list movie star Scarlett Johansson took legal action against tech company OpenAI for using her voice without permission, for promotion of their product. Just to elaborate, in case there's even the remotest chance that you don't know who or what OpenAI is, it's the company that is responsible for generative technology such as ChatGPT, Dall-E and Sora.

What happened was that OpenAI included Voice Mode to ChatGPT, where the user can choose a voice out of a few presented options, to engage with ChatGPT.

Voice Mode.

The sticking point was that one of the voices, named "Sky", sounded an awful lot like Scarlett Johansson herself. In the days that followed, ScarJo revealed that OpenAI's CEO, Sam Altman, had approached her twice for permission to use her voice for that purpose, and she had declined to give it. It sounded like OpenAI had gone ahead anyway, disregarding her objections, when they launched the new feature. Johanssen, herself no stranger to suing corporations when necessary, did not hesitate to call out Open AI.

Was it ScarJo, though?

Apparently not! OpenAI apologized to Johansson for the confusion and clarified that the voice wasn't hers, but belonged to a voice actor whom they hired for the job. She simply sounded an awful lot like Johansson, and had her mannerisms.

Nevertheless, OpenAI took down the voice "out of respect" but the bonfire that was the controversy continues to rage on. Though it begs the question: if OpenAI are completely innocent of the charge, why is there a need to do that?

A.I generated
image of ScarJo.

I can't verify if the voice is indeed Scarlett Johansson's, or simply sounds similar. I couldn't pick Johansson voice out of a lineup for the simple reason that I'm not great with voices. Also (heh heh) I spend significantly more time looking at rather than listening to her.

But perhaps that's not the important point in this entire thing. If A.I can legally replicate any celebrity's likeness for the purpose of promoting any product, service or agenda regardless of that celebrity's intentions, that entails a couple of implications.

The first, and arguably less important, is financial. Now, I'm not concerned with ScarJo specifically. She'll be fine; this woman makes more money in a month than most of us do in a year. However, just being able to ride on the back of someone else's popularity to promote whatever you want without needing to pay that person a cent, strikes me as fundamentally and ethically wrong.

The more alarming implication

The second implication, is, of course, having someone else use your image to sell something you don't endorse. In an age where deepfakes and misinformation abounds, this is a huge, glaring red flag.

Beware!

Take the case of one Gina Carano. Back in 2021, she found herself being cancelled due to some remarks she made on Social Media. Whether one agrees with her or not, the fact remains is that she aired those views. She stood behind them, and suffered the consequences. There is no debate about that.

But what if she didn't espouse those views? What if she only appeared to do so because someone made an A.I deepfake of her spouting those views? Now we're wandering into some truly sinister territory.

Sam Altman himself posted a Tweet on X, leading up to the release of the new feature, referencing the movie Her, which, quite ironically, starred Scarlett Johansson herself as an A.I generated voice, thus implying that the voice of Sky in ChatGPT's Voice Mode was really ScarJo. Now, he could have been completely innocent of the charge of stealing ScarJo's voice for OpenAI. The voice could really have, as claimed, come from a completely unrelated voice actress. With that stunt from him, it no longer matters. The existing fears that people have towards generative A.I could only have been amplified after this debacle, regardless of the legal outcome.

Seriously, between Sam Altman and Elon Musk, what is it with tech company CEOs doing stupid shit and acting like consequences are for lesser mortals?

Also, if anyone feels like arguing that it's not too far-fetched for Scarlett Johansson to endorse OpenAI, do remember that this woman is in the business of filmmaking, an industry that OpenAI's Sora may well one day disrupt. Call it a failure of imagination on my part, but I can't see her (or any other actor) championing that.

In the Final Analysis

It doesn't matter one iota if Sky was really ScarJo. Perception is everything, especially in a world where A.I is generating content.

Whether or not OpenAI survives this legal battle; indeed, whether this even results in a legal battle at all, its public image has taken a serious battering.

Looks like Sky is the limit!
T___T

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