Skip to content

Commit 692e549

Browse files
colinleachBethanyG
andauthored
[All Your Base] Approaches Doc (#3652)
* [All Your Base] draft approaches doc * Cleaned up the language a bit and added a couple of examples. * Added deque ref at end of doc. --------- Co-authored-by: BethanyG <BethanyG@users.noreply.github.com>
1 parent 35c5616 commit 692e549

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"introduction": {
3+
"authors": ["colinleach",
4+
"BethanyG"],
5+
"contributors": []
6+
}
7+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Introduction
2+
3+
The main aim of `All Your Base` is to understand how non-negative integers work in different bases.
4+
Given that mathematical understanding, implementation can be relatively straightforward.
5+
6+
7+
For this approach and its variations, no attempt was made to benchmark performance as this would distract from the main focus of writing clear, correct code for conversion.
8+
9+
10+
## General guidance
11+
12+
All successful solutions for base conversion involve three steps:
13+
14+
1. Check that inputs are valid (no non-integer or negative values).
15+
2. Convert the input list to a Python `int`, per the examples given in the instructions.
16+
3. Convert the `int` from step 2 into an output list in the new base.
17+
18+
Some programmers prefer to separate the two conversions into separate functions, others put everything in a single function.
19+
This is largely a matter of taste, and either structure can be made reasonably concise and readable.
20+
21+
22+
## 1. Checking the inputs
23+
24+
Solution code should check that the input base is at least 2, and that the output base is 2 or greater.
25+
Bases outside the range should rase `ValueError`s for input base and output base respectively.
26+
27+
```python
28+
if input_base < 2:
29+
raise ValueError("input base must be >= 2")
30+
31+
if not all( 0 <= digit < input_base for digit in digits) :
32+
raise ValueError("all digits must satisfy 0 <= d < input base")
33+
34+
if not output_base >= 2:
35+
raise ValueError("output base must be >= 2")
36+
37+
```
38+
39+
Additionally, all input numbers should be positive integers greater or equal to 0 and strictly less than the given number base.
40+
For the familiar base-10 system, that would mean 0 to 9.
41+
As implemented, the tests require that invalid inputs raise a `ValueError` with "all digits must satisfy 0 <= d < input base" as an error message.
42+
43+
44+
## 2. Convert the input digits to an `int`
45+
46+
The next step in the conversion process requires that the input list of numbers be converted into a single integer.
47+
The four code fragments below all show variations of this conversion:
48+
49+
```python
50+
# Simple loop
51+
value = 0
52+
for digit in digits:
53+
value = input_base * value + digit
54+
55+
# Loop, separating the arithmetic steps
56+
value = 0
57+
for digit in digits:
58+
value *= input_base
59+
value += digit
60+
61+
# Sum a generator expression over reversed digits
62+
value = sum(digit * input_base ** position for position, digit in enumerate(reversed(digits)))
63+
64+
# Sum a generator expression with alternative reversing
65+
value = sum(digit * (input_base ** (len(digits) - 1 - index)) for index, digit in enumerate(digits))
66+
```
67+
68+
In the first two, the `value *= input_base` step essentially left-shifts all the previous digits, and `value += digit` adds a new digit on the right.
69+
In the two generator expressions, an exponentation like `input_base ** position` left-shifts the current digit to the appropriate position in the output.
70+
71+
72+
````exercism/note
73+
74+
It is important to think about these procedures until they makes sense: these short code fragments are the main point of the exercise.
75+
In each code fragment, the Python `int` is called `value`, a deliberately neutral identifier.
76+
Surprisingly many students use names like `decimal` or `base10` for the intermediate value, which is misleading.
77+
78+
A Python `int` is an object with a complicated (but largely hidden) implementation.
79+
There are methods to convert an `int` to string representations such as octal, binary or hexadecimal, but these do not change the internal representation.
80+
````
81+
82+
83+
## 3. Convert the intermediate `int` to output digits
84+
85+
The `int` created in step 2 can now be reversed, using a different base.
86+
87+
Again, there are multiple code snippets shown below, which all do the same thing (essentially).
88+
In each case, we need the value and the remainder of integer division.
89+
The first snippet adds new digits at the start of the `list`, while the next two add them at the end.
90+
The final snippet uses [`collections.deque()`][deque] to prepend, then converts to a `list` in the `return` statement.
91+
92+
93+
These snippets represent choices of where to take the performance hit: appending to the end is a **much** faster and more memory efficient way to grow a `list` (O(1)), but the solution then needs an extra reverse step, incurring O(n) performance for the reversal.
94+
_Prepending_ to the `list` is very expensive, as every addition needs to move all other elements of the list "over" into new memory.
95+
The `deque` has O(1) prepends and appends, but then needs to be converted to a `list` before being returned, which is an O(n) operation.
96+
97+
98+
```python
99+
from collections import deque
100+
101+
102+
out = []
103+
104+
# Step forward, insert new digits at index 0 (front of list).
105+
# Least performant, and not recommended for large amounts of data.
106+
while value > 0:
107+
out.insert(0, value % output_base)
108+
value = value // output_base
109+
110+
# Append values to the end (mor efficient), then reverse the list.
111+
while value:
112+
out.append(value % output_base)
113+
value //= output_base
114+
out.reverse()
115+
116+
# Use divmod() and reverse list, same efficiency a above.
117+
while value:
118+
div, mod = divmod(value, output_base)
119+
out.append(mod)
120+
value = div
121+
out.reverse()
122+
123+
# Use deque() for effcient appendleft(), convert to list.
124+
converted_digits = deque()
125+
126+
while number > 0:
127+
converted_digits.appendleft(number % output_base)
128+
number = number // output_base
129+
130+
return list(converted_digits) or [0]
131+
```
132+
133+
134+
Finally, we return the digits just calculated.
135+
136+
A minor complication is that a zero value needs to be `[0]`, not `[]` according to the tests.
137+
Here, we cover this case in the `return` statement, but it could also have been trapped at the beginning of the program, with an early `return`:
138+
139+
140+
```python
141+
# return, with guard for empty list
142+
return out or [0]
143+
```
144+
145+
## Recursion option
146+
147+
An unusual solution to the two conversions is shown below.
148+
It works, and the problem is small enough to avoid stack overflow (Python has no tail recursion).
149+
150+
151+
In practice, few Python programmers would take this approach without carefully thinking about the bounds of the program and any possible memoization/performance optimizations they could take to avoid issues.
152+
While Python *allows* recursion, it does nothing to *encourage* it, and the default recursion limit is set to only 1000 stack frames.
153+
154+
155+
```python
156+
def base_to_dec(input_base, digits):
157+
if not digits:
158+
return 0
159+
return input_base * base_to_dec(input_base, digits[:-1]) + digits[-1]
160+
161+
162+
def dec_to_base(number, output_base):
163+
if not number:
164+
return []
165+
return [number % output_base] + dec_to_base(number // output_base, output_base)
166+
```
167+
168+
[deque]: https://docs.python.org/3/library/collections.html#collections.deque
169+

0 commit comments

Comments
 (0)