get notes distribution
14.11.2023 4 min readCreate 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.