From 443711356cdfba08557b5d37dc6f6684c1f1f451 Mon Sep 17 00:00:00 2001 From: Dominic Ricottone Date: Wed, 14 Sep 2022 21:03:02 -0500 Subject: [PATCH] Solution --- .gitignore | 1 + Makefile | 12 +++ main.py | 216 +++++++++++++++++++++++++++++++++++++++++++++++++++++ test.py | 171 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 400 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 main.py create mode 100644 test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..df9b60f --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: clean +clean: + rm -rf __pycache__ + +.PHONY: test +test: + python -m unittest --verbose + +.PHONY: solve +solve: + python -m main + diff --git a/main.py b/main.py new file mode 100644 index 0000000..bbb72cd --- /dev/null +++ b/main.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 + +"""NUMBERPLAY +Given + (1) N != U != M != B != E != R != P != L != A != Y + (2) N != B != P != 0 +Find all valid solutions to NUM + BER = PLAY +""" + +class Solution(object): + def __init__( + self, + *, + n=None, + u=None, + m=None, + b=None, + e=None, + r=None, + p=None, + l=None, + a=None, + y=None, + carry=0 + ): + self._validate("n", n, allow_none=True) + self.n = n + self._validate("u", u, allow_none=True) + self.u = u + self._validate("m", m, allow_none=True) + self.m = m + self._validate("b", b, allow_none=True) + self.b = b + self._validate("e", e, allow_none=True) + self.e = e + self._validate("r", r, allow_none=True) + self.r = r + self._validate("p", p, allow_none=True) + self.p = p + self._validate("l", l, allow_none=True) + self.l = l + self._validate("a", a, allow_none=True) + self.a = a + self._validate("y", y, allow_none=True) + self.y = y + if carry not in [0,1]: + raise ValueError("carry must be 0 or 1") + self.carry = carry + + def __str__(self): + _n = str(self.n) if self.n is not None else "N" + _u = str(self.u) if self.u is not None else "U" + _m = str(self.m) if self.m is not None else "M" + _b = str(self.b) if self.b is not None else "B" + _e = str(self.e) if self.e is not None else "E" + _r = str(self.r) if self.r is not None else "R" + _p = str(self.p) if self.p is not None else "P" + _l = str(self.l) if self.l is not None else "L" + _a = str(self.a) if self.a is not None else "A" + _y = str(self.y) if self.y is not None else "Y" + return f"{_n}{_u}{_m} + {_b}{_e}{_r} = {_p}{_l}{_a}{_y}" + + def __getitem__(self, key): + if key == "n": + return self.n + elif key == "u": + return self.u + elif key == "m": + return self.m + elif key == "b": + return self.b + elif key == "e": + return self.e + elif key == "r": + return self.r + elif key == "p": + return self.p + elif key == "l": + return self.l + elif key == "a": + return self.a + elif key == "y": + return self.y + else: + raise AttributeError(f"no position '{key}'") + + def __setitem__(self, key, value): + if self[key] is not None: + raise AttributeError(f"position '{key}' is already set") + + self._validate(key, value) + + if key == "n": + self.n = value + elif key == "u": + self.u = value + elif key == "m": + self.m = value + elif key == "b": + self.b = value + elif key == "e": + self.e = value + elif key == "r": + self.r = value + elif key == "p": + self.p = value + elif key == "l": + self.l = value + elif key == "a": + self.a = value + elif key == "y": + self.y = value + else: + raise AttributeError(f"no position '{key}'") + + def copy(self): + """Duplicate this solution.""" + return Solution(n=self.n, u=self.u, m=self.m, b=self.b, e=self.e, + r=self.r, p=self.p, l=self.l, a=self.a, y=self.y, carry=self.carry) + + def is_using(self, n): + """Check if n is already used in the solution.""" + if not isinstance(n, int) or n < 0 or 9 < n: + raise ValueError("values must be integers between 0 and 9") + + used = [] + for position in ["n", "u", "m", "b", "e", "r", "p", "l", "a", "y"]: + try: + used.append(self[position]) + except AttributeError: + pass + + return n in used + + def _validate(self, position, value, allow_none=False): + """Validate a value for an position.""" + if not isinstance(value, int): + if allow_none == False: + raise ValueError("value must be an integer") + elif value is not None: + raise ValueError("value must be an integer or None") + else: + if value < 0 or 9 < value: + raise ValueError("value must be an integer between 0 and 9") + elif position in ["n", "b", "p"] and value == 0: + raise ValueError("value must be an integer between 1 and 9") + elif self.is_using(value): + raise ValueError("value must be unique") + +def add_digits(a, b, carry): + sum = a + b + carry + new_carry = (10 <= sum) + c = int(str(sum)[-1]) + return new_carry, c + +def solve_column(partial, r1, r2, r3): + solutions = [] + + for a in range(0,10): + if partial.is_using(a): + continue + elif r1 == "n" and a == 0: + continue + + for b in range(0,10): + if a == b or partial.is_using(b): + continue + elif r2 == "b" and b == 0: + continue + + new_carry, c = add_digits(a, b, partial.carry) + if a == c or b == c or partial.is_using(c): + continue + + new_solution = partial.copy() + new_solution[r1] = a + new_solution[r2] = b + new_solution[r3] = c + new_solution.carry = new_carry + solutions.append(new_solution) + + return solutions + +def main(): + count = 0 + + # P = 1 (no values satisfy a + b >= 2000 where a, b <= 999) + solution = Solution(p=1) + + # solve right column N U * + # + B E * + # ------- + # P L A * + for s1 in solve_column(solution, 'm', 'r', 'y'): + + # solve middle column N * M + # + B * R + # ------- + # P L * Y + for s2 in solve_column(s1, 'u', 'e', 'a'): + + # solve left column * U M + # + * E R + # ------- + # P * A Y + for s3 in solve_column(s2, 'n', 'b', 'l'): + + # now just filter to solutions that carry a 1 to P + if s3.carry == 1: + print(s3) + count += 1 + + print(f"Total: {count} solutions") + +if __name__ == "__main__": + main() diff --git a/test.py b/test.py new file mode 100644 index 0000000..bcfed77 --- /dev/null +++ b/test.py @@ -0,0 +1,171 @@ +import unittest + +import main + +class TestAddDigits(unittest.TestCase): + """add_digits(a, b, carry)""" + + def test_simple_add(self): + # only test 0-8, since adding 1 to 9 would carry + for a in range(0, 9): + with self.subTest(a=a): + self.assertEqual(main.add_digits(a, 1, 0), (0, a+1, )) + + def test_carry(self): + # only test 1-9, since adding 0 to 9 would not carry + for a in range(1,10): + with self.subTest(a=a): + self.assertEqual(main.add_digits(a, 9, 0)[0], 1) + + def test_large_add(self): + # only test 1-9, since this test uses a-1 + for a in range(1,10): + with self.subTest(a=a): + self.assertEqual(main.add_digits(a, 9, 0)[1], a-1) + + def test_large_add_alt(self): + for a in range(0,10): + with self.subTest(a=a): + self.assertEqual(main.add_digits(a, 9, 1)[1], a) + +class TestSolution(unittest.TestCase): + """Solution()""" + + def test_str(self): + s = main.Solution(n=1, u=2, m=3, b=4, e=5, r=6, p=7, l=8, a=9, y=0) + self.assertEqual(str(s), "123 + 456 = 7890") + + def test_copy(self): + s = main.Solution(n=1) + c = s.copy() + c['u'] = 2 + self.assertEqual(s.u, None) + self.assertEqual(c.u, 2) + + def test_get_wrong_name(self): + with self.assertRaises(AttributeError): + main.Solution()['x'] + + def test_set_wrong_name(self): + with self.assertRaises(AttributeError): + main.Solution()['x'] = 1 + + def test_set_wrong_name_wrong_value(self): + with self.assertRaises(AttributeError): + main.Solution()['x'] = None + + def test_raise_leading_0(self): + with self.subTest(a="n"): + with self.assertRaises(ValueError): + main.Solution(n=0) + with self.subTest(a="b"): + with self.assertRaises(ValueError): + main.Solution(b=0) + with self.subTest(a="p"): + with self.assertRaises(ValueError): + main.Solution(p=0) + + with self.assertRaises(ValueError): + main.Solution(m=10) + + def test_raise_10(self): + with self.assertRaises(ValueError): + main.Solution(m=10) + + def test_raise_neg1(self): + with self.assertRaises(ValueError): + main.Solution(m=-1) + + def test_raise_set_leading_0(self): + for a in ["n", "b", "p"]: + with self.subTest(a=a): + with self.assertRaises(ValueError): + main.Solution()[a] = 0 + + def test_raise_set_10(self): + with self.assertRaises(ValueError): + main.Solution()["m"] = 10 + + def test_raise_set_neg1(self): + with self.assertRaises(ValueError): + main.Solution()["m"] = -1 + + def test_raise_set_none(self): + with self.assertRaises(ValueError): + main.Solution()["m"] = None + +class TestIsUsed(unittest.TestCase): + """Solution().is_used(n)""" + + def test_raise_10(self): + with self.assertRaises(ValueError): + main.Solution().is_using(10) + + def test_raise_neg1(self): + with self.assertRaises(ValueError): + main.Solution().is_using(-1) + + def test_empty_solution(self): + for a in range(0, 10): + with self.subTest(a=a): + self.assertFalse(main.Solution().is_using(a)) + + def test_unused(self): + # only test 0-8, since this test uses a+1 + for a in range(0, 9): + with self.subTest(a=a): + self.assertFalse(main.Solution(m=a+1).is_using(a)) + + def test_unused_alt(self): + # only test 1-9, since this test uses a-1 + for a in range(1, 10): + with self.subTest(a=a): + self.assertFalse(main.Solution(m=a-1).is_using(a)) + + def test_copy_unused(self): + # only test 0-8, since this test uses a+1 + for a in range(0, 9): + with self.subTest(a=a): + s = main.Solution(m=a+1) + self.assertFalse(s.copy().is_using(a)) + + def test_updated_copy_unused(self): + # only test 0-7, since this test uses a+1 and a+2 + for a in range(0, 8): + with self.subTest(a=a): + s = main.Solution(m=a+1) + c = s.copy() + c['r'] = a+2 + self.assertFalse(c.is_using(a)) + + def test_used(self): + for a in range(0, 10): + with self.subTest(a=a): + self.assertTrue(main.Solution(m=a).is_using(a)) + + def test_copy_used(self): + for a in range(0, 10): + with self.subTest(a=a): + s = main.Solution(m=a) + self.assertTrue(s.copy().is_using(a)) + + def test_updated_copy_used(self): + # only test 0-8, since this test uses a+1 + for a in range(0, 9): + with self.subTest(a=a): + s = main.Solution(m=a+1) + c = s.copy() + c['r'] = a + self.assertTrue(c.is_using(a)) + +class TestCase1(unittest.TestCase): + def test_case(self): + """N32 + B57 = 1L89""" + s = main.Solution(u=3, m=2, e=5, r=7, p=1, a=8, y=9) + self.assertEqual(str(s), "N32 + B57 = 1L89") + result = main.solve_column(s, 'n', 'b', 'l') + should_be = ["432 + 657 = 1089","632 + 457 = 1089"] + self.assertEqual([str(r) for r in result], should_be) + +if __name__ == '__main__': + unittest.main() -- 2.45.2