Last few weeks in Programming: Python, Ruby

You may notice it’s been a while since I last wrote about programming. Well, a lot of the programming I was doing with the kids was due to having more time on my hands thanks to COVID. With Scarlett back in school full-time, there’s a lot less time for that as we focus on making sure she gets the most out of her education as well as teaching the twins to make up for not being in preschool this year. This has left me with reduced time for all my projects and entertainment (games, TV, and reading). Up until now that has meant the programming was put off to the side – kind of. Because I’ve been loving my exercises from the Python Morsels website and I’ll be talking about that when I get to the Python section. But first up, Ruby!

Ruby

At some point I think I blogged about reading Ruby Wizardry: An Introduction to Programming for Kids. I got the book during one of the many (MANY) programming Humble Bundles I’ve purchased. Originally, I meant to go through it with Scarlett. I even thought we had a good hook in the fact that one of the characters in the book (which teaches programming through storytelling – I LOVE this idea and wish more kids’ programming books thought of it) would help. Unfortunately, it seems I scarred her when I had her do the Python for Kids book before she could truly read and write, making it a frustrating experience that turned her off of programming until we discovered Scratch. Well, I wanted to learn Ruby and it’s the one book I have, so I figured I could go through it now and I’ll have a better idea of what’s in there if any of the three kids ever wants to go through it. I put it into my “free time rotation”, but that list has grown so long that there was too much time between coming back to Ruby. So I decided to go ahead and just finish the book. It can’t be THAT long compared to an adult’s introduction to a programming language.

Here are some of the neat things I learned:

One of my favorite things in Python is relatively new to the language: F-strings. It allows you do do:

number = 1
print(f"The number is.... {number}")

instead of:

number = 1
print("The number is... %d".format(number))

While it’s clear in a trivial example like that, it can be hard to keep track of what’s going on once you start having 3 or 4 variables in there.

Ruby already had this built-in!

number = 1
puts "The number is... #{number}"

I thought that Python was pretty easy for new programmers with its much more English syntax and the fact that it uses tabs rather than curly braces for its code blocks. But Ruby is even more “user-friendly” from this perspective. Take this code example from the book:

flowmatic_on = true
water_available = true
if flowmatic_on && water_available
  flow_rate = 50
elsif !flowmatic_on
  puts "Flomatic is off!"
else
  puts "No water!"
end
if flow_rate > 50
  puts "Warning! flow_rate is above 50! It's #{flow_rate}."
  flow_rate = 50
  puts "The flow_rate has been reset to #{flow_rate}."
elsif flow_rate < 50
  puts "Warning! flow_rate is below 50! It's #{flow_rate}."
  flow_rate = 50
  puts "The flow_rate's been reset to #{flow_rate}."
else
  puts "The flow_rate is #{flow_rate} (thank goodness)"
end

First of all, there isn’t even a colon required after the conditionals (if/else). Second, the indentation is completely optional. Third, use of the word end clarifies to the programmer (especially if it’s not your code) that this set of conditionals is done. So for a complete novice, it seems that Ruby does indeed earn its status as one of the easier new languages to learn. I’m not sure if puts is more reasonable than print. “puts” might make the novice ask – put where? “print” might have them thinking it’s going to a printer.

In fact, it gets even MORE user friendly because a few paragraphs later while simplifying they explain that if you have a conditional that looks like this:

if variable != value
   do something

it can be rewritten as

unless variable == value
   do something

And that is definitely more human-readable.

There is one part where I found Python a little more readable. In Ruby you can do Python-style for loops:

