8
\$\begingroup\$

The Sequences & Series involved are Arithmetic and Geometric. stores and allows you to calculate using these given formulas and import fractions into micropython

Support for micropython! Micropython, however, only has a subset of functions of pythons, and it's standard library is very limited, so i had to reinvent the wheel on some things.

I'd like advice on efficiency, math, optimization, compactness, design, and general information about improving my program (it was designed for micropython).

Arithmetic:

  1. An = a1 + (n-1)d

  2. Sn = n/2 x (2a1 + (n-1)d) OR Sn = n/2(a + L)

Geometric:

  1. An = a1(r)^(n-1)

  2. Sn = (a(1-(r)^n)/(1-r)

  3. S_infinity = a/(1-r)

Both:

  1. ability to find the first nth to exceed a value
from math import sqrt, log
def take_inputs(*args):
 """
 takes a list of variable names, and sets it up so that that input is carried out, and
 a list is returned with the numerical values, to be used for unpacking into their variables
 Ex:
 take_inputs('a', 'b', 'c')
 a = 4
 b = 2
 c = 3
 - [4, 2, 3] -
 """
 values = []
 for prompt in args:
 values.append(eval(str(input(prompt + " = "))))
 return values
def check_for_L():
 """
 checks if an 'x' was inputted at L.
 if 'x', then return None (L wasn't inputted)
 if any other value, then return the float form of that value
 Ex:
 check_for_L()
 Enter x if L not present!
 Enter L: 5
 - 5.0 -
 """
 L = input("Enter x if L not present!\nEnter L: ")
 if L == 'x':
 return None
 return float(L)
class Arithmetic:
 def a_nth(self):
 print("An = a1 + (n-1)d")
 a1, n, d = take_inputs('a1', 'n', 'd')
 print(round(a1 + (n - 1) * d, 5))
 def sum(self):
 print("Sn = n/2(2a1+(n-1)d)", "Sn = n/2(a1+L)", sep="\n")
 L = check_for_L()
 if not L:
 a1, n, d = take_inputs('a1', 'n', 'd')
 print(round((n/2) * (2 * a1 + (n - 1) * d), 5))
 else:
 a1, n = take_inputs('a1', 'n')
 print(round((n/2) * (a1 + L), 5))
 def exceed_nth(self):
 print("a+(n-1)d > Value")
 a1, d, value = take_inputs('a1', 'd', 'value')
 n = round(( (value - a1) / d ) + 1, 5)
 print("n to exceed = " + str(n))
 def exceed_sum_to_nth(self):
 print("n/2(2a1+(n-1)d)>Value", "n/2(a1+L)>Value", sep="\n")
 L = check_for_L()
 if L:
 a1, value = take_inputs('a1', 'value')
 n = round((2 * value) / (a1 + L), 5)
 print("n to exceed = " + str(n))
 else:
 a1, d, value = take_inputs('a1', 'd', 'value')
 n = round((-2*a1 + d + sqrt(4*(a1**2) - 4*a1*d + d**2 + 8*d*value))/(2*d), 5)
 print("n to exceed = " + str(n))
class Geometric:
 def a_nth(self):
 print("An = ar^(n-1)")
 a1, r, n = take_inputs('a1', 'r', 'n')
 print(round(a1 * r ** (n - 1), 5))
 def sum(self):
 print("Sn = a(1-r^n)/(1-r)")
 a1, r, n = take_inputs('a1', 'r', 'n')
 print( round((a1 * (1 - r**n) )/(1 - r), 5))
 def sum_to_infinity(self):
 print("S(inf) = a/(1-r)")
 a1, r = take_inputs('a1', 'r')
 print(round(a1/(1-r), 5))
 def exceed_nth(self):
 print("ar^(n-1) > Value")
 a1, r, value = take_inputs('a1', 'r', 'value')
 n = round(log( r*(value/a1) ) / log(r), 5)
 print("n to exceed = " + str(n))
 def exceed_sum_to_nth(self):
 print("a(1-r^n)/(1-r)>Value")
 a1, r, value = take_inputs('a1', 'r', 'value')
 n = round(log( (a1 + r*value - value) / a1) / log(r), 5)
 print("n to exceed = " + str(n))
class BIOS:
 choices_prompt = "a for arithmetic\nb for geometric\n>> "
 def __init__(self):
 """
 responsible for handling basic input and output, for the terminal, and options/choices.
 """
 self.running = True
 self.arithmetic = Arithmetic()
 self.geometric = Geometric()
 self.choices = {'a': self.arithmetic_sequences, 'b': self.geometric_sequences}
 self.arithmetic_choices = {'a': self.arithmetic.a_nth, 'b': self.arithmetic.sum, 'c': self.arithmetic_exceed}
 self.geometric_choices = {'a': self.geometric.a_nth, 'b': self.geometric.sum, 'c': self.geometric.sum_to_infinity, 'd': self.geometric_exceed}
 self.arithmetic_exceed_choices = {'a': self.arithmetic.exceed_nth, 'b': self.arithmetic.exceed_sum_to_nth}
 self.geometric_exceed_choices = {'a': self.geometric.exceed_nth, 'b': self.geometric.exceed_sum_to_nth}
 @staticmethod
 def menu(*args):
 """
 a list of prompts is given as arguments,
 and a menu like input system is made.
 Ex:
 menu('a for apple', 'b for bye', 'c for cat')
 a for apple
 b for bye
 c for cat
 >> a
 - a -
 """
 return input("\n".join(list(args) + [">> "]))
 def stop_decorator(func):
 """
 Decorator for stopping certain functions, after they're done by asking with a prompt
 """
 def wrapper(self):
 func(self)
 command = input("Enter nothing to stop: ")
 if command == '':
 self.running = False
 return wrapper
 @stop_decorator
 def arithmetic_sequences(self):
 sub_choice = self.menu("Arithmetic:", "a for a_nth term", "b for sum", "c for min_term_exceed")
 self.arithmetic_choices.get(sub_choice, lambda: None)()
 @stop_decorator
 def geometric_sequences(self):
 sub_choice = self.menu("Geometric:", "a for a_nth term", "b for sum", "c for sum to infinity", "d for min terms exceed")
 self.geometric_choices.get(sub_choice, lambda: None)()
 def arithmetic_exceed(self):
 sub_choice = self.menu("Exceed Arithmetic:", "a for nth", "b for sum_to_nth")
 self.arithmetic_exceed_choices.get(sub_choice, lambda: None)()
 def geometric_exceed(self):
 sub_choice = self.menu("Exceed Geometric:", "a for nth", "b for sum_to_nth")
 self.geometric_exceed_choices.get(sub_choice, lambda: None)()
 def main(self):
 """
 runs the program indefinitely as long as the user wants.
 """
 while self.running:
 self.choices.get(input(self.choices_prompt), lambda: None)()
 print()
program = BIOS()
program.main()
```
Peilonrayz
44.4k7 gold badges80 silver badges157 bronze badges
asked Sep 13, 2020 at 9:19
\$\endgroup\$
1
  • \$\begingroup\$ yes, so that i can input values like 3/2, which is a fraction, and possibly pi, in the near future. I am aware this is a vulnerability/security issue, but this is just a math program lol for a calculator nonetheless. I'm pretty much gonna be the only one using it. \$\endgroup\$ Commented Sep 13, 2020 at 22:41

1 Answer 1

2
\$\begingroup\$

As a deleted comment probably said, eval has risk. That risk can be mitigated, depending on how much effort you want to put in and how security-sensitive the application is. I demonstrate with a constrained compile/eval; a more paranoid approach would validate an AST.

so that i can input values like 3/2, which is a fraction, and possibly pi

Keep in mind that entering a fraction-like string of 3/2 and evaluating it to a float is not the same as a true fraction. I will assume the former and not the latter.

Support for micropython!

Great, I guess; but I don't have a micropython environment so my demonstration makes simple use of modern CPython.

Consider replacing values.append with an iterator.

This is a strange choice:

"Enter x if L not present!\nEnter L: "

I would sooner just accept a blank string (from a return keystroke) to indicate "not present".

check_for_L returns a string coerced to a float; but why not leverage the same eval machinery?

Arithmetic and Geometric exhibit a classic antipattern of class-as-namespace. There are no members - convert these to modules if you want namespaces.

BIOS is a slightly obscure name, and again, there are no (justifiable) members; this can also be converted to a module. In terms of members,

  • running is a loop-flag anti-pattern that can just be replaced with a predicate on an input
  • arithmetic and geometric will no longer be objects
  • choices can be passed inline to a menu function. These are not so expensive as to warrant caching.

Instead of repeating calls to round all with the same precision, make a utility function. Note that round (a) is not needed for integers, (b) is not enough to remove the .0 default suffix for whole-number floats, and (c) will not function with complex numbers; so you will benefit from code that addresses all of these cases.

Some of your numeric expressions have redundant parens or can be simplified; especially,

log( (a1 + r*value - value) / a1) / log(r)

can be

math.log(1 + value*(r - 1)/a1, r)

menu can be rewritten so that it generates the a for, b for, etc. automatically.

Code like

input("\n".join(list(args) + [">> "]))

is a little convoluted; why not just have the input prompt be the >> alone, preceded by simple prints?

stop_decorator is way more complicated than it needs to be. This and self.running can go away and be replaced with a simple check in a loop.

get(sub_choice, lambda: None) is both more tricky than it should be and more quiet on failure than it should be. You can instead loop on a user's input until they enter a valid option.

Add a __main__ guard.

All together,

import builtins
import math
import numbers
import string
import typing
if typing.TYPE_CHECKING:
 # Don't use numbers.Number: it's missing most of its operand specifiers
 Number = numbers.Real | numbers.Complex
MATH_GLOBALS = {
 # Built-ins: only math-like functions
 '__builtins__': {
 k: getattr(builtins, k)
 for k in (
 'abs', 'complex', 'divmod', 'float', 'int', 'max', 'min', 'pow', 'round', 'sum',
 )
 }
} | {
 # Globals: only public symbols from the math module
 k: v
 for k, v in math.__dict__.items()
 if not k.startswith('_')
}
def eval_math(expr: str) -> 'Number':
 """
 eval() expr as a math-like expression, with the following safety restrictions:
 - 'eval' mode (no function definitions or control statements, etc.)
 - most built-ins (input, exec, etc.) removed, other than math-like built-ins such as `abs`
 - for globals, only the public symbols from the math module like `sin` and `pi`
 - no locals
 """
 code = compile(
 source=expr, filename='<expression>', mode='eval',
 flags=0, optimize=0, dont_inherit=True,
 )
 value = eval(code, MATH_GLOBALS, {}) # no locals
 if not isinstance(value, (numbers.Real, numbers.Complex)):
 raise TypeError(f'{value!r} is not a number')
 return value
def take_inputs(*args: str) -> 'typing.Iterator[Number]':
 """
 For each arg, loop until a valid input is captured, safe(ish) eval it, and yield its value
 """
 print('Enter variables (expressions supported):')
 for name in args:
 while True:
 expr = input(f'{name} = ')
 try:
 yield eval_math(expr)
 break
 except (ValueError, NameError, SyntaxError, TypeError) as e:
 print(str(e))
def check_for_L() -> 'Number | None':
 """
 Loop until user either enters an empty string (coerced to None), or a valid input:
 safe(ish) eval it and return its value
 """
 while True:
 expr = input('Enter L expression, or return for none: ')
 if expr == '':
 return None
 try:
 return eval_math(expr)
 except (ValueError, NameError, SyntaxError, TypeError) as e:
 print(str(e))
def trunc(x: 'Number', ndigits: int = 5) -> str:
 """
 Format x as a string with precision up to ndigits. This is different from f'{x:.5f}' where
 ndigits will always be shown regardless of whether they're zero.
 """
 match x:
 case int():
 return str(x)
 case float():
 x = round(x, ndigits=ndigits)
 if x.is_integer():
 return f'{x:.0f}'
 return str(x)
 case complex():
 isign = math.copysign(1, x.imag)
 sign_char = '+' if isign >= 0 else '-'
 return f'{trunc(x.real, ndigits)} {sign_char} {trunc(isign*x.imag, ndigits)}j'
 case _:
 raise TypeError(f'{x!r} is not a number')
def arithmetic_a_nth() -> None:
 print('An = a1 + (n-1)d')
 a1, n, d = take_inputs('a1', 'n', 'd')
 print(trunc(a1 + (n - 1)*d))
def arithmetic_sum() -> None:
 print('Sn = n/2(2a1+(n-1)d)')
 print('Sn = n/2(a1+L)')
 L = check_for_L()
 if L is None:
 a1, n, d = take_inputs('a1', 'n', 'd')
 print(trunc(n/2*(2*a1 + (n - 1)*d)))
 else:
 a1, n = take_inputs('a1', 'n')
 print(trunc(n/2*(a1 + L)))
def arithmetic_exceed_nth() -> None:
 print('a+(n-1)d > Value')
 a1, d, value = take_inputs('a1', 'd', 'value')
 print('n to exceed =', trunc((value - a1)/d + 1))
def arithmetic_exceed_sum_to_nth() -> None:
 print('n/2(2a1+(n-1)d)>Value')
 print('n/2(a1+L)>Value')
 L = check_for_L()
 if L is not None:
 a1, value = take_inputs('a1', 'value')
 n = 2*value/(a1 + L)
 else:
 a1, d, value = take_inputs('a1', 'd', 'value')
 n = (-2*a1 + d + math.sqrt(4*a1**2 - 4*a1*d + d**2 + 8*d*value))/(2*d)
 print('n to exceed =', trunc(n))
def geometric_a_nth() -> None:
 print('An = ar^(n-1)')
 a1, r, n = take_inputs('a1', 'r', 'n')
 print(trunc(a1 * r**(n - 1)))
def geometric_sum() -> None:
 print('Sn = a(1-r^n)/(1-r)')
 a1, r, n = take_inputs('a1', 'r', 'n')
 print(trunc(a1*(1 - r**n)/(1 - r)))
def geometric_sum_to_infinity() -> None:
 print('S(inf) = a/(1-r)')
 a1, r = take_inputs('a1', 'r')
 print(trunc(a1/(1 - r)))
def geometric_exceed_nth() -> None:
 print('ar^(n-1) > Value')
 a1, r, value = take_inputs('a1', 'r', 'value')
 n = math.log(r*value/a1, r)
 print('n to exceed =', trunc(n))
def geometric_exceed_sum_to_nth() -> None:
 print('a(1-r^n)/(1-r)>Value')
 a1, r, value = take_inputs('a1', 'r', 'value')
 n = math.log(1 + value*(r - 1)/a1, r)
 print('n to exceed =', trunc(n))
def menu(options: 'dict[str, typing.Callable[[], None]]') -> None:
 """
 Print a prompt like
 a for sum
 b for min_term_exceed
 >
 where the letters are enumerated automatically, the names are the keys of `options`, and
 the corresponding value from `options` is called once the user inputs a valid letter.
 """
 choices = dict(zip(string.ascii_lowercase, options.keys()))
 print('\n'.join(
 f'{letter} for {name}'
 for letter, name in choices.items()
 ))
 while True:
 selection = input('> ')
 if selection in choices:
 break
 options[choices[selection]]()
def arithmetic_sequences() -> None:
 print('Arithmetic:')
 menu({'a_nth': arithmetic_a_nth, 'sum': arithmetic_sum, 'min term exceed': arithmetic_exceed})
def geometric_sequences() -> None:
 print('Geometric:')
 menu({'a_nth': geometric_a_nth, 'sum': geometric_sum, 'sum to infinity': geometric_sum_to_infinity, 'min term exceed': geometric_exceed})
def arithmetic_exceed() -> None:
 print('Exceed Arithmetic:')
 menu({'nth': arithmetic_exceed_nth, 'sum to nth': arithmetic_exceed_sum_to_nth})
def geometric_exceed() -> None:
 print('Exceed Geometric:')
 menu({'nth': geometric_exceed_nth, 'sum to nth': geometric_exceed_sum_to_nth})
def main() -> None:
 while True:
 menu({'arithmetic': arithmetic_sequences, 'geometric': geometric_sequences})
 if input('Exit [y]/n? ') != 'n':
 break
 print()
if __name__ == '__main__':
 try:
 main()
 except KeyboardInterrupt:
 pass # silent ctrl+C break

Output demonstrating pi and fraction-like input:

a for arithmetic
b for geometric
> b
Geometric:
a for a_nth
b for sum
c for sum to infinity
d for min term exceed
> d
Exceed Geometric:
a for nth
b for sum to nth
> b
a(1-r^n)/(1-r)>Value
Enter variables (expressions supported):
a1 = pi
r = 5/3
value = exp(5)
n to exceed = 6.81458
Exit [y]/n? 

Output demonstrating complex numbers:

a for arithmetic
b for geometric
> a
Arithmetic:
a for a_nth
b for sum
c for min term exceed
> a
An = a1 + (n-1)d
Enter variables (expressions supported):
a1 = 3.2
n = 1/2
d = 10 + 12j
-1.8 - 6j
Exit [y]/n? 
answered Oct 24, 2024 at 15:32
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.