I will walk you through another Code Wars Python challenge. The next challenge I received was ranked 5kyu, considerably harder than the 7kyu and 8kyu challenges I received earlier but still fairly quick challenge.
Our challenge is to take 3 numbers representing Red Green Blue values and convert it to standard web hexadecimal format. This challenge brings in a few considerations we have to deal with.
As always, I recommend breaking our challenge down into smaller problems.
- Process the three input values
- Validate and restrain incoming parameters to 0-255
- Convert decimal to hexadecimal
- Ensure each hexadecimal color has two characters
- Concatenate results
While there are only five sub problems I can identify, we will run into a challenges accomplishing them.
The first thing I wanted to tackle is finding a way to efficiently process the three parameters. I felt a for in loop would handle this very well as I can process all three with the same core code. This is called the dry principal which stands for do not repeat yourself.
Using the same code in different areas of your application is asking for bugs. The dry principal means you always try to eliminate duplicate code, typically during a refactor but if you look out for areas where you may do this, you can usually avoid it on the first draft.
def rgb(r, g, b): for value in (r, g, b): pass
This is fairly simple but clever code that uses a potentially lesser known feature of Python where you can iterate a tuple.
A tuple in Python is like an immutable list. An immutable object cannot be changed once it has been assigned. As an immutable object, Python can make certain optimizations that increase the performance of tuples over lists. In this situation either would have worked fine, I just opt'd for a tuple as I knew it would not change.
At this point we have an efficient way to apply our code to all three parameters with one block of code.
I know we need to output a string, so we need to create a variable that can hold the string. We also need to use the hex function to convert the numbers to hexadecimal. We have other requirements as well, but let's hold off on those for now.
def rgb(r, g, b): output = '' for value in (r, g, b): output += hex(value) return output
Here we make our first attempt at converting the incoming RGB (Red Green Blue) values to a hexadecimal string. We process each parameter individually and concatenate the result into our output string. When the loop is finished, we simply return the string.
As you can see from the test output we have a similar problem as we did in our previous challenge. The hex() function returns a string with "0x" in front that denotes a hexadecimal value. From the previous challenge we know can remove this by using string slicing. Let's do that now.
def rgb(r, g, b): output = '' for value in (r, g, b): output += hex(value)[2:] return output
To remove the leading two characters from the hex() output, we used the [2:] string slicing notation. We want to start at the 2nd index (3rd character as we start with 0) and go to the end of the string. Let's see how this looks.
We are getting further along, but now we found a few challenges I warned you about. The first is we have input values that result in a single character hexadecimal output. The second is the tests assume the output is in upper case. Since the uppercase is an easier fix, let's do that now.
def rgb(r, g, b): output = '' for value in (r, g, b): output += hex(value)[2:].upper() return output
Just calling the string function upper() at the end of our string solves this problem nicely. Let's tackle the more difficult problem, making sure we are outputting two characters per hexadecimal color regardless of the input.
def rgb(r, g, b): output = '' for value in (r, g, b): output += hex(value)[2:].upper().zfill(2) return output
Ok, I lied. It wasn't really that difficult. There is a string function called zfill that will fill a string with 0's until it is as long as your parameter. In this case we are filling each result to be at least 2 characters. If you have two characters already, it will do nothing.
Another thing you might have noticed here is I am stringing along functions. This is called Method Chaining and is really just syntactical sugar, but it makes for cleaner and easier to read code.
We now have one final problem with our code, we are not validating and restraining our input to be limited to 0-255 decimal. This is a relatively easy problem to solve using min() and max() function.
def rgb(r, g, b): output = '' for value in (r, g, b): output += hex(max(0, min(255,value)))[2:].upper().zfill(2) return output
The code we use for this is
max(0, min(255,value)), this will first take the minimum value between 255 and our input value. If the input is 254, we use that, if it is 256, we limit it to 255. This handles our upper end, we now need to handle the lower end. If you look at the initial tests supplied, we will be fed a negative number.
If we take the min(255, value) and use that as one parameter to the max() function and 0 as the second. We do this to ensure the number cannot go below 0, and as long as it is 0 or above, the output from
min(255,value) will be used. This second parameter is properly clamped to 255 or lower. By combining these both, we are able to clamp our number to the required 0-255 range.
Let's test our code and see if we can pass the tests.
All five tests passed successfully. Now we can submit the solution and see how it does more aggressive testing.
All tests passed!
All that is left (and this is really the more important step) is to compare our solution with others.
While there are a lot of solutions provided, I generally only look at the first few solutions. The first one is very clever but difficult to read and understand quickly. The second solution is verbose, not technically a problem but will be more difficult to understand and ultimately maintain.
I think our solution is a better solution than both of these, so I consider that a very successful challenge.
This challenge was a lot harder than the previous ones as it was ranked 5kyu instead of 7kyu, but was still relatively easy and resulted in only a few lines of code. Usually you cannot achieve this succinct code until you refactor, but the way I approached the problem allowed us to reach a fairly optimal result on the first attempt. I will cover a common solution to this problem in a future post.
As always, let me know in the comment section if you like this type of post and want me to continue this series.