stops = ["East Bumpspark", "Endertromb Avenue", "New Mixico", "Mal
Abochny"]
for stop in stops
  next if stop.empty?
end

And other than the next keyword and question mark, it’s more or less the same as in Python. But here’s the more Ruby-esque (what’s the Ruby community’s version of Pythonic?) way to do it:

stops = ["East Bumpspark", "Endertromb Avenue", "New Mixico",
"Mal Abochny"]
stops.each do |stop|
  next if stop.empty?
end

And I find that WAY less readable; weird for a language that until now seemed more readable than Python. The cool thing is that they also support list comprehensions (who knows, maybe it appeared in Ruby before Python?)

stops = ["East Bumpspark", "Endertromb Avenue", "New Mixico",
"Mal Abochny"]
stops.each { |stop| next if stop.empty? }

Having finished the book, I think that naive Ruby is definitely more readable than Python. But once you start getting into a lot of the refactoring that emphasizes doing more code on one line, a lot of that syntax gets a bit cryptic.

Python

While I did fix a small documentation error in my Extra Life Donation Tracker, I haven’t done any project coding in Python over the last month. (Or rather I hadn’t when I first started writing this blog post, but since then I have started another project (details below)). But I have been really enjoying the Python Morsels exercises.

Python Morsels

What I’d like to do in this section is talk about the different exercises Trey has given me and what I’ve learned from each one.

add

The first problem that Trey gave me was to do matrix addition. The way he put it was if you had two lists of lists, you have to add up the numbers that line up with each other. It’s a bit clearer with the example he gave:

>>> matrix1 = [[1, -2], [-3, 4]]
>>> matrix2 = [[2, -1], [0, -1]]
>>> add(matrix1, matrix2)
[[3, -3], [-3, 3]]

Since this was the first of the problems he gave me, I hadn’t yet aligned my brain into Python problem solution mode. So it was really hard and I never got past the base question. However, I did learn a few things. First of all, here’s the solution I came up with:

def add(matrix1, matrix2):
    return [[val1 + val2 for val1, val2 in zip(list1, list2)] for list1, list2 in zip(matrix1, matrix2)]


if __name__ == "__main__":
    matrix1 = [[1, -2], [-3, 4]]
    matrix2 = [[2, -1], [0, -1]]
    add(matrix1, matrix2)

Interestingly, when I was reading Serious Python, I’d noted that I might want to learn how to use the built-in function zip. So when I was trying to figure this problem out, I came across that and decided to see if it could help me. It turned out to be perfect! Additionally, from Trey’s List Comprehension page, I learned how to finally do a good list comprehension. So, what this code is doing is first taking my matrices and making a tuple that consists of the two lists I need to add. (This is the right-most zip – the one that involves the matrix1 and matrix2 variables) Then the left-hand side is taking that tuple and adding up the numbers that line up with each other (first with first, second with second, and so on) and creating a list to return. It’s incredibly elegant that I can do all that with just one line (not counting the function signature). The most important thing I learned is that any time you have a for loop that is just creating a list, you should use list comprehension.

circle

Once I got to circle, this is when I really started to see the benefits of the way Trey has Python Morsels set up. So, first off, he asked me to create a class to represent a circle. I didn’t even need to stretch myself for this one.

import cmath


class Circle:
    def __init__(self, radius: float = 1):
        self.radius: float = radius
        self.diameter: float = radius * 2
        self.area: float = cmath.pi * self.radius * self.radius

    def __repr__(self):
        return f"Circle({self.radius})"

But then he started adding in constraints. First I had to be able to have the radius update and have the other values update automatically. Then I had to be able to set the diameter and have everything else adjust. Finally, it couldn’t have a negative radius set. This really stumped me. How could I make sure things were changing if someone changed the value of the radius? I could create methods to do that if I were just designing this for myself, but he had unit tests it had to pass that required me to allow someone to do circle.radius = some_value and it had to work. So I finally learned the true Pythonic way to do setter and getter methods. Having last used them in Java in school, I had been doing it in a very un-Pythonic way. So here was my final code for that challenge:

import cmath


class Circle:
    def __init__(self, radius: float = 1):

        self._radius: float = radius

    @property
    def radius(self) -> float:
        return self._radius

    @radius.setter
    def radius(self, new_radius):
        if new_radius < 0:
            raise ValueError("Radius cannot be negative")
        else:
            self._radius = new_radius

    @property
    def diameter(self) -> float:
        return self.radius * 2

    @diameter.setter
    def diameter(self, diameter) -> None:
        if diameter < 0:
            raise ValueError("Diameter cannot be negative")
        self.radius = diameter/2

    @property
    def area(self) -> float:
        return cmath.pi * self.radius * self.radius

    @area.setter
    def area(self, area) -> None:
        raise AttributeError

    def __repr__(self):
        return f"Circle({self.radius})"

Then, after reading Trey’s solution I realized tehre were a few things I had in there that weren’t necessary. For example, I didn’t need to create an @area.setter just to raise an AttributeError. Simply not having an @area.setter method would have done that for me. So I ended up with the following in the end:

import cmath


class Circle:
    def __init__(self, radius: float = 1):

        self.radius: float = radius

    @property
    def radius(self) -> float:
        return self._radius

    @radius.setter
    def radius(self, new_radius):
        if new_radius < 0:
            raise ValueError("Radius cannot be negative")
        else:
            self._radius = new_radius

    @property
    def diameter(self) -> float:
        return self.radius * 2

    @diameter.setter
    def diameter(self, diameter) -> None:
        self.radius = diameter/2

    @property
    def area(self) -> float:
        return cmath.pi * self.radius * self.radius

    def __repr__(self):
        return f"Circle({self.radius})"
fix_csv

Every once in a while, I’ve got enough experience to nail the solution without any trouble. This was on such case.

import argparse
import csv

parser = argparse.ArgumentParser(description="Change CSV delimiters")
parser.add_argument('original', help="The original CSV file to convert")
parser.add_argument('destination', help="The destination CSV file, comma delimited")
parser.add_argument('--in-delimiter', help="The character for the delimiter in your input file")
parser.add_argument('--in-quote', help="The quote character in the in-file")
args = parser.parse_args()

infile_delimiter = "|"
infile_quote = '\"'

if args.in_delimiter:
    infile_delimiter = args.in_delimiter

if args.in_quote:
    infile_quote = args.in_quote

with open(args.original) as csv_file:
    original_input = None
    if args.in_delimiter or args.in_quote:
        original_input = list(csv.reader(csv_file, delimiter=infile_delimiter, quotechar=infile_quote))
    else:
        dialect = csv.Sniffer().sniff(csv_file.read())
        csv_file.seek(0)
        original_input = list(csv.reader(csv_file, dialect))

with open(args.destination, 'w') as csv_file:
    output_writer = csv.writer(csv_file, delimiter=',')
    output_writer.writerows(original_input)

For this program, the only part I needed a hint for was in the second bonus to find out that the Sniffer (line 25) class exists in the csv module.

tail

This was another one where the base solution was pretty easy for me, but as Trey tacked on more and more restrictions with the bonuses, it forced me to really think outside the box and, eventually, learn about a new part of Python. The problem statement was to take in a sequence and a number. The number tells you how many items from the end of the sequence should be kept and returned as a list. His initial examples:

>>> tail([1, 2, 3, 4, 5], 3)
[3, 4, 5]
>>> tail('hello', 2)
['l', 'o']
>>> tail('hello', 0)
[]

At first I was able to do a nice easy solution:

def tail(sequence, number_of_elements):
    if number_of_elements > 0:
        return list(sequence[-number_of_elements:])
    else:
        return []

But his Bonus 2 unit test created a problem for me because it created a sequence so large it would crash my system! So I needed to figure something out! By going through his hints I learned about deques. Essentially it’s a capped list where you tell it how big it can get and it FIFOs (First In, First Out) the rest of the items. So I ended up with:

import collections
import collections.abc


def tail(sequence, number_of_elements):
    if number_of_elements > 0:
        if isinstance(sequence, collections.abc.Sequence):
            return list(sequence[-number_of_elements:])
        else:
            deque = collections.deque(maxlen=number_of_elements)
            for item in sequence:
                deque.append(item)
            return list(deque)
    else:
        return []

In his solution I learned I could have even made things more elegant by replacing the for loop with:

list(deque(iterable,maxlen=n))
count_words

For this one, Trey started out with:

>>> count_words("oh what a day what a lovely day")
{'oh': 1, 'what': 2, 'a': 2, 'day': 2, 'lovely': 1}
>>> count_words("don't stop believing")
{"don't": 1, 'stop': 1, 'believing': 1}

Which was pretty easy for me as I’d done something similar in Impractical Python. But as I worked through the bonuses, which eventually required Regular Expressions, I’d done enough of these by this point to know when I was writing ugly, inelegant code. Here was my solution before reading through Trey’s solution:

from collections import Counter
import re


def count_words(phrase):
    counter = Counter()
    sentence_list = phrase.lower().split(" ")
    # regular expression to get rid of punctuation.
    front_pattern = re.compile('\A\W\w')
    end_pattern = re.compile('\w+\W$')
    for word in sentence_list:
        if front_pattern.match(word):
            word = word[1:]
        elif end_pattern.match(word):
            word = word[:-1]
        counter[word] += 1
    return counter

What’s interesting is that, just like deque last week, I don’t actually need to make use of a for-loop. The Counter function will auto-iterate for me. (Which is weird, because the documentation shows a for-loop, but maybe that’s just to make things more clear?)

A more important thing is the Regular Expression part (I really should start going through at least one of my many RE books from O’Reilly). Instead of trying to eliminate the prefix and suffix punctuation, I should have just done:

r"\b[\w'-]+\b"

This means look for words, apostrophes, and dashes and get rid of whatever’s outside the word. So this gets by the problem I was having at first with the word “don’t”. So here is the final solution – a mashup of my code and Trey’s:

from collections import Counter
import re


def count_words(phrase):
    return Counter(re.findall(r"\b[\w'-]+\b", phrase.lower()))

Crazy how much shorter that is, right?!?!

point

This one once again had me learning more and more with each bonus after the base solution didn’t present any challenges to me. We were to make a Point class to represent a point in 3D space, allow math functions on it, determine if two point objects were the same point in space, and allow unpacking of the coordinates with multiple assignment. When it came to the third bonus, (multiple assignment) I didn’t even have the first clue of what I needed to do to solve this bonus. So, off to hints land! This comes together with everything I’ve been learning so far with Trey and ties nicely with all the iteration, iterator, etc stuff he’s been having us do! The solution is to do iter and create a generator that gives the answers. So here’s my code after all that:

class Point:
    def __init__(self, x: int, y: int, z: int):
        self.x = x
        self.y = y
        self.z = z

    def __add__(self, other):
        return Point(self.x+other.x, self.y+other.y, self.z+other.z)

    def __sub__(self, other):
        return Point(self.x-other.x, self.y-other.y, self.z-other.z)

    def __mul__(self, other):
        return Point(other*self.x, other*self.y, other*self.z)

    def __rmul__(self, other):
        return Point(other*self.x, other*self.y, other*self.z)

    def __eq__(self, other):
        if other.x == self.x and other.y == self.y and other.z == self.z:
            return True
        return False

    def __iter__(self):
        yield self.x
        yield self.y
        yield self.z

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y}, z={self.z})"

