Skip to content

Instantly share code, notes, and snippets.

@thegamecracks
Created April 11, 2024 23:34
Show Gist options
  • Save thegamecracks/43e7489d5d760ef7ffea386a3062c60d to your computer and use it in GitHub Desktop.
Save thegamecracks/43e7489d5d760ef7ffea386a3062c60d to your computer and use it in GitHub Desktop.
A collection of some random scripts I wrote on Termux
#!/usr/bin/env python3
"""Converts resistor parameters into their corrresponding color bands."""
import argparse
from decimal import Decimal
# list of colors ordered by their corresponding digit (0-9)
DIGIT_BANDS = [
"black",
"brown",
"red",
"orange",
"yellow",
"green",
"blue",
"purple",
"grey",
"white",
]
# maps exponent to corresponding band
MULTIPLIER_BANDS = {-2: "silver", -1: "gold", **dict(enumerate(DIGIT_BANDS))}
# maps tolerance in hundredths of a percent into corresponding color
TOLERANCE_BANDS = {
100: "brown",
200: "red",
50: "green",
25: "blue",
10: "purple",
5: "grey",
500: "gold",
1000: "silver",
}
NO_BAND_TOLERANCE = 20.0
def _decimal_frexp10(n: Decimal) -> tuple[list[int], int]:
"""Splits a decimal into its fractional part
and exponent in base 10.
"""
# based on https://stackoverflow.com/a/26890567
t = n.normalize().as_tuple()
return t.digits, t.exponent
def resistor_bands(value: Decimal, n_bands: int, tolerance: float):
"""Returns a list of color bands representing the given resistance
value and tolerance.
Warning: this method does not support the real 6 band standard where
the last band represents the temperature coefficient.
Note: a tolerance of 0.2 (20%) does not occupy any band.
:param value:
The resistance to be represented.
:param n_bands:
The number of bands to use. At least one band has to represent the
most significant digit, one band for the multiplier, and one band
for the tolerance if it isn't the implied 20%.
The more bands given, the more digits that can be displayed.
:param tolerance:
The tolerance of the resistor as a decimal.
:returns:
A list of strings stating the color of each band.
:raises ValueError:
Either the resistance was too small/large to be represented,
or there was no corresponding color for the given tolerance.
"""
# only include tolerance band if it isn't implied 20%
i_tol = n_bands
if tolerance != NO_BAND_TOLERANCE:
i_tol -= 1
n_digits = i_tol - 1
if n_digits < 1:
raise ValueError(f"{-n_digits + 1} more bands are required")
# convert value into scientific notation
digits, exp = _decimal_frexp10(value)
excess = n_digits - len(digits)
if excess > 0:
# pad with leading/trailing zeros if there are an excess number of bands
# (trailing zeros are preferred to make exponent zero)
trailing = min(max(exp, 0), excess)
exp -= trailing
leading = excess - trailing
digits = (0,) * leading + digits + (0,) * trailing
elif excess < 0:
# truncate digits and adjust exponent accordingly
digits = digits[:n_digits]
exp += -excess
bands = []
for i in range(n_bands):
if i < n_digits:
# Value band
b = DIGIT_BANDS[digits[i]]
elif i == n_digits:
# Multiplier band
b = MULTIPLIER_BANDS.get(exp)
if b is None:
min_value = 10 ** min(MULTIPLIER_BANDS)
if value < min_value:
raise ValueError(
"resistances less than 0.01 ohms cannot be represented"
)
raise ValueError(
"resistances greater than 1 teraohms cannot be represented"
)
else:
# Tolerance band
b = TOLERANCE_BANDS.get(int(tolerance * 10000))
if b is None:
raise ValueError(f"no matching band for tolerance: {tolerance}")
bands.append(b)
return bands
def percent(s: str) -> float:
s = s.removesuffix("%")
return float(s) / 100
def print_bands(value, n_bands, tolerance):
try:
bands = resistor_bands(value, n_bands, tolerance)
except ValueError as e:
print(e)
else:
print("Resistor:")
print(", ".join(bands))
def main():
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-t",
"--tolerance",
type=percent,
default="5%",
help="the manufacturing tolerance as a percentage (%%)",
)
parser.add_argument(
"-b",
"--bands",
type=int,
default=4,
help="the number of color bands",
)
parser.add_argument(
"resistance",
type=Decimal,
help="the resistance value to convert (ohms)",
)
args = parser.parse_args()
print_bands(args.resistance, args.bands, args.tolerance)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Converts postfix strings into infix.
Currently this program only supports binary operators.
"""
import argparse
import textwrap
from typing import Any
from tree import format_tree
def plural(s: str, n: int, suffix: str = "s") -> str:
if n != 1:
return s + suffix
return s
def isoperator(token: str) -> bool:
"""Determines if a token is an operator."""
return token in "+-*/"
def popoperands(stack: list[Any], op: str) -> list[str]:
"""Pops the required operands from the stack."""
n = 2
if len(stack) < n:
missing = n - len(stack)
raise ValueError(
"missing {} {} for {!r} operator".format(
missing, plural("operand", missing), op
)
)
# NOTE: if implementing unary operators, must somehow implement associativity
operands = [stack.pop() for _ in range(n)]
operands.reverse()
return operands
def wrap(s: str) -> str:
"""Wraps a string in parentheses."""
return f"({s})"
def to_infix_tree(expr: str) -> list[Any]:
"""Converts a sequence of tokens in postfix format into a syntax tree."""
stack: list[Any] = []
for token in expr:
if isoperator(token):
operands = popoperands(stack, token)
node = [token] + operands
stack.append(node)
else:
stack.append([token])
if len(stack) > 1:
raise ValueError(
"missing operator for {!r} and {!r}".format(
render_line(stack[0]), render_line(stack[1])
)
)
elif not stack:
return stack
return stack[0]
def render_tree(infix_tree: list[Any]) -> str:
return "\n".join(format_tree(infix_tree))
def render_line(infix_tree: list[Any]) -> str:
operator, *operands = infix_tree
for i, e in enumerate(operands):
if len(e) > 1:
operands[i] = render_line(e)
else:
operands[i] = e[0]
return wrap(operands[0] + operator + operands[1])
def main():
# reading from command line
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"-t",
"--tree",
action="store_const",
dest="render",
const=render_tree,
default=render_line,
help="renders each expression as a syntax tree",
)
parser.add_argument(
"expressions",
nargs="*",
metavar="expr",
help="the postfix expression(s) to evaluate (example: abc-+d/)",
)
args = parser.parse_args()
expressions: list[str] = args.expressions
if not expressions:
return parser.print_help()
# convert each argument
for expr in expressions:
print(expr)
try:
infix = to_infix_tree(expr)
text = args.render(infix)
print(textwrap.indent(text, " "))
except ValueError as e:
print(" ", "failed:", e)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Provides tools for formatting tree structures.
Trees are represented as nested lists, where the first element is the
node's value and each sublist after it is a child node.
Example tree:
[1, [2, [3], [4]], [5]]
"""
from typing import Any, Sequence
RD = "├── "
DO = "│ "
RO = "└── "
EM = " "
def heap_tree(h: Sequence[object]) -> Sequence[Any]:
"""Recursively converts a heap list
into a tree structure.
"""
def nodeify(h, i=0):
node = []
if i >= length:
return node
node.append(h[i])
if left := nodeify(h, 2 * i + 1):
node.append(left)
# right node can only exist when left node exists as well
if right := nodeify(h, 2 * i + 2):
node.append(right)
return node
length = len(h)
return nodeify(h)
def format_tree(t: Sequence[Any]) -> Sequence[str]:
"""Returns a list of lines visually
representing a tree structure.
"""
def render(node: Sequence[Any]) -> Sequence[list[str]]:
# Rendering the tree can be thought of as creating a table of prefixes,
# indentation, and one item per line. Child nodes can be rendered as
# subtables and then prefixed with the parent node.
table: list[list[str]] = []
for i, child in enumerate(node):
if i == 0:
# First element is the node's value
table.append([str(child)])
continue
elif not isinstance(child, list):
raise TypeError(f"expected list as child, not {child!r}")
# Select appropriate indent sequences
if i + 1 == len(node):
predent = RO
indent = EM
else:
predent = RD
indent = DO
# Render child node
for ic, line in enumerate(render(child)):
line.insert(0, indent if ic else predent)
table.append(line)
return table
final_table = render(t)
return ["".join(line) for line in final_table]
def print_tree(t):
print("\n".join(format_tree(t)))
def main():
import argparse, heapq
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title="sub-commands")
parser.set_defaults(func=None)
parser_heap = subparsers.add_parser(
"heap",
help="displays a heap data structure as a tree",
)
heap_group = parser_heap.add_mutually_exclusive_group()
heap_group.add_argument(
"-l",
"--levels",
type=int,
default=4,
metavar="n",
help="the depth of the heap to generate",
)
heap_group.add_argument(
"-n",
"--size",
type=int,
default=None,
metavar="n",
help="the number of elements to generate",
)
heap_group.add_argument(
"elements",
action="extend",
nargs="*",
default=[], # NOTE: this default is forced
help="optional elements to use instead "
"of randomized ones; can be integers "
"or strings, but not both",
)
parser_heap.add_argument(
"-x",
"--max",
action="store_const",
dest="heapify",
const=heapq._heapify_max,
default=heapq.heapify,
help="creates max-heap instead of a min-heap",
)
parser_heap.set_defaults(func=main_heap)
args = parser.parse_args()
if args.func is None:
parser.print_help()
else:
args.func(args)
def main_heap(args):
import random
no_negatives = "{} cannot be negative"
# Interpreting arguments
if args.elements:
h: Sequence[str | int] = args.elements
# Convert to integers if possible
nums: list[int] = []
err_mixed = ValueError("elements cannot have strings and integers mixed")
for i, e in enumerate(h):
try:
n = int(e)
except ValueError:
if nums:
raise err_mixed from None
else:
if i > 0 and not nums:
raise err_mixed
nums.append(n)
if nums:
h = nums
elif args.size is not None:
size = args.size
if size < 0:
raise ValueError(no_negatives.format("size"))
h = list(range(size))
random.shuffle(h)
else:
levels = args.levels
if levels < 0:
raise ValueError(no_negatives.format("levels"))
size = sum(2**n for n in range(levels))
h = list(range(size))
random.shuffle(h)
args.heapify(h)
tree = heap_tree(h)
print("Heap:")
print(h)
print("Tree:")
print_tree(tree)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import functools
import inspect
import re
DUNDER_PATTERN = re.compile(r"__.+__")
def typecheck(use_default_type=True):
"""Returns a decorator that applies type checking
to given arguments based on the function's
annotations.
:param use_default_type:
If there are parameters with only a default
value, the type of that value will be used
in-place of missing annotations.
"""
def _check_type(param, value):
if param.annotation is not param.empty:
expected = param.annotation
elif use_default_type and param.default is not param.empty:
expected = type(param.default)
else:
return
# PEP 563 support (may be a string literal)
if isinstance(expected, str):
expected = eval(expected, func.__globals__)
# NOTE: this type checking does not handle
# many cases like generics
if not isinstance(value, expected):
raise TypeError(
'expected type {} for parameter "{}", got '
"type {} instead".format(
expected.__name__, param.name, type(value).__name__
)
)
def decorator(func):
# Unwrap any other decorators
while wrapped := getattr(func, "__wrapped__", None):
func = wrapped
signature = inspect.signature(func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
bound = signature.bind(*args, **kwargs)
# Typecheck each argument
for name, value in bound.arguments.items():
param = signature.parameters[name]
_check_type(param, value)
return func(*args, **kwargs)
return wrapper
return decorator
class MethodTypeCheckerMeta(type):
"""A metaclass that adds runtime type checking
to its methods.
Extra keyword arguments passed to the subclass
are routed to the typecheck() decorator.
:param check_dunder:
If True, dunder methods will also be
type checked.
"""
def __new__(cls, name, bases, namespace, check_dunder=False, **kwargs):
# look for methods and apply typechecking
for k, v in namespace.items():
if not inspect.isroutine(v):
continue
elif not check_dunder and DUNDER_PATTERN.fullmatch(k):
continue
namespace[k] = typecheck(**kwargs)(v)
return super().__new__(cls, name, bases, namespace)
class MethodTypeChecker(metaclass=MethodTypeCheckerMeta):
pass
class Foo(MethodTypeChecker, check_dunder=True):
def __init__(self, n=0):
self.n = n
def __call__(self, m: int):
return self.n + m
def mask(self, m: int):
return self.n & m
@classmethod
def from_string(cls, s: str):
return cls(int(s))
x = Foo(45)
print(x(4))
print(x.mask(0b00001111))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment