Skip to content

Commit f94e8cd

Browse files
authored
[Roman Numerals] Approach docs fixes (#4289)
* various fixes * improve "see for details" lines & fix spacing * fully separate lists from paragraphs
1 parent c373a34 commit f94e8cd

9 files changed

Lines changed: 115 additions & 129 deletions

File tree

‎exercises/practice/roman-numerals/.approaches/config.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"uuid": "4ed8396c-f2c4-4072-abc9-cc8fe2780a5a",
2121
"slug": "loop-over-romans",
2222
"title": "Loop Over Romans",
23-
"blurb": "Test Roman Numerals from the largest down and eat the maximum possible at each step.",
23+
"blurb": "Test Roman numerals from the largest down and eat the maximum possible at each step.",
2424
"authors": [
2525
"BethanyG",
2626
"colinleach"

‎exercises/practice/roman-numerals/.approaches/if-else/content.md‎

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,27 @@ def roman(number):
1414
res = ''
1515

1616
if m > 0:
17-
res += m * 'M'
17+
res += m * 'M'
1818

1919
if 4 > c > 0:
20-
res += c * 'C'
20+
res += c * 'C'
2121
elif c == 4:
22-
res += 'CD'
22+
res += 'CD'
2323
elif 9 > c > 4:
24-
res += 'D' + ((c - 5) * 'C')
24+
res += 'D' + ((c - 5) * 'C')
2525
elif c == 9:
26-
res += 'CM'
26+
res += 'CM'
2727

28-
if 4 > x > 0:
28+
if 4 > x > 0:
2929
res += x * 'X'
3030
elif x == 4:
3131
res += 'XL'
3232
elif 9 > x > 4:
3333
res += 'L' + ((x - 5) * 'X')
3434
elif x == 9:
35-
res += 'XC'
35+
res += 'XC'
3636

37-
if 4 > i > 0:
37+
if 4 > i > 0:
3838
res += i * 'I'
3939
elif i == 4:
4040
res += 'IV'
@@ -61,19 +61,16 @@ This can be done with a list comprehension, left-padding with zeros as necessary
6161
digits = ([0, 0, 0, 0] + [int(d) for d in str(number)])[-4:]
6262
```
6363

64-
The blocks for hundreds, tens and units are all essentially the same, so we can put that code in a function.
64+
The blocks for hundreds, tens, and units are all essentially the same, so we can put that code in a function.
6565
We just need to pass in the digit, plus a tuple of translations for `(1, 4, 5, 9)` or their 10x and 100x equivalents.
6666

6767
It is also unnecessary to keep retesting the lower bounds within an `elif`, as the code line will only be reached if that is satisfied.
6868

69-
Using `return` instead of `elif` is a matter of personal preference.
7069
Given that, the code simplifies to:
7170

7271
```python
7372
def roman(number: int) -> str:
7473
def translate_digit(digit: int, translations: iter) -> str:
75-
assert isinstance(digit, int) and 0 <= digit <= 9
76-
7774
units, four, five, nine = translations
7875
if digit < 4:
7976
return digit * units
@@ -83,7 +80,6 @@ def roman(number: int) -> str:
8380
return five + (digit - 5) * units
8481
return nine
8582

86-
assert isinstance(number, int)
8783
m, c, x, i = ([0, 0, 0, 0] + [int(d) for d in str(number)])[-4:]
8884
res = ''
8985

@@ -99,5 +95,6 @@ def roman(number: int) -> str:
9995
return res
10096
```
10197

102-
The last few lines are quite similar and it would be possible to refactor them into a loop, but this is enough to illustrate the principle.
98+
(Using `return` instead of `elif` is a matter of personal preference.)
10399

100+
The last few lines are quite similar and it would be possible to refactor them into a loop, but this is enough to illustrate the principle.

‎exercises/practice/roman-numerals/.approaches/introduction.md‎

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,28 @@ In the version used for this exercise, the longest string needed to represent a
99
Minor variants of the system have been used which represent 4 as IIII rather than IV, allowing for longer strings, but those are not relevant here.
1010

1111
The system is inherently decimal: the number of human fingers has not changed since ancient Rome, nor the habit of using them for counting.
12-
However, there is no zero value available, so Roman numerals represent powers of 10 with different letters (I, X, C, M), not by position (1, 10, 100, 1000).
12+
However, there is no zero value available, so Roman numerals represent powers of 10 with different letters (I, X, C, and M), not by position (1, 10, 100, 1000, etc).
1313

1414
The approaches to this exercise break down into two groups, with many variants in each:
15+
1516
1. Split the input number into digits, and translate each separately.
1617
2. Iterate through the Roman numbers, from large to small, and convert the largest valid number at each step.
1718

1819
## Digit-by-digit approaches
1920

20-
The concept behind this class of approaches:
21-
1. Split the input number into decimal digits.
22-
2. For each digit, get the Roman equivalent and append to a list.
23-
3. Join the list into a string and return it.
21+
The process behind this class of approaches:
22+
23+
1. Split the input number into decimal digits.
24+
2. For each digit, get the Roman equivalent and append to a list.
25+
3. Join the list into a string and return it.
26+
2427
Depending on the implementation, there may need to be a list-reverse step.
2528

2629
### With `if` conditions
2730

2831
```python
2932
def roman(number: int) -> str:
30-
assert isinstance(number, int)
31-
3233
def translate_digit(digit: int, translations: iter) -> str:
33-
assert isinstance(digit, int) and 0 <= digit <= 9
34-
3534
units, four, five, nine = translations
3635
if digit < 4:
3736
return digit * units
@@ -54,44 +53,44 @@ def roman(number: int) -> str:
5453
return res
5554
```
5655

57-
See [`if-else`][if-else] for details.
56+
See the [`if-else`][if-else] approach for details.
5857

5958
### With table lookup
6059

6160
```python
6261
def roman(number):
63-
assert (number > 0)
64-
6562
# define lookup table (as a tuple of tuples, in this case)
6663
table = (
67-
("I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"),
68-
("X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"),
69-
("C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"),
70-
("M", "MM", "MMM"))
64+
('I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'),
65+
('X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'),
66+
('C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM'),
67+
('M', 'MM', 'MMM'))
7168

7269
# convert the input integer to a list of single digits
7370
digits = [int(d) for d in str(number)]
7471

75-
# we need the row in the lookup table for our most-significant decimal digit
76-
inverter = len(digits) - 1
72+
# get the row in the lookup table for the most-significant decimal digit
73+
inverter = len(digits) - 1
7774

7875
# translate decimal digits list to Roman numerals list
79-
roman_digits = [table[inverter - i][d - 1] for (i, d) in enumerate(digits) if d != 0]
76+
roman_digits = [table[inverter - i][d - 1] for i, d in enumerate(digits) if d != 0]
8077

8178
# convert the list of Roman numerals to a single string
8279
return ''.join(roman_digits)
8380
```
8481

85-
See [`table-lookup`][table-lookup] for details.
82+
See the [`table-lookup`][table-lookup] approach for details.
8683

8784

8885
## Loop over Romans approaches
8986

9087
In this class of approaches we:
91-
1. Create a mapping from Roman to Arabic numbers, in some suitable format. (_`dicts` or `tuples` work well_)
92-
2. Iterate nested loops, a `for` and a `while`, in either order.
93-
3. At each step, append the largest possible Roman number to a list and subtract the corresponding value from the number being converted.
94-
4. When the number being converted drops to zero, join the list into a string and return it.
88+
89+
1. Create a mapping from Roman to Arabic numbers, in some suitable format. (_`dicts` or `tuples` work well._)
90+
2. Iterate nested loops, a `for` and a `while`, in either order.
91+
3. At each step, append the largest possible Roman number to a list and subtract the corresponding value from the number being converted.
92+
4. When the number being converted drops to zero, join the list into a string and return it.
93+
9594
Depending on the implementation, there may need to be a list-reverse step.
9695

9796
This is one example using a dictionary:
@@ -105,15 +104,15 @@ def roman(number: int) -> str:
105104
result = ''
106105
while number:
107106
for arabic in ROMAN.keys():
108-
if number >= arabic:
107+
if number >= arabic:
109108
result += ROMAN[arabic]
110109
number -= arabic
111110
break
112111
return result
113112
```
114113

115114
There are a number of variants.
116-
See [`loop-over-romans`][loop-over-romans] for details.
115+
See the [`loop-over-romans`][loop-over-romans] approach for details.
117116

118117
## Other approaches
119118

@@ -152,7 +151,7 @@ This is a recursive version of the `loop-over-romans` approach, which only works
152151

153152
```python
154153
ARABIC_NUM = (1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1)
155-
ROMAN_NUM = ("M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I")
154+
ROMAN_NUM = ('M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I')
156155

157156
def roman(number: int) -> str:
158157
return roman_recur(number, 0, [])
@@ -167,7 +166,7 @@ def roman_recur(num: int, idx: int, digits: list[str]):
167166
return roman_recur(num, idx + 1, digits)
168167
```
169168

170-
See [`recurse-match`][recurse-match] for details.
169+
See the [`recurse-match`][recurse-match] approach for details.
171170

172171

173172
### Over-use a functional approach
@@ -176,7 +175,7 @@ See [`recurse-match`][recurse-match] for details.
176175
def roman(number):
177176
return ''.join(one*digit if digit<4 else one+five if digit==4 else five+one*(digit-5) if digit<9 else one+ten
178177
for digit, (one,five,ten)
179-
in zip([int(d) for d in str(number)], ["--MDCLXVI"[-i*2-1:-i*2-4:-1] for i in range(len(str(number))-1,-1,-1)]))
178+
in zip([int(d) for d in str(number)], ['--MDCLXVI'[-i*2-1:-i*2-4:-1] for i in range(len(str(number))-1,-1,-1)]))
180179
```
181180

182181
*This is Python, but not as we know it*.

‎exercises/practice/roman-numerals/.approaches/itertools-starmap/content.md‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
from itertools import starmap
55

66
def roman(number: int) -> str:
7-
orders = [(1000, "M "), (100, "CDM"), (10, "XLC"), (1, "IVX")]
8-
options = lambda I, V, X: ["", I, I * 2, I * 3, I + V, V, V + I, V + I * 2, V + I * 3, I + X]
7+
orders = [(1000, 'M '), (100, 'CDM'), (10, 'XLC'), (1, 'IVX')]
8+
options = lambda I, V, X: ['', I, I * 2, I * 3, I + V, V, V + I, V + I * 2, V + I * 3, I + X]
99
compute = lambda n, chars: options(*chars)[number % (n * 10) // n]
10-
return "".join(starmap(compute, orders))
10+
return ''.join(starmap(compute, orders))
1111
```
1212