This is one of the exercises that works really well to me as the proof of why Python Morsels is a really awesome exercise for improving the understanding of Pythonic code. As I said above – the algorithmic answer was easy. But it was not the best, most Pythonic code. Let’s take a look at what I can change based on his official solution for the base problem.

class Point:
    def __init__(self, x: int, y: int, z: int):
        self.x, self.y, self.z = x, y, z

    def __add__(self, other):
        return Point(self.x+other.x, self.y+other.y, self.z+other.z)

    def __sub__(self, other):
        return Point(self.x-other.x, self.y-other.y, self.z-other.z)

    def __mul__(self, other):
        return Point(other*self.x, other*self.y, other*self.z)

    def __rmul__(self, other):
        return Point(other*self.x, other*self.y, other*self.z)

    def __eq__(self, other):
        return (self.x, self.y, self.z) == (other.x, other.y, other.z)

    def __iter__(self):
        yield self.x
        yield self.y
        yield self.z

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y}, z={self.z})"

I was most excited about the refactoring to the equality code. I’ve gone from 3 lines to just one. I already knew from previous exercises that often when dealing with a return, an “else” is redundant. So I didn’t have that in my original solution. But I always forget that you can just do something like this because if it’s true, it’ll cause a return true.

When I was working on my base solution, I had the intuitive idea that I could use dataclasses, but I didn’t have any experience with them, so I didn’t bother in order to see if I could get things working without using it. So here’s the solution that’s the more Pythonic while eliminating a lot of boiler plate code.

