Skip to content

Commit f2ca903

Browse files
brandenc40siriak
andauthored
Optimize roman to int and adde int to roman (#353)
Co-authored-by: Andrii Siriak <[email protected]>
1 parent 1fce86c commit f2ca903

File tree

4 files changed

+154
-29
lines changed

4 files changed

+154
-29
lines changed

conversion/integertoroman.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package conversion
2+
3+
import (
4+
"errors"
5+
)
6+
7+
var (
8+
// lookup arrays used for converting from an int to a roman numeral extremely quickly.
9+
r0 = []string{"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"} // 1 - 9
10+
r1 = []string{"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"} // 10 - 90
11+
r2 = []string{"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"} // 100 - 900
12+
r3 = []string{"", "M", "MM", "MMM"} // 1,000 - 3,000
13+
)
14+
15+
// IntToRoman converts an integer value to a roman numeral string. An error is
16+
// returned if the integer is not between 1 and 3999.
17+
func IntToRoman(n int) (string, error) {
18+
if n < 1 || n > 3999 {
19+
return "", errors.New("integer must be between 1 and 3999")
20+
}
21+
// Concatenate strings for each of 4 lookup array categories.
22+
//
23+
// Key behavior to note here is how math with integers is handled. Values are floored to the
24+
// nearest int, not rounded up. For example, 26/10 = 2 even though the actual result is 2.6.
25+
//
26+
// For example, lets use an input value of 126:
27+
// `r3[n%1e4/1e3]` --> 126 % 10_000 = 126 --> 126 / 1_000 = 0.126 (0 as int) --> r3[0] = ""
28+
// `r2[n%1e3/1e2]` --> 126 % 1_000 = 126 --> 126 / 100 = 1.26 (1 as int) --> r2[1] = "C"
29+
// `r1[n%100/10]` --> 126 % 100 = 26 --> 26 / 10 = 2.6 (2 as int) --> r1[2] = "XX"
30+
// `r0[n%10]` --> 126 % 10 = 6 --> r0[6] = "VI"
31+
// FINAL --> "" + "C" + "XX" + "VI" = "CXXVI"
32+
//
33+
// This is efficient in Go. The 4 operands are evaluated,
34+
// then a single allocation is made of the exact size needed for the result.
35+
return r3[n%1e4/1e3] + r2[n%1e3/1e2] + r1[n%100/10] + r0[n%10], nil
36+
}

conversion/integertoroman_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package conversion
2+
3+
import "testing"
4+
5+
func TestIntToRoman(t *testing.T) {
6+
for expected, input := range romanTestCases {
7+
out, err := IntToRoman(input)
8+
if err != nil {
9+
t.Errorf("IntToRoman(%d) returned an error %s", input, err.Error())
10+
}
11+
if out != expected {
12+
t.Errorf("IntToRoman(%d) = %s; want %s", input, out, expected)
13+
}
14+
}
15+
_, err := IntToRoman(100000)
16+
if err == nil {
17+
t.Errorf("IntToRoman(%d) expected an error", 100000)
18+
}
19+
_, err = IntToRoman(0)
20+
if err == nil {
21+
t.Errorf("IntToRoman(%d) expected an error", 0)
22+
}
23+
}
24+
25+
func BenchmarkIntToRoman(b *testing.B) {
26+
b.ReportAllocs()
27+
for i := 0; i < b.N; i++ {
28+
_, _ = IntToRoman(3999)
29+
}
30+
}

conversion/romantointeger.go

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,52 @@
66

77
package conversion
88

9-
var romans = map[string]int{"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000}
9+
import (
10+
"errors"
11+
"strings"
12+
)
1013

11-
func RomanToInteger(roman string) int {
12-
total := 0
13-
romanLen := len(roman)
14-
for holder := range roman {
15-
if holder+1 < romanLen && romans[string(roman[holder])] < romans[string(roman[holder+1])] {
16-
total -= romans[string(roman[holder])]
17-
} else {
18-
total += romans[string(roman[holder])]
14+
// numeral describes the value and symbol of a single roman numeral
15+
type numeral struct {
16+
val int
17+
sym string
18+
}
19+
20+
// lookup array for numeral values sorted by largest to smallest
21+
var nums = []numeral{
22+
{1000, "M"},
23+
{900, "CM"},
24+
{500, "D"},
25+
{400, "CD"},
26+
{100, "C"},
27+
{90, "XC"},
28+
{50, "L"},
29+
{40, "XL"},
30+
{10, "X"},
31+
{9, "IX"},
32+
{5, "V"},
33+
{4, "IV"},
34+
{1, "I"},
35+
}
36+
37+
// RomanToInteger converts a roman numeral string to an integer. Roman numerals for numbers
38+
// outside the range 1 to 3,999 will return an error. Nil or empty string return 0
39+
// with no error thrown.
40+
func RomanToInteger(input string) (int, error) {
41+
if input == "" {
42+
return 0, nil
43+
}
44+
var output int
45+
for _, n := range nums {
46+
for strings.HasPrefix(input, n.sym) {
47+
output += n.val
48+
input = input[len(n.sym):]
1949
}
2050
}
21-
return total
51+
// if we are still left with input string values then the
52+
// input was invalid and the bool is returned as false
53+
if len(input) > 0 {
54+
return 0, errors.New("invalid roman numeral")
55+
}
56+
return output, nil
2257
}

conversion/romantointeger_test.go

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,52 @@ package conversion
22

33
import "testing"
44

5-
type romanToIntegerConversionTest struct {
6-
input string
7-
expected int
8-
name string
9-
}
10-
11-
var romanToIntegerTests = []romanToIntegerConversionTest{
12-
{input: "DCCLXXXIX", expected: 789, name: "DCCLXXXIX-789"},
13-
{input: "MLXVI", expected: 1066, name: "MLXVI-1066"},
14-
{input: "MCMXVIII", expected: 1918, name: "MCMXVIII-1918"},
15-
{input: "V", expected: 5, name: "V-5"},
5+
var romanTestCases = map[string]int{
6+
"I": 1, "II": 2, "III": 3, "IV": 4, "V": 5, "VI": 6,
7+
"VII": 7, "VIII": 8, "IX": 9, "X": 10, "XI": 11, "XII": 12,
8+
"XIII": 13, "XIV": 14, "XV": 15, "XVI": 16, "XVII": 17,
9+
"XVIII": 18, "XIX": 19, "XX": 20, "XXXI": 31, "XXXII": 32,
10+
"XXXIII": 33, "XXXIV": 34, "XXXV": 35, "XXXVI": 36, "XXXVII": 37,
11+
"XXXVIII": 38, "XXXIX": 39, "XL": 40, "XLI": 41, "XLII": 42,
12+
"XLIII": 43, "XLIV": 44, "XLV": 45, "XLVI": 46, "XLVII": 47,
13+
"XLVIII": 48, "XLIX": 49, "L": 50, "LXXXIX": 89, "XC": 90,
14+
"XCI": 91, "XCII": 92, "XCIII": 93, "XCIV": 94, "XCV": 95,
15+
"XCVI": 96, "XCVII": 97, "XCVIII": 98, "XCIX": 99, "C": 100,
16+
"CI": 101, "CII": 102, "CIII": 103, "CIV": 104, "CV": 105,
17+
"CVI": 106, "CVII": 107, "CVIII": 108, "CIX": 109, "CXLIX": 149,
18+
"CCCXLIX": 349, "CDLVI": 456, "D": 500, "DCIV": 604, "DCCLXXXIX": 789,
19+
"DCCCXLIX": 849, "CMIV": 904, "M": 1000, "MVII": 1007, "MLXVI": 1066,
20+
"MCCXXXIV": 1234, "MDCCLXXVI": 1776, "MMXXI": 2021, "MMDCCCVI": 2806,
21+
"MMCMXCIX": 2999, "MMM": 3000, "MMMCMLXXIX": 3979, "MMMCMXCIX": 3999,
1622
}
1723

1824
func TestRomanToInteger(t *testing.T) {
19-
20-
for _, test := range romanToIntegerTests {
21-
convertedValue := RomanToInteger(test.input)
22-
if convertedValue != test.expected {
23-
t.Errorf(
24-
"roman to integer test %s failed. expected '%d' but got '%d'",
25-
test.name, test.expected, convertedValue,
26-
)
25+
for input, expected := range romanTestCases {
26+
out, err := RomanToInteger(input)
27+
if err != nil {
28+
t.Errorf("RomanToInteger(%s) returned an error %s", input, err.Error())
2729
}
30+
if out != expected {
31+
t.Errorf("RomanToInteger(%s) = %d; want %d", input, out, expected)
32+
}
33+
}
34+
_, err := RomanToInteger("IVCMXCIX")
35+
if err == nil {
36+
t.Error("RomanToInteger(IVCMXCIX) expected an error")
37+
}
38+
39+
val, err := RomanToInteger("")
40+
if val != 0 {
41+
t.Errorf("RomanToInteger(\"\") = %d; want 0", val)
42+
}
43+
if err != nil {
44+
t.Errorf("RomanToInteger(\"\") returned an error %s", err.Error())
45+
}
46+
}
47+
48+
func BenchmarkRomanToInteger(b *testing.B) {
49+
b.ReportAllocs()
50+
for i := 0; i < b.N; i++ {
51+
_, _ = RomanToInteger("MMMCMXCIX")
2852
}
2953
}

0 commit comments

Comments
 (0)