January 23, 2024

Starting off easy: Week 1 of 48 in 24

Exercism – one of my favourite “learn to code” websites – is doing a thing where each week (ish?) there is a new challenge to solve a coding exercise in several languages. It’s called “48 in 24”. It looks like you get a bronze medal if you solve the coding exercise in some language, a silver medal if you solve it in three specific languages, and a gold medal if you solve it in any three languages in 2024. We’ll see how long this lasts, but I’m going to try to blog about my attempts to solve the exercise. To be clear, these are my attempts to solve the exercise, likely often in languages I’m not that familiar with, so the solutions might not be optimal or idiomatic. I’m also not going to explain every key word and language construct I use, I’ll only explain those things that I think you might not be able to guess by looking at the code.

So week one requires us to solve the exercise “Leap” in several languages. This requires us to take a year, and return whether or not it’s a lear year, according to the following rules:

In every year that is evenly divisible by 4. Unless the year is evenly divisible by 100, in which case it’s only a leap year if the year is also evenly divisible by 400.

The chosen languages this week are Clojure, MIPS Assembly and Python. Since I’ve already solved the exercise in Python in a previous year, I’m going to have to pick another language to have a go at, and I picked Elixir.

Elixir

I’m going to start with my Elixir solution, because it ended up being the least interesting of the three.

defmodule Year do
  @doc """
  Returns whether 'year' is a leap year.
 
  A leap year occurs:
 
  on every year that is evenly divisible by 4
    except every year that is evenly divisible by 100
      unless the year is also evenly divisible by 400
  """
  @spec leap_year?(non_neg_integer) :: boolean
  def leap_year?(year) when rem(year, 400) == 0 do
    true
  end
  def leap_year?(year) when rem(year, 100) == 0 do
    false
  end
  def leap_year?(year) when rem(year, 4) == 0 do
    true
  end
  def leap_year?(year) do
    false
  end
end

This makes use of Elixir’s kind of neat “multiple clause functions” where you can write several function definitions for different cases, rather than a big ol' logic tree inside your function. The first function definition that matches the inputs is the one that will be executed. So if the year value is divisible by 400, the first function definition will match, and it will return true. If year is not divisible by 400, but is divisible by 100, the second will match and return false. And so on.

MIPS Assembly

MIPS assembly was the least familiar of the languages required for the silver medal, I’ve never really done any assembly language programming (apart from in Zachtronics games). So my first attempt was kind of inefficient.

.globl is_leap_year

is_leap_year:
        li $t3, 400     # $t3 holds the divisor
        li $t1, 0       # $t1 holds constant zero
        div $a0, $t3
        mfhi $t0        # $t0 holds the remainder
        beq $t0, $t1, return_true
        li $t3, 100
        div $a0, $t3
        mfhi $t0
        beq $t0, $t1, return_false
        li $t3, 4
        div $a0, $t3
        mfhi $t0
        beq $t0, $t1, return_true

return_false:
        li $v0, 0
        j end_jump

return_true:
        li $v0, 1

end_jump:
        jr $ra

The first and last lines of this code were part of the stub exercism provided for the solution so I haven’t touched them. This is basically the MIPS equivalent of a standard “return early” approach to solving the problem. If the year is divisible by 400, return true. If not, then if the year is divisible by 100, return false. And so on. So li r v puts value v in register r, and div x y does integer division, and stores the result and remainder in two registers (called lo and hi) that can then be accessed with mflo and mfhi (move from lo, move from hi). The remainder is what we want, so we move from hi into a register called $t0. beq x y j is a jump instruction: jump to j if x==y. The return_true:, for example, is a label that you can jump to. So if the remainder from the division is equal to the constant zero, jump to the return label, which sets the output value. Rinse and repeat for the other values. The instruction cheat sheet I was working on didn’t include it, but you can apparently use rem $t0, $a0, $t3 instead of using div and mfhi like I do. Also, there is a beqz and bnez instructions that branch if the first argument is equal (not equal) to zero. So I could have used them rather than setting one register to be a constant 0.

So this approach to the solution can be rewritten more concisely as:

.globl is_leap_year

is_leap_year:
        rem $t0, $a0, 400
        beqz $t0, return_true
        rem $t0, $a0, 100
        beqz $t0, return_false
        rem $t0, $a0, 4
        beqz $t0, return_true

return_false:
        li $v0, 0
        j end_jump

return_true:
        li $v0, 1

end_jump:
        jr $ra

Clojure

Finally, we come to clojure. OK, so I’ve solved this exercise in a number of languages at this point, and the approach I take is normally broadly what I did with MIPS: if it’s divisible by 400 return true, if divisible by 100 return false, etc etc. So I decided to take a different approach. The trick is to notice that there are three relevant properties to check:

  • year divisible by 4?
  • year divisible by 100?
  • year divisible by 400?

And, given that each property entails the properties above it, a year is a leap year if an odd number of the properties hold of it. That is, either all three are true, or only the first one is. And that’s how I approached my clojure solution:

(ns leap)
(defn leap-year? [year] ;; <- argslist goes here
  (odd? (reduce + (map (fn [x] (if (= (rem year x) 0) 1 0)) [400 100 4])))
)

So, (rem year x) returns the remainder when dividing year by x. We then check if the remainder is equal to zero, (if (= (rem year x) 0) 1 0) and return 1 if the remainder is zero, 0 otherwise. We need to run this function for the several values [400 100 4], so we use a map: (map (fn [x] (if (= (rem year x) 0) 1 0)) [400 100 4]) to map that function over that list. This will give us a three element list of 0s and 1s. Now we just need to sum the elements of that list (using reduce) and then check if the output is odd (using odd?).

Conclusion

I don’t really know whether I will shoot for gold every week (it’s already Wednesday of week 2, and I’m only just writing up my notes on week 1) but I’ll probably try to do at least bronze most weeks. Will I write up what I did each week? The evidence of my blogging schedule up til now suggests that I will not.

© Seamus Bradley 2021–3

Powered by Hugo & Kiss.