from dataclasses import astuple, dataclass


@dataclass
class Point:

    x: float
    y: float
    z: float

    def __add__(self, other):
        return Point(self.x+other.x, self.y+other.y, self.z+other.z)

    def __sub__(self, other):
        return Point(self.x-other.x, self.y-other.y, self.z-other.z)

    def __mul__(self, other):
        return Point(other*self.x, other*self.y, other*self.z)

    def __rmul__(self, other):
        return Point(other*self.x, other*self.y, other*self.z)

    def __iter__(self):
        yield from astuple(self)

So now I’m able to eliminate init, eq, and repr. I’m also able to simplify the iter method. It’s also a lot less code.

parse_range

This problem, where Trey wanted to give us a string with a list of numeric ranges and have us return a list with the numbers in that range, was the first time that I disagreed with Trey on some of the solutions in terms of their readability. Now, that may simply be because he’s more advance than I am or it may be one those those to-may-toe/to-mah-toe things. Or it might actually be that, as an instructor, he’s purposely making choices with his base solutions and bonus solutions to teach us something about refactoring, but it was interesting that I ended up making code that didn’t have to change much from base to bonus 3 while his had to change a lot. Now, his final solution was way more elegant than mine. But the steps in between were not, which I found interesting. Here’s my final code (again, his is better):

