Skip to content

Commit c002817

Browse files
committed
[All Your Base] draft approaches doc
1 parent f1a064b commit c002817

File tree

2 files changed

+154
-0
lines changed

2 files changed

+154
-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: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Introduction
2+
3+
The main aim of this exercise is to understand how non-negative integers work in different bases.
4+
5+
Given that mathematical understanding, the code to implement it can be relatively simple.
6+
7+
For this exercise, no attempt was made to benchmark performance, as this would distract from the main focus of writing clear, correct code.
8+
9+
## General guidance
10+
11+
Essentially all succesful solutions involve three steps:
12+
13+
1. Check that inputs are valid.
14+
2. Convert the input list to a Python `int`.
15+
3. Convert that `int` to an output list in the new base.
16+
17+
Some programmers prefer to separate the two conversions into separate functions, others put everything in a single function.
18+
19+
This is largely a matter of taste, and either structure can be made reasonably concise and readable.
20+
21+
## 1. Check the inputs
22+
23+
```python
24+
if input_base < 2:
25+
raise ValueError("input base must be >= 2")
26+
27+
if not all( 0 <= digit < input_base for digit in digits) :
28+
raise ValueError("all digits must satisfy 0 <= d < input base")
29+
30+
if not output_base >= 2:
31+
raise ValueError("output base must be >= 2")
32+
33+
```
34+
35+
A valid number base must be `>=2` and all digits must be at least zero and strictly less than the number base.
36+
37+
For the familiar base-10 system, this means 0 to 9.
38+
39+
As implemented, the tests require that invalid input raise a `ValueError` with a suitable error message.
40+
41+
## 2. Convert the input digits to an `int`
42+
43+
These four code fragments all do essentially the same thing:
44+
45+
```python
46+
# Simplest loop
47+
val = 0
48+
for digit in digits:
49+
val = input_base * val + digit
50+
51+
# Loop, separating the arithmetic steps
52+
val = 0
53+
for digit in digits:
54+
val *= input_base
55+
val += digit
56+
57+
# Sum a comprehension over reversed digits
58+
val = sum(digit * input_base ** pos for pos, digit in enumerate(reversed(digits)))
59+
60+
# Sum a comprehension with alternative reversing
61+
val = sum((digit * (input_base ** (len(digits) - 1 - i)) for i, digit in enumerate(digits)))
62+
```
63+
64+
In the first two, the `val *= input_base` step essentially left-shifts all the previous digits, and `val += digit` adds a new digit on the right.
65+
66+
In the two comprehensions, an exponentation like `input_base ** pos` left-shifts the current digit to the appropriate position in the output.
67+
68+
*Please think about this until it makes sense:* these short code fragments are the main point of the exercise.
69+
70+
In each code fragment, the Python `int` is called `val`, a deliberately neutral identifier.
71+
72+
Surprisingly many students use names like `decimal` or `base10` for the intermediate value, which is misleading.
73+
74+
A Python `int` is an object with a complicated (but largely hidden) implementation.
75+
76+
There are methods to convert an `int` to string representations such as decimal, binary or hexadecimal, but the internal representation of `int` is certainly not decimal.
77+
78+
## 3. Convert the intermediate `int` to output digits
79+
80+
Now we have to reverse step 2, with a different base.
81+
82+
```python
83+
out = []
84+
85+
# Step forward, insert new digits at beginning
86+
while val > 0:
87+
out.insert(0, val % output_base)
88+
val = val // output_base
89+
90+
# Insert at end, then reverse
91+
while val:
92+
out.append(val % output_base)
93+
val //= output_base
94+
out.reverse()
95+
96+
# Use divmod()
97+
while val:
98+
div, mod = divmod(val, output_base)
99+
out.append(mod)
100+
val = div
101+
out.reverse()
102+
```
103+
104+
Again, there are multiple code snippets shown above, which all do the same thing.
105+
106+
In each case, we essentially need the value and remainder of an integer division.
107+
108+
The first snippet above adds new digits at the start of the list, while the next two add at the end.
109+
110+
This is a choice of where to take the performance hit: appending to the end is a faster way to grow the list, but needs an extra reverse step.
111+
112+
The choice of append-reverse would be obvious in Lisp or SML, but the difference is less important in Python.
113+
114+
```python
115+
# return, with guard for empty list
116+
return out or [0]
117+
```
118+
119+
Finally, we return the digits just calculated.
120+
121+
A minor complcation is that a zero value should be `[0]`, not `[]`.
122+
123+
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`.
124+
125+
## Recursion option
126+
127+
```python
128+
def base2dec(input_base: int, digits: list[int]) -> int:
129+
if not digits:
130+
return 0
131+
return input_base * base2dec(input_base, digits[:-1]) + digits[-1]
132+
133+
134+
def dec2base(number: int, output_base: int) -> list[int]:
135+
if not number:
136+
return []
137+
return [number % output_base] + dec2base(number // output_base, output_base)
138+
```
139+
140+
An unusual solution to the two conversions is shown above.
141+
142+
It works, and the problem is small enough to avoid stack overflow (Python has no tail recursion).
143+
144+
In practice, few Python programmers would take this approach in a language without the appropriate performance optimizations.
145+
146+
To simplify: Python only *allows* recursion, it does nothing to *encourage* it: in contrast to Scala, Elixir, and similar languages.
147+

0 commit comments

Comments
 (0)