/now
projects
ramblings
smol projects

get notes distribution

14.11.2023 4 min read

Create a function that takes an array of students and returns an object representing their notes distribution. Keep in mind that all invalid notes should not be counted in the distribution. Valid notes are: 1, 2, 3, 4, 5

getNotesDistribution([
  {
    "name": "Steve",
    "notes": [5, 5, 3, -1, 6]
  },
  {
    "name": "John",
    "notes": [3, 2, 5, 0, -3]
  }
] ➞ {
  5: 3,
  3: 2,
  2: 1
})

In Crystal, we have two options: we can create a Class or a Struct. The difference is that a Struct is immutable, and is allocated on the stack. Using a Struct will provide better performance if we’re just passing small copies around.

require "spec"

struct Person
  property name
  property notes

  def initialize(@name : String, @notes : Array(Int32))
  end
end

def get_notes_distribution(people : Array(Person))
  h = Hash(Int32, Int32).new
  n = [] of Int32
  people.each { |p| n.concat(p.notes) }
  n.reject! { |i| i <= 0 || i > 5 }.each do |i|
    h[i] = h.has_key?(i) ? (h[i] += 1) : (h[i] = 1)
  end
  return h
end

get_notes_distribution([
  Person.new(name = "Steve", notes = [5, 5, 3, -1, 6]),
  Person.new(name = "John", notes = [3, 2, 5, 0, -3]),
]).should eq Hash{5 => 3, 3 => 2, 2 => 1}

Nim:

import std/[tables, sequtils]

type Person = object
    name: string
    notes: seq[int]

proc getNotesDistribution(people: seq[Person]): Table[int, int] =
    var t = initTable[int, int]()
    var a: seq[int] = @[]
    for person in people:
        a = a.concat(person.notes)
    a = a.filterIt(it > 0 and it <= 5)
    for note in a:
        if t.hasKey(note):
            t[note].inc
        else:
            t[note] = 1
    return t

assert getNotesDistribution(@[
    Person(name: "Steve", notes: @[5, 5, 3, -1, 6]),
    Person(name: "John", notes: @[3, 2, 5, 0, -3]),
]) == {5: 3, 3: 2, 2: 1}.toTable

Not too much of a difference here, except we use a Table (it’s analogous to a hash).

Raku:

use Test;

class Person {
    has str $.name;
    has int @.notes;
}

sub get-notes-distribution(Person @ppl) {
    my Int %h = %{};
    my Int @a = [];
    @a.append($_.notes) for @ppl;
    @a = @a.grep({$_ > 0 && $_ <= 5});
    for @a -> $n {
        %h{$n} = %h{$n}:exists ?? (%h{$n} += 1) !! (%h{$n} = 1);
    }
    %h;
}

my Person @ppl = [
    Person.new(name => "Steve", notes => [5, 5, 3, -1, 6]),
    Person.new(name => "John", notes => [3, 2, 5, 0, -3])
];

ok get-notes-distribution(@ppl) == {5 => 3, 3 => 2, 2 => 1};

Last one, Javascript:

import deepEqual from "deep-equal";
import assert from "./assert";

type Person = {
  name: string;
  notes: number[];
};

function getNotesDistribution(people: Person[]) {
  return people.reduce((acc, curr) => {
    const notes = curr.notes.filter((i) => i > 0 && i <= 5);
    for (const note of notes) {
      acc[note] = acc[note] ? (acc[note] += 1) : (acc[note] = 1);
    }
    return acc;
  }, {} as Record<string, number>);
}

const result = getNotesDistribution([
  {
    name: "Steve",
    notes: [5, 5, 3, -1, 6],
  },
  {
    name: "John",
    notes: [3, 2, 5, 0, -3],
  },
]);

const expected = {
  5: 3,
  3: 2,
  2: 1,
};

assert(deepEqual(result, expected));

And we’re done! …or so I thought. I realized that my Javascript implementation was quite different from the other implementations, in that it was more “functional”. So, I decided to revisit the implementations for each…

I don’t think Crystal’s reduce works the same way as Javascript’s, but we have each_with_object, which seems to be analogous:

def get_notes_distribution_func(people : Array(Person))
  notes = people.map { |p| p.notes }.flatten.select { |n| n > 0 && n <= 5 }
  return notes.each_with_object(Hash(Int32, Int32).new) do |el, acc|
    next if acc.has_key?(el)
    acc[el] = notes.count { |i| i == el }
  end
end

I thought it was going to be difficult in Nim, but no:

proc getNotesDistributionFunc(people: seq[Person]): CountTable[int] =
    people.mapIt(it.notes).foldl(a & b).filterIt(it > 0 and it <= 5).toCountTable

Lucky for us, we have toCountTable.

Last one in Raku:

sub get-notes-distribution-func(Person @ppl) {
    my @n = @ppl.reduce({$^a.notes.append($^b.notes)}).grep({$_ > 0 && $_ <= 5});
    my %h = do for @n { $_ => @n.grep({$_}).elems }
    return %h;
}

Overall, quite a fun one.

Built with Astro and Tailwind 🚀