def parse_ranges(string):
    ranges = string.split(',')
    for item in ranges:
        number = item.split('-')
        for thing in number:
            try:
                int(thing)
            except ValueError:
                number.remove(thing)
        for numeral in range(int(number[0]), int(number[-1])+1):
            yield numeral


if __name__ == "__main__":
    parse_ranges('1-2,4-4,8-10')
is_anagram

This is one of those programs that almost everyone does when they’re learning programming because it usually demonstrates a lot of concepts at once. Trey then ups the ante by having us ignore spaces and punctuation and allowing accented vowels to be matched with non-accented vowels. Things got off to a bad start when I accidentally confused anagrams with palindromes. (Mostly because the first example Trey gave us almost looks like a palindrome). Wasted about an hour on that, but I did get a better understanding of various string and list functions. Once I realized it was anagrams we were after, it was pretty easy to realize you just wanted to use sets and do comparison. Unlike a list, sets don’t care about order, just membership. Of course, that also meant I had to check for length as the skin, sinks test was there to remind us. After all the bonuses I came up with:

import re

from unicodedata import normalize


def is_anagram(string1: str, string2: str) -> bool:
    """Returns true if string1 and string2 are anagrams of each other"""
    # get rid of unicode
    string1 = normalize('NFD', string1)
    string2 = normalize('NFD', string2)

    # get rid of all non-alphanumerics
    string1 = re.sub('[^A-Za-z0-9]+', '', string1)
    string2 = re.sub('[^A-Za-z0-9]+', '', string2)

    if len(string1) != len(string2):
        return False
    else:
        return set(string1.lower()) == set(string2.lower())


if __name__ == "__main__":
    result = is_anagram("teá", "eat")
    print(result)

