Let’s start with a helper function that returns the number of characters that compose a number.
function howManyChars(num::Int)::Int
return num |> string |> length
end
The function is quite simple, it sends (|>
) a number (num
) to string
(converts a number to its textual representation) and redirects the result (|>
) to length
. I find this form clearer that the equivalent length(string(num))
or string(num) |> length
or (length ∘ string)(num)
(∘
is a function composition operator that you obtain by typing \circ
and pressing Tab).
Time for a test run.
map(howManyChars, [5, -8, -11, 303])
[1, 2, 3, 3]
Appears to be working fine.
Now let’s write a function that takes a vector of integers and returns the number of characters in the longest of them (we will need it to determine the width of the stem later on).
function getMaxLengthOfNum(nums::Vec{Int})::Int
maxLen::Int = map(howManyChars, nums) |> maximum
return max(2, maxLen)
end
Note. Instead of
map(howManyChars, nums)
above we could have just usedmap(length ∘ string, nums)
. This would save us some typing (no need to definehowManyChars
in the first place), but made the code a bit more cryptic at first read.
Again, a piece of cake, we just use map
to apply howManyChars
to every number in a vector (nums
) and get the length of the longest number by sending (|>
) the lengths to maximum
. Notice, that the function doesn’t return the expected maxLen
. This is because in a moment, we will write getStemAndLeaf(num::Int, maxLenOfNum::Int)
that brakes a number into two parts: stem and leaf. It will require maxLenOfNum
to be at least 2 (so that at least one digit serves as a stem and one as a leaf), hence return max(2, maxLen)
.
function getStemAndLeaf(num::Int, maxLenOfNum::Int)::Tuple{Str, Str}
@assert maxLenOfNum > 1 "maxLenOfNum must be greater than 1"
@assert howManyChars(num) <= maxLenOfNum
"character count in num must be <= maxLenOfNum"
numStr::Str = lpad(abs(num), maxLenOfNum, "0")
stem::Str = numStr[1:end-1] |> string
leaf::Str = numStr[end] |> string
stem = parse(Int, stem) |> string #1
stem = num < 0 ? "-" * stem : stem #2
stem = lpad(stem, maxLenOfNum-1, " ") #3
return (stem, leaf)
end
We begin with lpad
. This function converts its first input (abs(num)
) to string of a given length (maxLenOfNum
). It adds a padding ("0"
) to the left side of the result (if necessary) in order to obtain the string with a desired number of characters. Next, we proceed to obtain the stem
which contains all the characters from numStr
, except the last one (end-1
). The |> string
makes sure that the end result is Str
(since, e.g. stem
from "21"
would be '2'
which is of type Char
). Similarly, we produce leaf
by taking the last character of numStr
. We could stop here, and it would likely work fine for a positive integer. However, handling broader range of inputs (num
and maxLenOfNum
) requires some further stem
processing. Hence the lines designated with #1-#3
that were added in later iterations of getStemAndLeaf
.#1
removes superfluous 0s from the left side of the string (e.g. "001"
becomes "1"
and "00"
becomes "0"
). #2
adds "-"
sign if the input (num
) was negative. #3
aligns the text (stem
) to the right. It does so by adding spaces (" "
) to the left site of stem
. All that’s left to do is to return
our stem
and leaf
and see how it works for some exemplary inputs.
Dict(n => getStemAndLeaf(n, 3) for n in [-12, -3, 3, 8, 10, 145])
Dict{Int64, Tuple{String, String}} with 6 entries:
-12 => ("-1", "2")
10 => (" 1", "0")
145 => ("14", "5")
-3 => ("-0", "3")
8 => (" 0", "8")
3 => (" 0", "3")
Time to write getLeafCounts
a function that for a vector of numbers returns a mapping (Dict
) between stems (keys) and leaves (values).
# returns Dict{stem, [leaves]}
function getLeafCounts(nums::Vec{Int},
maxLenOfNum::Int)::Dict{Str, Vec{Str}}
@assert length(unique(nums)) > 1 "numbers musn't be the same"
counts::Dict{Str, Vec{Str}} = Dict()
for num in nums
stem, leaf = getStemAndLeaf(num, maxLenOfNum) # for's local vars
if haskey(counts, stem)
counts[stem] = push!(counts[stem], leaf)
else
counts[stem] = [leaf]
end
end
return counts
end
First, we initialize an empty Dict
(counts
) that will hold our result. Next, we brake each number (for num in nums
) into stem
and leaf
parts. If the counts
the already contains such a stem
(haskey(counts, stem)
), then we add the leaf
to the vector of already existing leaves (push!(counts[stem], leaf)
). Otherwise (else
), we add a leaf
as a 1-element vector ([leaf
]) for a given stem
. Finally, we return the counts
.
Let’s see how it works.
# prime numbers below 100
primesLeafCounts = getLeafCounts(
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47,
53, 59, 61, 67, 71, 73, 79, 83, 89, 97],
2
)
Dict{String, Vector{String}} with 10 entries:
"8" => ["3", "9"]
"4" => ["1", "3", "7"]
"1" => ["1", "3", "7", "9"]
"5" => ["3", "9"]
"0" => ["2", "3", "5", "7"]
"2" => ["3", "9"]
"6" => ["1", "7"]
"7" => ["1", "3", "9"]
"9" => ["7"]
"3" => ["1", "7"]
Looks, alright. Time to pretty print the result. First, let’s get a formatted row.
function getStemLeafRow(key::Str, leafCounts::Dict{Str, Vec{Str}})::Str
row::Str = key * "|"
if haskey(leafCounts, key)
row *= sort(leafCounts[key]) |> join
end
return row * "\n"
end
We define our row
as a string that contains the key
and separator. If our leafCounts
contains a given key
then we append its sorted values concatenated together with join
function (e.g., ["1", "1", "3"] |> join
becomes "113"
). We return row
with a newline character (\n
).
Time for the whole stem and leaf plot.
function getStemLeafPlot(nums::Vec{Int})::Str
maxLenOfNum::Int = getMaxLengthOfNum(nums)
leafCounts::Dict{Str, Vec{Str}} = getLeafCounts(nums, maxLenOfNum)
low::Int, high::Int = extrema(nums)
testedStems::Dict{Str, Bool} = Dict()
result::Str = ""
for num in low:1:high
stem, _ = getStemAndLeaf(num, maxLenOfNum)
if haskey(testedStems, stem)
continue
end
result *= getStemLeafRow(stem, leafCounts)
testedStems[stem] = true
end
return result
end
At the onset, we define a few variables. Some of them deserve a short explanation. low
and high
are the two extrema
(minimum and maximum) of our numbers (nums
). testedStems
will contain the keys from leafCounts
, i.e. the stems from our stem-leaf plot that rows has been already obtained. Next, we use for
loop to travel through all the numbers in our range (low
to high
). For each tested number (num
) we get its stem
. If the stem
was already obtained (if haskey
) we continue
to another for
loop iteration. Otherwise, we add the row to our result
(result *= getStemLeafRow
) and insert the stem
among the already visited (testedStems[stem] = true
). When we finish we return the whole stem-leaf-plot (return result
).
And that’s it. Let’s see how it works on Wikipedia’s examples. First, prime numbers below 100:
getStemLeafPlot(stemLeafTest1)
0|2357
1|1379
2|39
3|17
4|137
5|39
6|17
7|139
8|39
9|7
Now, the numbers from the Construction section:
getStemLeafPlot(stemLeafTest2)
4|4679
5|
6|34688
7|2256
8|148
9|
10|6
All that’s left to do is to adjust our function for the example with floats.
function getStemLeafPlot(nums::Vec{Flt})::Str
ints::Vec{Int} = round.(Int, nums)
return getStemLeafPlot(ints)
end
And voila:
getStemLeafPlot(stemLeafTest3)
-2|4
-1|2
-0|3
0|466
1|7
2|5
3|
4|
5|7
It appears to be working as intended so I think we can finish here.