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 definehowManyCharsin 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.