@@ 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()
@@ 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()