diff --git a/README.md b/README.md index ae246c7..dcfdaa0 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ There are four choices: 1. Addition 2. Subtraction 3. Multiplication -4. Mixed +4. Division +5. Mixed ## Requirements [python3](https://www.python.org/downloads/) @@ -34,7 +35,7 @@ pip install -r requirements.txt ## How to Use 1. Generate the worksheet in pdf format with the following command: ``` -python3 run.py --type [+|-|x|mix] --digits [1|2|3] [-q|--question_count] [int] --output [custom-name.pdf] +python3 run.py --type [+|-|x|/|mix] --digits [1|2|3] [-q|--question_count] [int] --output [custom-name.pdf] ``` 2. Print out the generated file `worksheet.pdf` @@ -70,7 +71,6 @@ I appreciate all suggestions or PRs which will help kids learn math better. Feel ## TODO 1. Add date/name/score section to the front page -2. Add support for division in long division format ## Special Thanks My long time friend San for the inspiration of this project and lovely sons Tim and Hin. Thanks [thedanimal](https://github.com/thedanimal) for reviewing this README and adding new features. @@ -92,4 +92,6 @@ Thank you for your coverage. [Real Python Facebook](https://www.facebook.com/LearnRealPython/posts/1688239528018053?__tn__=-R) -[Github Trends Telegram](https://t.me/githubtrending/9007) \ No newline at end of file +[Github Trends Telegram](https://t.me/githubtrending/9007) + +[Python Trending Twitter](https://twitter.com/pythontrending/status/1316659466935373826) \ No newline at end of file diff --git a/division.png b/division.png new file mode 100644 index 0000000..1188aac Binary files /dev/null and b/division.png differ diff --git a/run.py b/run.py index 7af3a23..b2428a3 100644 --- a/run.py +++ b/run.py @@ -7,6 +7,7 @@ import argparse import random from fpdf import FPDF +from functools import reduce from typing import List, Tuple QuestionInfo = Tuple[int, str, int, int] @@ -32,6 +33,23 @@ def __init__(self, type_: str, max_number: int, question_count: int): self.font_1 = 'Times' self.font_2 = 'Arial' + # From https://stackoverflow.com/questions/6800193/what-is-the-most-efficient-way-of-finding-all-the-factors-of-a + # -number-in-python + def factors(self, n: int): + return set(reduce(list.__add__, + ([i, n//i] for i in range(1, int(n**0.5) + 1) if n % i == 0))) + + def division_helper(self, num) -> [int, int, int]: + # prevent num = 0 or divisor = 1 or divisor = dividend + factor = 1 + while not num or factor == 1 or factor == num: + num = random.randint(0, self.max_number) + # pick a factor of num; answer will always be an integer + if num: + factor = random.sample(self.factors(num), 1)[0] + answer = int(num / factor) + return [num, factor, answer] + def generate_question(self) -> QuestionInfo: """Generates each question and calculate the answer depending on the type_ in a list To keep it simple, number is generated randomly within the range of 0 to 100 @@ -40,7 +58,7 @@ def generate_question(self) -> QuestionInfo: num_1 = random.randint(0, self.max_number) num_2 = random.randint(0, self.max_number) if self.main_type == 'mix': - current_type = random.choice(['+', '-', 'x']) + current_type = random.choice(['+', '-', 'x', '/']) else: current_type = self.main_type @@ -52,6 +70,9 @@ def generate_question(self) -> QuestionInfo: answer = num_1 - num_2 elif current_type == 'x': answer = num_1 * num_2 + elif current_type == '/': + num_1, num_2, answer = self.division_helper(num_1) + else: raise RuntimeError(f'Question main_type {current_type} not supported') return num_1, current_type, num_2, answer @@ -94,40 +115,63 @@ def split_arr(self, x: int, y: int): quotient, remainder = divmod(x, y) if remainder != 0: return [y] * quotient + [remainder] - else: - return [y] * quotient + + return [y] * quotient def print_top_row(self, question_num: str): """Helper function to print first character row of a question row""" self.pdf.set_font(self.font_1, size=self.middle_font_size) self.pdf.cell(self.pad_size, self.pad_size, txt=question_num, border='LT', align='C') - self.pdf.cell(self.size, self.pad_size, border='T', align='C') - self.pdf.cell(self.size, self.pad_size, border='T', align='C') - self.pdf.cell(self.pad_size, self.pad_size, border='TR', align='C') + self.pdf.cell(self.size, self.pad_size, border='T') + self.pdf.cell(self.size, self.pad_size, border='T') + self.pdf.cell(self.pad_size, self.pad_size, border='TR') def print_second_row(self, num: int): """Helper function to print second character row of a question row""" self.pdf.set_font(self.font_2, size=self.large_font_size) - self.pdf.cell(self.pad_size, self.size, border='L', align='C') - self.pdf.cell(self.size, self.size, align='C') + self.pdf.cell(self.pad_size, self.size, border='L') + self.pdf.cell(self.size, self.size) self.pdf.cell(self.size, self.size, txt=str(num), align='R') - self.pdf.cell(self.pad_size, self.size, border='R', align='C') + self.pdf.cell(self.pad_size, self.size, border='R') + + def print_second_row_division(self, num_1: int, num_2: int): + """Helper function to print second character row of a question row for division""" + self.pdf.set_font(self.font_2, size=self.large_font_size) + self.pdf.cell(self.pad_size, self.size, border='L') + self.pdf.cell(self.size, self.size, txt=str(num_2), align='R') + x_cor = self.pdf.get_x() - 3 + y_cor = self.pdf.get_y() + self.pdf.image(name='division.png', x=x_cor, y=y_cor) + self.pdf.cell(self.size, self.size, txt=str(num_1), align='R') + self.pdf.cell(self.pad_size, self.size, border='R') def print_third_row(self, num: int, current_type: str): """Helper function to print third character row of a question row""" - self.pdf.set_font(self.font_2, size=self.large_font_size) - self.pdf.cell(self.pad_size, self.size, border='L', align='C') + self.pdf.cell(self.pad_size, self.size, border='L') self.pdf.cell(self.size, self.size, txt=current_type, align='L') self.pdf.cell(self.size, self.size, txt=str(num), align='R') - self.pdf.cell(self.pad_size, self.size, border='R', align='C') + self.pdf.cell(self.pad_size, self.size, border='R') + + def print_third_row_division(self): + """Helper function to print third character row of a question row for division""" + self.pdf.cell(self.pad_size, self.size, border='L') + self.pdf.cell(self.size, self.size, align='L') + self.pdf.cell(self.size, self.size, align='R') + self.pdf.cell(self.pad_size, self.size, border='R') def print_bottom_row(self): """Helper function to print bottom row of question""" - self.pdf.set_font(self.font_2, size=self.large_font_size) - self.pdf.cell(self.pad_size, self.size, border='LB', align='C') - self.pdf.cell(self.size, self.size, border='TB', align='C') - self.pdf.cell(self.size, self.size, border='TB', align='R') - self.pdf.cell(self.pad_size, self.size, border='BR', align='C') + self.pdf.cell(self.pad_size, self.size, border='LB') + self.pdf.cell(self.size, self.size, border='TB') + self.pdf.cell(self.size, self.size, border='TB') + self.pdf.cell(self.pad_size, self.size, border='BR') + + def print_bottom_row_division(self): + """Helper function to print bottom row of question""" + self.pdf.cell(self.pad_size, self.size, border='LB') + self.pdf.cell(self.size, self.size, border='B') + self.pdf.cell(self.size, self.size, border='B') + self.pdf.cell(self.pad_size, self.size, border='BR') def print_edge_vertical_separator(self): """Print space between question for the top or bottom row""" @@ -139,7 +183,7 @@ def print_middle_vertical_separator(self): def print_horizontal_separator(self): """Print line breaker between two rows of questions""" - self.pdf.cell(self.size, self.size, align='C') + self.pdf.cell(self.size, self.size) self.pdf.ln() def print_question_row(self, data, offset, num_problems): @@ -149,15 +193,24 @@ def print_question_row(self, data, offset, num_problems): self.print_edge_vertical_separator() self.pdf.ln() for x in range(0, num_problems): - self.print_second_row(data[x + offset][0]) + if data[x + offset][1] == '/': + self.print_second_row_division(data[x + offset][0], data[x + offset][2]) + else: + self.print_second_row(data[x + offset][0]) self.print_middle_vertical_separator() self.pdf.ln() for x in range(0, num_problems): - self.print_third_row(data[x + offset][2], data[x + offset][1]) + if data[x + offset][1] == '/': + self.print_third_row_division() + else: + self.print_third_row(data[x + offset][2], data[x + offset][1]) self.print_middle_vertical_separator() self.pdf.ln() - for _ in range(0, num_problems): - self.print_bottom_row() + for x in range(0, num_problems): + if data[x + offset][1] == '/': + self.print_bottom_row_division() + else: + self.print_bottom_row() self.print_edge_vertical_separator() self.pdf.ln() @@ -194,11 +247,12 @@ def main(type_, size, question_count, filename): parser.add_argument( '--type', default='+', - choices=['+', '-', 'x', 'mix'], + choices=['+', '-', 'x', '/', 'mix'], help='type of calculation: ' '+: Addition; ' '-: Subtraction; ' 'x: Multiplication; ' + '/: Division; ' 'mix: Mixed; ' '(default: +)', ) diff --git a/tests/test_math_worksheet_generator.py b/tests/test_math_worksheet_generator.py index b649590..9979f90 100644 --- a/tests/test_math_worksheet_generator.py +++ b/tests/test_math_worksheet_generator.py @@ -23,6 +23,11 @@ def test_generate_question_multiplication(self): question = g.generate_question() self.assertEqual(question[0] * question[2], question[3]) + def test_generate_question_division(self): + g = Mg(type_='/', max_number=9, question_count=10) + question = g.generate_question() + self.assertEqual(question[0] / question[2], question[3]) + def test_generate_question_unsupport_type_(self): g = Mg(type_='p', max_number=9, question_count=10) with self.assertRaisesRegex(RuntimeError, expected_regex=r"Question main_type p not supported"): @@ -40,6 +45,32 @@ def test_make_question_page_page_count(self): total_page = math.ceil(g.question_count / (g.num_x_cell * g.num_y_cell)) self.assertEqual(total_page, g.pdf.page) + def test_factors_two_digits(self): + g = Mg(type_='x', max_number=9, question_count=2) + expect_factors = {1, 2, 4, 13, 26, 52} + self.assertEqual(expect_factors, g.factors(52)) + + def test_factors_three_digits(self): + g = Mg(type_='x', max_number=9, question_count=2) + expect_factors = {1, 2, 3, 4, 6, 12, 73, 146, 219, 292, 438, 876} + self.assertEqual(expect_factors, g.factors(876)) + + def test_division_helper_zero_input(self): + g = Mg(type_='x', max_number=9, question_count=2) + division_info = g.division_helper(0) + self.assertNotEqual(0, division_info[0]) + + def test_division_helper_divisor_not_equal_one_nor_dividend(self): + g = Mg(type_='x', max_number=9, question_count=2) + division_info = g.division_helper(876) + self.assertNotEqual(1, division_info[0]) + self.assertNotEqual(division_info[2], division_info[0]) + + def test_division_helper_divisor_answer_type_is_int(self): + g = Mg(type_='x', max_number=9, question_count=2) + division_info = g.division_helper(876) + self.assertIs(type(division_info[2]), int) + if __name__ == '__main__': unittest.main()