I knew I probably wasn’t doing this to most efficient way when I did sets because I had to have the extra return False when they had different lengths. If there’s one thing I’ve learned about Trey’s problem sets, it’s that they’re usually much simpler than you want to do them at first. I think his solutions were actually pretty darn elegant. He offers using both Counter and sorted. Counter works because it’s creating a dictionary counting up each letter and if they are equal, then they inherently have the same length. For sorted, it’s basically putting the letters in order and the first time there’s a duplicate (or if one is longer than the other), it’ll become False. Funnily enough, he ended up with the same bonus solutions as me. I mean, he was using Counter, but he also used replace.

I’m surprised that for his bonus 2 example, he mentions using a generator. I was explicitly trying NOT to use any kind of for loops beacuse I was certain that wasn’t the solution he’d be looking for. I’m SHOCKED he doesn’t do anything at all with regular expressions. Could have saved myself about an hour’s worth of work just doing a for loop.

In the end he creates 2 helper functions to help him do it. I actually almost like mine better, even if Counter is more elegant.

meetup_date

Here Trey wanted us to give the date for a meetup if you have the data something like – I want it to be the the fourth Thursday of the month. For this particular exercise, I’d like to go through each of my solutions because my feelings as I went through each one were pretty fun. I’d also like to go through how it compares with his solutions to give a great example of how much I love learning from these. So, first up was to just calculate the 4th Thursday of a month.

from datetime import date


def meetup_date(year, month):
    """Given a year and month, return the day of the month that is the fourth Thursday."""
    thursdays = 0
    for day_of_month in range(1, 32):
        if date(year, month, day_of_month).weekday() == 3:
            thursdays = thursdays + 1
        if thursdays == 4:
            return date(year, month, day_of_month)


if __name__ == "__main__":
    date = meetup_date(2020, 9)
    print(date)

My first inclination was to use the Calendar module. Its method itermonthdays4 seems to return exactly what we need – a datetime.date object. However, after playing with that for about half an hour, I couldn’t quite get it to do what I needed. After that I decided to start off with a naive attempt at the problem to see if a better solution presented itself.

So, interestingly, Trey says it will be tricky with just datetime, but I found it SO MUCH easier than what I was trying to do with calendar. I’ll be VERY interested in seeing what his solution is!

Next he wanted us to accept the nth day of the month, both specified by the user:

from datetime import date


def meetup_date(year, month, nth=4, weekday=3):
    """Given a year and month, return the day of the month that is the fourth Thursday."""
    target_weekday = 0
    for day_of_month in range(1, 32):
        if date(year, month, day_of_month).weekday() == weekday:
            target_weekday = target_weekday + 1
        if target_weekday == nth:
            return date(year, month, day_of_month)


if __name__ == "__main__":
    date = meetup_date(2020, 9)
    print(date)

Bonus 1 was extremely easy. All I had to do was un-hardcode a few values. I suspect with bonus 2, things might get a bit trickier.

Then we had to be able to count from the back of the month.

from datetime import date


def meetup_date(year, month, nth=4, weekday=3):
    """Given a year and month, return the day of the month that is the fourth Thursday."""
    start = 0
    end = 0
    step = 0
    if nth > 0:
        start = 1
        end = 32
        step = 1
    else:
        start = 31
        end = 0
        step = -1
    target_weekday = 0
    for day_of_month in range(start, end, step):
        try:
            if date(year, month, day_of_month).weekday() == weekday:
                target_weekday = target_weekday + 1
            if target_weekday == abs(nth):
                return date(year, month, day_of_month)
        except:
            print("short month")


if __name__ == "__main__":
    date = meetup_date(2020, 2, -1)
    print(date)

OK, things are getting ridiculous here. There is no way this is the right way to solve this – it’s bonkers. But it works! Which, I think is the whole reason I’m doing these exercises. To learn the more elegant ways to program in Python.

Finally we had to use a Weekday object:

from datetime import date
from collections import namedtuple

