HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Multiplication or addition of decimals or integers for prehighschool; non deterministic testing

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
decimalsprehighschoolmultiplicationnonadditiontestingfordeterministicintegers

Problem

I'm creating a simple android app for pre-highschool students which teaches the very basics of addition or multiplication of integers or decimals.

The part of the program in this question is aimed at allowing them to practice without the need of a teacher giving them exercises and checking their results.

Note: This is the first time I m using unit tests so I'm not experienced at unit-testing at all.

The following are of great interest to me:

  • Unit tests:



  • What should I improve? I don't aim for 100% code coverage since that


feels more of a fixation on following blindly a "rule" without the
actual need for it (but of course I might be wrong).

  • Since my methods contain random.random() numbers I was thinking of creating tests that test a method millions or billions of times (e.g. assertIsInstance(_int_term(), int) inside a for _ in range(10**8) loop). This would very strongly indicate (but not with absolute certainty) that the methods work correctly. Should I create such tests?



  • Methods with optional parameters, e.g. Terms._int_term(max_val): The parameters are made optional only because I wanted to be able to test the method easily (that is, call it without the need to instantiate Terms() class. If I were not using unit tests, I would simply make it a non static method, and would remove the parameter completely. Is this a bad practice?



  • Docstrings: Are more docstrings needed? Or do good method names suffice? The reason I didn't create dosctings for everything is that they often go stale when I change code without also updating all the related docstrings.



Of course any other comments unrelated to the above 2 points are also very welcome.

Directory tree

  • project_dir



  • main.py



  • tests



  • test_main



  • test_questionandanswer.py



  • test_terms.py



Code

Main module:

```
"""
Used for the creation of "questions" for the user,
along with the expected answer (that is, the result of the operation)

Some examples of "questions" and their "answers":

"+

Solution

Style

Python has a style guide called PEP 8 which is definitly worth reading and and worth following if you do not have good reasons not to. In you case, your usage of empty lines for instance is not compliant to PEP8. You'll find tools online to check your code compliancy to PEP8 in a automated way if you want to.

Code organisation

You've written your code in classes which is sometimes a good thing. Sometimes, you don't need a class.

In any case, most of the things in the Terms class could be extracted out of it (also removing the need for many self.). By the way, it may be interesting to point out that variables defined at the class levels can be pretty complicated and are not entirely needed and could be simple global variables.

Also, you have variables that are not used at all and you should get rid of them.

Once everything than can be moved out of the class (and renamed) is moved out, you get:

```
"""
Used for the creation of "questions" for the user,
along with the expected answer (that is, the result of the operation)

Some examples of "questions" and their "answers":

"+2-4" , "-2"
"(-2)(+7)", "-14"
"-1.60-2.04", "-3.64"

The program should be aimed specifically at teaching
the very basics of addition or multiplication of integers or decimals,
in a scaling difficulty.

Exercises that deviate from those specific concepts should be avoided.
Examples of what should NOT be implemented:

"2-4", # Deviates (slightly) since it also teaches that 2 == +2
"-2(+7)", # Deviates (same as example above)
"+2-4(-5)", # Deviates since it combines addition and multiplication

"""

import decimal
from random import randint, choice, random, seed

MAX_ABS_VALUE = 10

def rand_int_term(max_val=MAX_ABS_VALUE):
return randint(0, max_val)

def rand_float_term(max_val=MAX_ABS_VALUE):
return random() * max_val

def create_term(term_without_sign):
"""
Creates a single term.

:param term_without_sign:
:return: (num)
"""
sign = choice(['-', '+'])

if sign == '-':
term = -1 * term_without_sign
else:
term = term_without_sign

return term

DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP = {
1: dict(
terms_count=2,
terms_type='int'
),
2: dict(
terms_count=3,
terms_type='int'
),
3: dict(
terms_count=2,
terms_type='float'
)
}

TOTAL_DIFFICULTY_LVLS = len(DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP)

class Terms(object):

def __init__(self, difficulty_lvl):
terms_data = DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP[difficulty_lvl]
self.terms_count = terms_data['terms_count']
self.terms_type = terms_data['terms_type']

def all_terms(self):
lst = []

if self.terms_type == 'int':
func = rand_int_term
elif self.terms_type == 'float':
func = rand_float_term
else:
raise NotImplemented('{}'.format(self.terms_type))

for _ in range(self.terms_count):
term_without_sign = func()
term = create_term(term_without_sign=term_without_sign)
lst.append(term)

return lst

class QuestionAndAnswer(object):
"""
Based on difficulty and operation type,
creates terms (either float or ints) which are then rounded to allow
specific number of decimals.
"""

OPERATIONS_TYPES = {'addition', 'multiplication'}

def __init__(self, difficulty_lvl, op_type):
self.terms_as_numbers = Terms(difficulty_lvl=difficulty_lvl).all_terms()
self.op_type = op_type

@staticmethod
def round_single_term_2_decimals(given_float):

num = decimal.Decimal(str(given_float))
return num.quantize(decimal.Decimal('.01'), decimal.ROUND_HALF_UP)

def terms_to_rounded_numbers(self):
"""
Ensures all terms have an appropriate number of decimals.

:return: (list)
"""

lst = []

for t in self.terms_as_numbers:

if isinstance(t, int):
lst.append(t)

else:
t = self.round_single_term_2_decimals(given_float=t)
lst.append(t)

return lst

def terms_as_strings(self):
lst = []

for t in self.terms_to_rounded_numbers():

if t > 0:
lst.append('+{}'.format(t))

elif t == 0:
random_sign = choice(['+', '-'])
lst.append('{sign}{term}'.format(sign=random_sign, term=t))

else:
lst.append(str(t))

return lst

def operation_str(self):
"""
Creates the operation string presented to the user.

:return: (str)
"""

terms_as_strings = self.terms_as_strings()

if self.op_type == 'addition':
op_str = ''.join(terms_as_strings)

elif self.op_type == 'multiplication':
op_str = ''
for t in terms_as_strings

Code Snippets

"""
Used for the creation of "questions" for the user,
along with the expected answer (that is, the result of the operation)

Some examples of "questions" and their "answers":

"+2-4" ,        "-2"
"(-2)(+7)",     "-14"
"-1.60-2.04",   "-3.64"

The program should be aimed specifically at teaching
the very basics of addition or multiplication of integers or decimals,
in a scaling difficulty.

Exercises that deviate from those specific concepts should be avoided.
Examples of what should NOT be implemented:

"2-4",          # Deviates (slightly) since it also teaches that 2 == +2
"-2(+7)",       # Deviates (same as example above)
"+2-4(-5)",     # Deviates since it combines addition and multiplication



"""


import decimal
from random import randint, choice, random, seed

MAX_ABS_VALUE = 10

def rand_int_term(max_val=MAX_ABS_VALUE):
    return randint(0, max_val)

def rand_float_term(max_val=MAX_ABS_VALUE):
    return random() * max_val

def create_term(term_without_sign):
    """
    Creates a single term.

    :param term_without_sign:
    :return: (num)
    """
    sign = choice(['-', '+'])

    if sign == '-':
        term = -1 * term_without_sign
    else:
        term = term_without_sign

    return term

DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP = {
    1: dict(
        terms_count=2,
        terms_type='int'
    ),
    2: dict(
        terms_count=3,
        terms_type='int'
    ),
    3: dict(
        terms_count=2,
        terms_type='float'
    )
}

TOTAL_DIFFICULTY_LVLS = len(DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP)


class Terms(object):

    def __init__(self, difficulty_lvl):
        terms_data = DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP[difficulty_lvl]
        self.terms_count = terms_data['terms_count']
        self.terms_type = terms_data['terms_type']

    def all_terms(self):
        lst = []

        if self.terms_type == 'int':
            func = rand_int_term
        elif self.terms_type == 'float':
            func = rand_float_term
        else:
            raise NotImplemented('{}'.format(self.terms_type))

        for _ in range(self.terms_count):
            term_without_sign = func()
            term = create_term(term_without_sign=term_without_sign)
            lst.append(term)

        return lst


class QuestionAndAnswer(object):
    """
    Based on difficulty and operation type,
    creates terms (either float or ints) which are then rounded to allow
    specific number of decimals.
    """

    OPERATIONS_TYPES = {'addition', 'multiplication'}

    def __init__(self, difficulty_lvl, op_type):
        self.terms_as_numbers = Terms(difficulty_lvl=difficulty_lvl).all_terms()
        self.op_type = op_type

    @staticmethod
    def round_single_term_2_decimals(given_float):

        num = decimal.Decimal(str(given_float))
        return num.quantize(decimal.Decimal('.01'), decimal.ROUND_HALF_UP)

    def terms_to_rounded_numbers(self):
        """
        Ensures all terms have an appropriate number of decimals.

DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP = {
    1: dict(
        terms_count=2,
        terms_type=rand_int_term
    ),
    2: dict(
        terms_count=3,
        terms_type=rand_int_term
    ),
    3: dict(
        terms_count=2,
        terms_type=rand_float_term
    )
}

...

def all_terms(self):
    lst = []
    for _ in range(self.terms_count):
        term_without_sign = self.terms_type()
        term = create_term(term_without_sign=term_without_sign)
        lst.append(term)
def all_terms(self):
    return [ create_term(term_without_sign=self.terms_type())
             for _ in range(self.terms_count)]
if isinstance(t, int):
            lst.append(t)
        else:
            t = round_single_term_2_decimals(given_float=t)
            lst.append(t)
if not isinstance(t, int):
            t = round_single_term_2_decimals(given_float=t)
        lst.append(t)

Context

StackExchange Code Review Q#135167, answer score: 5

Revisions (0)

No revisions yet.