Codewars: Highest Lowest
In this little assignment you are given a string of space separated numbers, and have to return the highest and lowest number.
Examples
HighAndLow("1 2 3 4 5") // return "5 1"
HighAndLow("1 2 -3 4 5") // return "5 -3"
HighAndLow("1 9 3 4 -5") // return "9 -5"
Notes
- All numbers are valid
Int32
, no need to validate them. - There will always be at least one number in the input string.
- Output string must be two numbers separated by a single space, and highest number is first.
First Thoughts
- I’m being given a string, so I’m going to need to split them into tokens (individual pieces).
- It’s unfortunate that one of the limits isn’t the range of the numbers. I see single digit numbers in the example, but I don’t know that they’ll always be single digit.
- I could have used that to determine that strings with >1 characters are negative.
- I could have used that to leverage the ASCII value (https://www.asciitable.com/) of the number strings (e.g., 1 is 49, 2 is 50, 3 is 51).
- I understand getting the input as a string (can’t control that, maybe) but it’s odd to me that we’re smashing them back into a string to return them. A slice of integers would be more useful. But this isn’t a real function so critiquing it is a little silly. 😄
- Double checking what the range on
Int32
is since it says that’s the type. Googled “int32 golang” to visit the first match at https://www.educative.io/edpresso/what-is-type-int32-in-golang and get the answer “A variable of typeint32
can store integers ranging from-2147483648
to2147483647
.”
Okay, so on to the quick solution…
Googled “golang split string” and found https://yourbasic.org/golang/split-string-into-slice/ with an interesting little candy graphic and three ways to split. I’ll take strings.Split
for the return of []string
.
package kata
import "strings"
func HighAndLow(in string) string {
// divide the string into little bitty pieces
tokens := strings.Slice(in)
}
Now to turn those into a []Int32
so I can sort them - how about creating an empty one, iterating over the elements in []string
and adding them to the new one? I already know how big the []Int32
is (same as the []string
) so I can help it along by setting that as the size/capacity of the new one with make
. Of course I can’t remember the syntax for make, so on to google with “golang make” which gives me https://go.dev/tour/moretypes/13 (which is a lovely site in general - great examples).
package kata
import "strings"
func HighAndLow(in string) string {
// divide the string into little bitty pieces
tokens := strings.Slice(in)
// convert the slice from strings to integers
nums := make([]Int32, 0, len(tokens))
for _, t := range tokens {
nums = append(nums, t)
}
}
Alright, on to sorting! Googling “golang sort slice numbers” and it’s another hit on yourbasic.org with https://yourbasic.org/golang/how-to-sort-in-go/. I’m starting to enjoy these wild graphics at the top of these posts. Anyway, it looks like sort.Ints
is the ticket and it sorts them in-place (as opposed to returning a new sorted slice). I wasn’t totally sure about the “in-place” terminology so I googled “function sort in-place” and it looks like that’s the name folx are using.
package kata
import "strings"
func HighAndLow(in string) string {
// divide the string into little bitty pieces
tokens := strings.Slice(in)
// convert the slice from strings to integers
nums := make([]Int32, 0, len(tokens))
for _, t := range tokens {
nums = append(nums, t)
}
// this sorts them in-place, destroying the previous order
sort.Ints(nums)
}
So now if we started with 1 9 3 4 -5
we should now have -5 1 3 4 9
but they’re in a slice and not a squished-together string like the function is expecting. Time to do some squishing. My first though is a “join” where the pieces are strung together with whatever we want to join them on. In this case it’d be a space.
Back to google with “join slice elements string golang” and the first answer is https://appdividend.com/2020/04/16/how-to-join-strings-in-golang-go-string-join-example/. It looks like there’s strings.Join
but that wants a slice of strings. That’d require us to convert them back to strings before we did that which would involve another loop. We might go that route, but let’s see if there’s something that will handle non-strings first.
Googling “concatenate integers into string golang” gave me an interesting post about combining a string and integer, but that’s not helpful here. There’s a StackOverflow post about strings.Join
being 3x faster than fmt.Sprintf
for stringing things together of different types.0
Googling “convert int slice to string golang” lead to a spicy argument about a clever method that used strings.Trim
, strings.Replace
, and fmt.Sprint
that I couldn’t even wrap my head around. But the answer just below that looks like the ticket. My first thought was to create another specifically-sized slice and append to it, but I keep forgetting that if you know that you’re going to be processing X elements, you can just write to them using the index of the slice like this…
And just as I was about to write that code I remembered they only want the highest and lowest, so we don’t have to convert the entire thing. Once we know that the lowest is at the front and the highest is at the end, a fmt.Sprintf
would be perfect. It’ll give us the new string X Y
where X
is the highest and Y
is the lowest.
package kata
import (
"strings"
"fmt"
)
func HighAndLow(in string) string {
// divide the string into little bitty pieces
tokens := strings.Slice(in)
// convert the slice from strings to integers
nums := make([]Int32, 0, len(tokens))
for _, t := range tokens {
nums = append(nums, t)
}
// this sorts them in-place, destroying the previous order
sort.Ints(nums)
// return our highest (end of the slice), and our lowest (beginning of the slice)
return fmt.Sprintf("%d %d", nums[-1], nums[0])
}
I hadn’t been testing this as I wrote it, so let’s see what bugs have crept in.
# codewarrior/kata
./solution.go:7:13: undefined: strings.Slice
./solution.go:10:18: undefined: Int32
./solution.go:16:3: undefined: sort
./solution.go:18:10: undefined: fmt
Alright, one at a time…
./solution.go:7:13: undefined: strings.Slice
Okay, right, there’s no strings.Slice
. It’s strings.Split
. I googled for strings.Slice
and was very confused when I couldn’t find it. I finally googled “strings golang pkg” so I could look at the actual package documentation at https://pkg.go.dev/strings and then it hit me. Whoops.
./solution.go:10:18: undefined: Int32
I think I grabbed Int32
because that was what was in the instructions. The actual types are lowercase, so switching that to int32
after googling “int32 golang” and seeing sample code at https://www.educative.io/edpresso/what-is-type-int32-in-golang with the lowercase version.
./solution.go:16:3: undefined: sort
./solution.go:18:10: undefined: fmt
These are both the same issue, so I’ll bundle them. I didn’t add those packages to the import statement. That’s one thing about Codewars that’s a little awkward. With an IDE like Visual Studio Code (or any of them) goimports
(I think that’s what does it now? It was gofmt
previously.) automatically handles the import statements for me as I use packages in my code provided it already knows about them and I’ve only used out-of-the-box packages. So I have to manually adjust the import to include them.
Alright, all together now!
package kata
import (
"fmt"
"strings"
"sort"
)
func HighAndLow(in string) string {
// divide the string into little bitty pieces
tokens := strings.Split(in)
// convert the slice from strings to integers
nums := make([]int32, 0, len(tokens))
for _, t := range tokens {
nums = append(nums, t)
}
// this sorts them in-place, destroying the previous order
sort.Ints(nums)
// return our highest (end of the slice), and our lowest (beginning of the slice)
return fmt.Sprintf("%d %d", nums[-1], nums[0])
}
Okay, more errors!
# codewarrior/kata
./solution.go:11:26: not enough arguments in call to strings.Split
have (string)
want (string, string)
./solution.go:16:18: cannot use t (type string) as type int32 in append
./solution.go:20:12: cannot use nums (type []int32) as type []int in argument to sort.Ints
./solution.go:23:35: invalid slice index -1 (index must be non-negative)
One at a time again…
./solution.go:11:26: not enough arguments in call to strings.Split
have (string)
want (string, string)
Split wants two strings, but it doesn’t say what those two strings are. I assume one of them is what we want to split. The other is the delimiter probably? It needs to know how to split them up. It could be commas, spaces, tabs, or something else. I don’t have any pop-up help without being in an editor (at least I don’t think?). I already have the strings
package documentation open from the lookup about strings.Slice
not being a real thing. Checking out that function at https://pkg.go.dev/strings#Split tells me the signature is this:
func Split(s, sep string) []string
Alright, the second parameter, then, for the space.
tokens := strings.Split(in, " ")
Next error!
./solution.go:16:18: cannot use t (type string) as type int32 in append
./solution.go:20:12: cannot use nums (type []int32) as type []int in argument to sort.Ints
I’m going to take these together because they’re related. It’s complaining that the things I’m trying to cram into that new []int32
slice aren’t int32
. Well, that makes sense. I forgot to convert them first. Whoops. But then it also complains that I can’t use sort.Ints
to sort int32
values. That makes sense. It’s sort of in the name sort.Ints
. I vaguely recall that the int
type changes based on the architecture of the machine. Or it applies to all integers, or something.
Googling “golang int” and getting our old friend A Tour of Go at https://go.dev/tour/basics/11. The bit as the bottom jives with what I remembered about architecture.
The
int
,uint
, anduintptr
types are usually 32 bits wide on 32-bit systems and 64 bits wide on 64-bit systems. When you need an integer value you should useint
unless you have a specific reason to use a sized or unsigned integer type.
So we were assured by the instructions that the numbers would fit inside an int32
which means they’ll also fit into an int64
(because that’s a bigger range) so int
it is then!
// convert the slice from strings to integers
nums := make([]int, 0, len(tokens))
That doesn’t account for the fact that I’m not actually converting anything when I append
then, so let’s take care of that with a conversion from the strconv
package at https://pkg.go.dev/strconv. The one I’m looking at is Atoi
(mnemonic: alpha to integer, even though the documentation says string to integer and never actually explains where they got the “A” from) which is documented right at the top:
i, err := strconv.Atoi("-42")
s := strconv.Itoa(-42)
The instructions assured us that all of the things they’re giving us fit into int32
and that we don’t need to validate that, but since it’s the Go thing to handle all errors it doesn’t hurt us to check that we got an integer before appending it. What we do about that bad data would depend on where our function is going to live in our code, but most of the time returning an error up through the stack (to the code that called our code) makes the most sense. Since the tests aren’t expecting a string and an error I can’t adjust it to make it the way I want, but if I could, it’d look like this:
func HighAndLow(in string) (string, error)
That way I could return the answer they want, but I could also return an error if one of the bits of the string they sent me didn’t convert properly. But we can’t, so onward with just either puking (panic) entirely, or just skipping that portion of the string because it’s no good.
So our loop now looks like this. We’re panicking if we aren’t able to convert one of them to an int
and now the append
should be less grumpy about what we’re trying to do.
// convert the slice from strings to integers
nums := make([]int, 0, len(tokens))
for _, t := range tokens {
// attempt to convert alpha to integer
n, err := strconv.Atoi(t)
if err != nil {
panic(err)
}
// add the newly converted integer to the slice
nums = append(nums, n)
}
One more error to deal with and that’s the negative index (-1
) that I’m trying to use to get the last element.
./solution.go:31:35: invalid slice index -1 (index must be non-negative)
So you don’t have to scroll back, this is the part of the code it’s unhappy with. I know I want the last element, but -1
to work from the end backwards is not flying.
// return our highest (end of the slice), and our lowest (beginning of the slice)
return fmt.Sprintf("%d %d", nums[-1], nums[0])
Python would have been totally happy with that, but since we’re using Go, off to google “get last index value golang” which gives us another yourbasic.org match (Google must think we enjoy those fun blog headers) and one from the ever-present StackOverflow. The first article is this one that talks about doing it like this:
a := []string{"A", "B", "C"}
s := a[len(a)-1] // C
Alright, we get the length and then we subtract one. So if the length is 10, we subtract one and get 9 (because indexes start at zero). So with a 10-element slice that’d be 0, 1, 2, 3, 4, 5, 6, 7, 8, 9; making 9 the last one. Easy adjustment, let’s do it.
// return our highest (end of the slice), and our lowest (beginning of the slice)
return fmt.Sprintf("%d %d", nums[len(nums)-1], nums[0])
The tests seem to be happy with that, so that’s going to be our solution!
My Solution(s)
package kata
import (
"fmt"
"strconv"
"strings"
"sort"
)
func HighAndLow(in string) string {
// divide the string into little bitty pieces
tokens := strings.Split(in, " ")
// convert the slice from strings to integers
nums := make([]int, 0, len(tokens))
for _, t := range tokens {
// attempt to convert alpha to integer
n, err := strconv.Atoi(t)
if err != nil {
panic(err)
}
// add the newly converted integer to the slice
nums = append(nums, n)
}
// this sorts them in-place, destroying the previous order
sort.Ints(nums)
// return our highest (end of the slice), and our lowest (beginning of the slice)
return fmt.Sprintf("%d %d", nums[len(nums)-1], nums[0])
}
Best Practice Solution(s)
With 63 votes, this is the best practice solution!
package kata
import (
"strings"
"strconv"
"fmt"
)
func HighAndLow(in string) string {
var tmpH, tmpL int
for i, s := range strings.Fields(in) {
n, _ := strconv.Atoi(string(s))
if i == 0 {
tmpH = n
tmpL = n
}
if n > tmpH {
tmpH = n
}
if n < tmpL {
tmpL = n
}
}
return fmt.Sprintf("%d %d", tmpH, tmpL)
}
The Fields Function
The strings
package has a Fields
function that “splits the string s around each instance of one or more consecutive white space characters, as defined by unicode.IsSpace, returning a slice of substrings of s or an empty slice if s contains only white space.”
First, that’s cool and I’ll have to remember that for next time, but second, now that I think about it, if I had input like this it’s possible that I’d wind up with a slice one element larger than I expected.
"1 3 7 -8 4 9"
(notice the two spaces between 4
and 9
)
Without the extra space, I’d expect this to turn into []string{"1", "3", "7", "-8", "4", 9"}
, but with the extra space it’d turn into []string{"1", "3", "7", "-8", "4", "", "9"}
. Notice the extra element with nothing in it? That’s what is between those two spaces.
If we write a little test program on The Go Playground we can show that this is true. Note the %!V(string=)
portion between 4 and 9.
package main
import (
"fmt"
"strings"
)
func main() {
s := "1 3 7 -8 4 9"
ss := strings.Split(s, " ")
fmt.Printf("%V", ss)
}
// Output: [%!V(string=1) %!V(string=3) %!V(string=7) %!V(string=-8) %!V(string=4) %!V(string=) %!V(string=9)]
So by using Fields
we account for multiple types of whitespace and the possibility that there may be more than one of them in a row between the values we want.
Discarding the Error
Because the function has no error
return in its signature there’s really no reason to hold onto that error from strconv.Atoi
but because I prefer to make sure I deal with errors I’m going to stick with my panic
, but this is how you would ignore it with an underscore.
n, _ := strconv.Atoi(string(s))
One Less Package
Since the solution required us to report the lowest and the highest number and we don’t really need the rest of the numbers to be in order, this routine makes it so we’re only storing the lowest number on each loop. Here is that portion of code with some comments explaining what’s going on. By doing the comparisons on our own without sorting the entire slice, it saves us from having to import sort
at all.
// if we're on the first element (index 0), set both values the first element, whatever that may be
if i == 0 {
tmpH = n
tmpL = n
}
// if the current element is higher than the highest number we've seen, overwrite it
// note this is only possible on/after the second value
if n > tmpH {
tmpH = n
}
// if the current element is lower than the lowest number we've seen, overwrite it
// note: this is only possible on/after the second value
if n < tmpL {
tmpL = n
}
I’m not entirely sure it’s faster than what sort.Ints
can do, but it’s Sunday and poring over the source code is not on the menu. But if you’re feeling froggy, here it is:
https://cs.opensource.google/go/go/+/refs/tags/go1.18.1:src/sort/sort.go
Clever Solution(s)
The cleverest solution with a score of 66! Let’s look at it:
package kata
import (
"fmt"
"sort"
"strconv"
"strings"
)
func HighAndLow(in string) string {
numStrings := strings.Fields(in)
var nums = []int{}
for _, i := range numStrings {
j, _ := strconv.Atoi(i)
nums = append(nums, j)
}
sort.Ints(nums)
return fmt.Sprintf("%d %d", nums[len(nums)-1], nums[0])
}
Oh, actually, that’s exactly what I did if I a) hadn’t handled the error, b) used string.Fields
, and c) didn’t comment it at all. I usually don’t think of my brute force methods as clever, but I’ll take it. 😄