diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 8104b3117f..b8d23d4829 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -696,3 +696,36 @@ class TestDelay(BaseEvenniaTest): timedelay ) # Clock must advance to trigger, even if past timedelay self.assertEqual(self.char1.ndb.dummy_var, "dummy_func ran") + + +class TestIntConversions(TestCase): + def test_int2str(self): + self.assertEqual("three", utils.int2str(3)) + # special adjective conversion + self.assertEqual("3rd", utils.int2str(3, adjective=True)) + # generic adjective conversion + self.assertEqual("5th", utils.int2str(5, adjective=True)) + # No mapping return int as str + self.assertEqual("15", utils.int2str(15)) + + def test_str2int(self): + # simple conversions + self.assertEqual(5, utils.str2int("5")) + + # basic mapped numbers + self.assertEqual(3, utils.str2int("three")) + self.assertEqual(20, utils.str2int("twenty")) + + # multi-place numbers + self.assertEqual(2345, utils.str2int("two thousand, three hundred and forty-five")) + + # ordinal numbers + self.assertEqual(1, utils.str2int("1st")) + self.assertEqual(1, utils.str2int("first")) + self.assertEqual(4, utils.str2int("fourth")) + # ordinal sound-change conversions + self.assertEqual(5, utils.str2int("fifth")) + self.assertEqual(20, utils.str2int("twentieth")) + + with self.assertRaises(ValueError): + utils.str2int("not a number") \ No newline at end of file diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 1fed76df21..6c4b1a6df6 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -2755,3 +2755,87 @@ def int2str(number, adjective=False): if adjective: return _INT2STR_MAP_ADJ.get(number, f"{number}th") return _INT2STR_MAP_NOUN.get(number, str(number)) + +_STR2INT_MAP = { + "one": 1, "two": 2, "three": 3, + "four": 4, "five": 5, "six": 6, + "seven": 7, "eight": 8, "nine": 9, + "ten": 10, "eleven": 11, "twelve": 12, + "thirteen": 13, "fourteen": 14, "fifteen": 15, + "sixteen": 16, "seventeen": 17, "eighteen": 18, + "nineteen": 19, "twenty": 20, "thirty": 30, + "forty": 40, "fifty": 50, "sixty": 60, + "seventy": 70, "eighty": 80, "ninety": 90, + "hundred": 100, "thousand": 1000, +} +_STR2INT_ADJS = { + "first": 1, "second": 2, "third": 3, +} +def str2int(number): + """ + Converts a string to an integer. + + Args: + number (str): The string to convert. It can be a digit such as "1", or a number word such as "one". + + Returns: + int: The string represented as an integer. + """ + number = str(number) + original_input = number + try: + # it's a digit already + return int(number) + except: + # if it's an ordinal number such as "1st", it'll convert to int with the last two characters chopped off + try: + return int(number[:-2]) + except: + pass + + # convert sound changes for generic ordinal numbers + if number[-2:] == "th": + # remove "th" + number = number[:-2] + if number[-1] == "f": + # e.g. twelfth, fifth + number = number[:-1] + "ve" + elif number[-2:] == "ie": + # e.g. twentieth, fortieth + number = number[:-2] + "y" + # custom case for ninth + elif number[-3:] == "nin": + number += "e" + + if i := _STR2INT_MAP.get(number): + # it's a single number, return it + return i + + # remove optional "and"s + number = number.replace(" and "," ") + + # split number words by spaces, hyphens and commas, to accommodate multiple styles + numbers = [ word.lower() for word in re.split(r'[-\s\,]',number) if word ] + sums = [] + for word in numbers: + # check if it's a known number-word + if i := _STR2INT_MAP.get(word): + if not len(sums): + # initialize the list with the current value + sums = [i] + else: + # if the previous number was smaller, it's a multiplier + # e.g. the "two" in "two hundred" + if sums[-1] < i: + sums[-1] = sums[-1]*i + # otherwise, it's added on, like the "five" in "twenty five" + else: + sums.append(i) + elif i := _STR2INT_ADJS.get(word): + # it's a special adj word; ordinal case will never be a multiplier + sums.append(i) + else: + # invalid number-word, raise ValueError + raise ValueError(f"String {original_input} cannot be converted to int.") + return sum(sums) +