weekday_tuple = namedtuple('Weekday', ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'],
                           defaults=[0, 1, 2, 3, 4, 5, 6])
Weekday = weekday_tuple()


def meetup_date(year, month, nth=4, weekday=3):
    """Given a year and month, return the day of the month that is the fourth Thursday."""
    start = 0
    end = 0
    step = 0
    if nth > 0:
        start = 1
        end = 32
        step = 1
    else:
        start = 31
        end = 0
        step = -1
    target_weekday = 0
    for day_of_month in range(start, end, step):
        try:
            if date(year, month, day_of_month).weekday() == weekday:
                target_weekday = target_weekday + 1
            if target_weekday == abs(nth):
                return date(year, month, day_of_month)
        except:
            print("short month")


if __name__ == "__main__":
    date = meetup_date(2018, 1, nth=1, weekday=Weekday.MONDAY)
    print(date)

He said object, not class, so I’m guessing maybe a named tuple or extended dictionary? Looks like I was right. A namedTuple with default values turned out to provide a working solution for Bonus 3. However my code is bananas. Let’s see what Trey actually recommends.

So it looks like my intuition to use the Calendar module was correct. For almost every solution, except Bonus 1, he mentions that the Calendar module is the easiest and/or more readable. And a big focus on these exercises is not just to make things more elegant or Pythonic, but to make them more readable and maintainable. At one point I also thought of using a generator, but having gotten stuck with the Calendar module, I had already decided to go on the naive route. This is one of the rare times where I’m not sure if I prefer my solution or Trey’s solution more in terms of understanding what it does as well as adaptability. It was very easy to adapt mine without needing to create helper functions. I just moved from magic values to variable names. But near the end Trey ends up having to create helper functions.

For example, this is his first solution:

from calendar import Calendar, weekday, THURSDAY

def meetup_date(year, month):
    """Return a date of the fourth Thursday of the given month."""
    nth = (
        4
        if weekday(year, month, 1) != THURSDAY
        else 3
    )
    thursday_calendar = Calendar(THURSDAY).monthdatescalendar(year, month)
    return thursday_calendar[nth][0]

Essentially he’s saying that if first day of the month is a Thursday, then you want the 3rd week of a calendar that has Thursday as the first day of the week (instead of Sunday or Monday). Otherwise, you want the fourth week. This is why things get tricky in bonus 2 when you can count backwards and there are sometimes 5 weeks in a month. With my solution, it doesn’t matter, we’re just counting calendar days and figuring out if we’ve reached the right Thursday yet.

However, you can’t deny the Pythonic-ness of his Bonus 2 solution, even if it takes a bit more to understand.

from datetime import date
from calendar import monthcalendar, THURSDAY

def weekdays_in_month(year, month, weekday):
    """Return list of all 4/5 dates with given weekday and year/month."""
    return [
        date(year, month, week[weekday])
        for week in monthcalendar(year, month)
        if week[weekday] != 0
    ]

def meetup_date(year, month, *, nth=4, weekday=THURSDAY):
    """Return date of the nth weekday of the given month."""
    return weekdays_in_month(year, month, weekday)[nth-1 if nth > 0 else nth]

Basically, he’s using a helper function that puts all the Thursdays in a month into a list via list comprehension. Then he just uses list index (hence the need to subtract 1 from nth since lists start counting from 0). It’s pretty genius!

As for Bonus 3, I guess a class would have been fine. But at least I learned about using namedTuples.

Podman APIs Project (not a Python Morsel)

When I heard that Podman switched to a new restful interface, I decided I could write a Python implementation of the API that would help me implement some automation. So I started a new repo, podmanapis. What makes things a bit tricky is that the restful API is not consistent in what it returns. Sometimes it returns JSON, sometimes it just returns 200 OK. Other times it returns a stream. So trying to make a consistent Python API is a bit of a challenge. Not impossible, but a bit more work, Definitely means my implementation will end up being “opinionated”.

Published by Eric Mesa

To find out a little more about me, see About Me