1313
This approach is certainly concise and ingenious, though it takes functional programming to a level that some Python programmers might consider a little cryptic.
@@ -24,7 +24,7 @@ The underlying reason is that lambdas are intended to be anonymous functions emb
2424
Internally, they are all given the same name `<lambda>`, which can greatly complicate debugging.
2525
Their use is a particular bugbear of the Python track maintainer.
2626

27-
We can refactor the code to satisfy the linter by using named `def` statements, and lowercase argument names.
27+
We can refactor the code to satisfy the linter by using named `def` statements and lowercase argument names.
2828
Type hints are also added for documentation:
2929

3030
```python
@@ -33,21 +33,21 @@ from itertools import starmap
3333

3434
def roman(number: int) -> str:
3535
def options(i: str, v: str, x: str):
36-
return ["", i, i * 2, i * 3, i + v, v, v + i, v + i * 2, v + i * 3, i + x]
36+
return ['', i, i * 2, i * 3, i + v, v, v + i, v + i * 2, v + i * 3, i + x]
3737

3838
def compute(n: int, chars: str) -> iter:
3939
return options(*chars)[number % (n * 10) // n]
4040

41-
orders = [(1000, "M "), (100, "CDM"), (10, "XLC"), (1, "IVX")]
42-
return "".join(starmap(compute, orders))
41+
orders = [(1000, 'M '), (100, 'CDM'), (10, 'XLC'), (1, 'IVX')]
42+
return ''.join(starmap(compute, orders))
4343
```
4444

4545
## Analysis
4646

4747
The central concept is that Roman letters are defined for 1, 5 and 10, times various powers of 10.
4848

49-
`orders` is relatively straightforward: a list of tuples, with each tuple containing the powers of 10 and (as far as possible) the letters for that number times (1, 5, 10).
50-
Roman numerals for 5,000 and 10,000 are not defined, so are replaced here by spaces.
49+
`orders` is relatively straightforward: a list of tuples, with each tuple containing the powers of 10 and (as far as possible) the letters for that number times (1, 5, 10).
50+
Roman numerals for 5,000 and 10,000 are not defined, so spaces are used here instead.
5151

5252
The `options()` function just takes the three letters from one of these tuples and returns a list of numerals that can be constructed from them.
5353
For example, the 10 to 90 range:
@@ -60,15 +60,15 @@ options('X', 'L', 'C')
6060
There is no zero, so that is replaced by an empty string.
6161

6262
The `compute()` function takes a tuple from `orders` plus the top-level parameter `number`, and converts the appropriate decimal digit to its Roman equivalent, returning an `iterator`.
63-
For example the first digit of 723:
63+
For example, the first digit of 723:
6464

6565
```python
6666
number = 723
67-
[x for x in compute(100, "CDM")]
67+
[x for x in compute(100, 'CDM')]
6868
# => ['D', 'C', 'C']
6969
```
7070

71-
The `starmap()` function ties `orders`, `options()` and `compute` together, splitting up strings and tuples as necessary to give each function the parameters it needs.
71+
The `starmap()` function ties `orders`, `options()`, and `compute()` together, splitting up strings and tuples as necessary to give each function the parameters it needs.
7272
Again, an iterator is returned:
7373

7474
```python
@@ -79,7 +79,7 @@ number = 723
7979

8080
Finally, `''.join()` converts this iterator to a single string that can be returned as the desired answer.
8181

82-
Once we get past the deliberate obfuscation, it is quite an elegant approach.
82+
Once we get past the deliberate obfuscation, it is quite an elegant approach.
8383
Though perhaps not the most idiomatic Python.
8484

8585
## Credit

0 commit comments

Comments
 (0)