diff --git a/.gitignore b/.gitignore index 25ff4b0..ee7ee2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,129 +1,139 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ diff --git a/readme.rst b/readme.rst new file mode 100644 index 0000000..5ec2989 --- /dev/null +++ b/readme.rst @@ -0,0 +1,30 @@ +sl4ng +===== + +This package serves as a workflow booster for emailing, iterating, and a +veritable smorgasboard of cases scattered in between. + +You want persistant data but Pickle and Dill ask one too many damned +questions? sl4ng.persistance.save sl4ng.persistance.load + +You be writing generators so nice that you want to use them twice, +thrice, or indefinitely? We gotchu! sl4ng.types.regenerator + +YouΓÇÖre on the brink of the creative breakthrough of the ages but youΓÇÖll +end it all in a fit of rage if you accidentally overwite your projects +ever again? We gotchu! sl4ng.files.paths.namespacer + +You want to look at an objectΓÇÖs source code but inspect.getsource makes +a mess of rendering things in your REPL, or perhaps you want to jump +straight into its package-folder or source-file? sl4ng.debug.getsource +sl4ng.debug.pop + +You want to want to see if your iterable is the codomain of a constant +function? We gotchu sl4ng.functional.eq + +You really like your dictionary, but itΓÇÖs got some duplicate values? +We.. Got.. Chu! sl4ng.iteration.deduplicate + +YouΓÇÖve read this far and think ΓÇ£Damn, son, this package looks diggity +fresh, but some of those functions are deeply nestedΓÇ¥? We gotchu +Everything is imported to init diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f9741b4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +psutil +pyperclip \ No newline at end of file diff --git a/setup.py b/setup.py index 3bd069b..3879741 100644 --- a/setup.py +++ b/setup.py @@ -3,13 +3,15 @@ from setuptools import setup, find_packages with open('README.md', 'r') as fob: long_description = fob.read() +with open('requirements.txt', 'r') as fob: + requirements = fob.readlines() + setup( name='sl4ng', - version='0.0.1', - author='Kenneth Elisandro', - author_email='eli2and40@tilde.club', - url='https://tildegit.org/eli2and40/sl4ng', - # packages=find_packages(), + version='0.0.2', + author='Kenneth Sabalo', + author_email='kennethsantanasabalo@tilde.club', + url='https://github.com/kendfss/sl4ng', packages=['sl4ng'], classifiers=[ "Programming Language :: Python :: 3", @@ -20,15 +22,6 @@ setup( long_description_content_type='text/markdown', keywords='utilities productivity', license='MIT', - requires=[ - 'pyperclip', - 'dill', - 'psutil', - 'send2trash', - 'tqdm', - 'filetype', - ], - # py_modules=['sl4ng'], - python_requires='>=3.8', - + requires=requirements, + python_requires='>3.9', ) \ No newline at end of file diff --git a/sl4ng/__init__.py b/sl4ng/__init__.py index 99378f2..88638fd 100644 --- a/sl4ng/__init__.py +++ b/sl4ng/__init__.py @@ -2,21 +2,22 @@ from typing import Iterable, Any, Iterator, Sequence import os from .debug import * -from .files import * -from .functional import * +# from .files import * +# from .functional import * from .iteration import * from .maths import * -from .persistance import * +# from .persistence import * from .stats import * from .strings import * from .system import * -from .types import * +# from .types import * from .web import * +from .magnitudes import * -HERE,THIS = os.path.split(__file__) +HERE, THIS = os.path.split(__file__) diff --git a/sl4ng/debug.py b/sl4ng/debug.py index 07840d7..ad4aa41 100644 --- a/sl4ng/debug.py +++ b/sl4ng/debug.py @@ -1,30 +1,36 @@ -from typing import Any, Iterable +from typing import Any, Iterable, Generator, Callable import sys, winsound, os, time, inspect import pyperclip -from .types import SineWave, generator, regurge, function - - separator = " ".join(70*'-') spaceprint = lambda x: print(x, "\n{}".format(separator)) -main = '__name__ == "__main__"' - +mainame = main = '__name__ == "__main__"' -def tipo(inpt:Any=type(lambda:0), keep_module:bool=False) -> str: +def nome(inpt): + """ + Towards a tool that can get the name of a variable + """ + for k, v in locals().items(): + if v == inpt: + return k + +def tipo(inpt, keep_module:bool=True) -> str: """ Return the name of an object's type - Dependencies: None - In: object - Out: str + params: + keep_module + If set to False, the name of the class will be returned without any explicit reference to package nesting """ + if isinstance(inpt, type): + inpt = str(inpt) if keep_module: return str(type(inpt)).split("'")[1] return str(type(inpt)).split("'")[1].split('.')[-1] -def beeper(inpt:int) -> SineWave: +def beeper(inpt:int): """Produces a series of beeps of increasing frequency Dependencies: Beep(a function from the winsound module) In: (number of beeps desired) @@ -90,6 +96,7 @@ def modir(module:type(os), copy:bool=True) -> str: def printer(iterable:Iterable, indent:int=0) -> None: """ Prints each element of an iterable on a new line, features an optional indent argument + ultimately replaced by "show" """ assert isinstance(indent, int), "Indentation level must be a positive integer" for i in iterable: @@ -137,33 +144,26 @@ def aspectRatio(x:int, y:int, w:int=None, h:int=None) -> float: return w/h==x/y if w and h else (w, (w*(y/x))) if w else ((h*(x/y)), h) if h else x/y -def show(iterable:Iterable[Any], indentation:int=0, enum:bool=False, first:int=1, indentor:str='\t', tail=True, head=False, file=sys.stdout, sep:str='') -> None: +def show(iterable:Iterable[Any], indentation:int=0, enum:bool=False, first:int=1, indentor:str='\t', tail=True, head=True, file=sys.stdout, sep:str='') -> Generator: """ - Print each element of an array. + Print each element of an iterable. >>> show(zip('abc','123')) ('a', '1') ('b', '2') ('c', '3') - - - >>> """ - # consumable = regurge(iterable) - consumable = iter(regurge(iterable)) if (wasstr:=isinstance(file, str)): file = open(file) print('\n', file=file) if head else None - for i, j in enumerate(consumable, first): + for i, j in enumerate(iterable, first): print( ( - f"{indentation*indentor}{j}", - f"{indentation*indentor}{i}\t{j}" + f"{indentation * indentor}{j}", + f"{indentation * indentor}{i}\t{j}" )[enum], sep=sep, file=file ) - # if sep: - # print(sep) print('\n', file=file) if tail else None if wasstr: file.close() @@ -221,8 +221,9 @@ def getsource(obj:Any, *args, copy:bool=False, **kwargs) -> str: """ text = inspect.getsource(obj).splitlines() if copy: - pyperclip.copy('\n'.join(text)) - return + text = '\n'.join(text) + pyperclip.copy(text) + return text show(text, *args, **kwargs) diff --git a/sl4ng/files/__init__.py b/sl4ng/files/__init__.py deleted file mode 100644 index 692bdbd..0000000 --- a/sl4ng/files/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from .paths import * -from .operations import * -from .tasks import * - - - - - - - - -if __name__ == "__main__": - pass \ No newline at end of file diff --git a/sl4ng/files/__pycache__/__init__.cpython-38.pyc b/sl4ng/files/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 0d1f2c8..0000000 Binary files a/sl4ng/files/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/sl4ng/files/__pycache__/operations.cpython-38.pyc b/sl4ng/files/__pycache__/operations.cpython-38.pyc deleted file mode 100644 index 371254f..0000000 Binary files a/sl4ng/files/__pycache__/operations.cpython-38.pyc and /dev/null differ diff --git a/sl4ng/files/__pycache__/paths.cpython-38.pyc b/sl4ng/files/__pycache__/paths.cpython-38.pyc deleted file mode 100644 index fe4a311..0000000 Binary files a/sl4ng/files/__pycache__/paths.cpython-38.pyc and /dev/null differ diff --git a/sl4ng/files/__pycache__/tasks.cpython-38.pyc b/sl4ng/files/__pycache__/tasks.cpython-38.pyc deleted file mode 100644 index bf0d06c..0000000 Binary files a/sl4ng/files/__pycache__/tasks.cpython-38.pyc and /dev/null differ diff --git a/sl4ng/files/operations.py b/sl4ng/files/operations.py deleted file mode 100644 index 9b8980f..0000000 --- a/sl4ng/files/operations.py +++ /dev/null @@ -1,57 +0,0 @@ -import os, subprocess - -from send2trash import send2trash -from tqdm import tqdm - -from .paths import nameUpdater - - -def convert(file:str, format:str='.wav', bitRate:int=450, delete:bool=False, options:str='') -> str: - """ - Convert an audio file - """ - trash = discard - os.chdir(os.path.split(file)[0]) - - _title = lambda file: file.split(os.sep)[-1].split('.')[0] - _new = lambda file, format: nameUpdater(_title(file)+format) - _name = lambda file: file.split(os.sep)[-1] - format = '.' + format if '.' != format[0] else format - - name = _title(file) - new = _new(file, format) - cmd = f'ffmpeg -y -i "{file}" -ab {bitRate*1000} "{new}"' if bitRate != 0 else f'ffmpeg {options} -y -i "{file}" "{new}"' - announcement = f"Converting:\n\t{file} --> {new}\n\t{cmd=}" - print(announcement) - subprocess.run(cmd) - print('Conversion is complete') - if delete: - trash(file) - print(f'Deletion is complete\n\t{new}\n\n\n') - return new - - -def move(file:str, destination:str) -> None: - """ - Move a file to a given directory - """ - nmxt = os.path.split(file)[-1] - ext = os.path.splitext(nmxt)[-1].replace('.', '') - with open(file, 'rb') as src: - src = tuple(i for i in src) - with open(destination, 'wb') as dst: - print(f'moving {nmxt.split(".")[0]} to the "{ext}" directory') - for datum in tqdm(src): - dst.write(datum) - - -def discard(path:str, recycle:bool=True) -> None: - """ - Remove an address from the file-system - """ - fb = (os.remove, send2trash) - first, backup = fb if not recycle else fb[::-1] - try: - first(path) - except PermissionError: - backup(path) \ No newline at end of file diff --git a/sl4ng/files/paths.py b/sl4ng/files/paths.py deleted file mode 100644 index 4f42e47..0000000 --- a/sl4ng/files/paths.py +++ /dev/null @@ -1,151 +0,0 @@ -from typing import Iterable - -import os, sys, time - - -defaultScriptDirectory = r'e:\projects\monties' -commons = { - 'music': os.path.join(os.path.expanduser('~'), 'music', 'collection'), - 'images': os.path.join(os.path.expanduser('~'), 'pictures'), - 'pictures': os.path.join(os.path.expanduser('~'), 'pictures'), - 'pics': os.path.join(os.path.expanduser('~'), 'pictures'), - 'videos': os.path.join(os.path.expanduser('~'), 'videos'), - 'ytdls': { - 'music': { - 'singles': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'music', 'singles'), - 'mixes': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'music', 'mixes'), - 'albums': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'music', 'album'), - }, - 'graff': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'graff'), - 'bullshit': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'bullshitters'), - 'bull': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'bullshitters'), - 'code': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'cscode'), - 'cs': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'cscode'), - 'maths': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'maths'), - 'math': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'maths'), - 'movies': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'movies'), - 'other': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'other'), - 'physics': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'physics'), - 'phys': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'physics'), - 'politics': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'politics'), - 'pol': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'politics'), - }, - 'documents': os.path.join(os.path.expanduser('~'), 'documents'), - 'docs': os.path.join(os.path.expanduser('~'), 'documents'), - 'downloads': os.path.join(os.path.expanduser('~'), 'downloads'), - 'desktop': os.path.join(os.path.expanduser('~'), 'desktop'), - 'books': os.path.join(os.path.expanduser('~'), 'documents', 'bookes'), - 'monties': os.path.join(defaultScriptDirectory, str(time.localtime()[0])), - 'scripts': defaultScriptDirectory, - 'demos': os.path.join(defaultScriptDirectory, 'demos'), - 'site': os.path.join(sys.exec_prefix, 'lib', 'site-packages'), - 'home': os.path.expanduser('~'), - 'user': os.path.expanduser('~'), - 'root': os.path.expanduser('~'), - '~': os.path.expanduser('~'), -} -os.makedirs(commons['monties'], exist_ok=True) - - -def delevel(path:str, steps:int=1) -> str: - """ - This will climb the given path tree by the given number of steps. - No matter how large the number of steps, it will stop as soon as it reaches the root. - Probably needs revision for paths on systems which hide the root drive. - example - >>> for i in range(4):print(delevel(r'c:/users/admin',i)) - c:/users/admin - c:/users - c:/ - c:/ - dependencies: os.sep - """ - while steps and (len(path.split(os.sep))-1): - path = os.sep.join((path.split(os.sep)[:-1])) - steps -= 1 - return path if not path.endswith(':') else path+os.sep - - -def namespacer(path:str, sep:str='_', start:int=2) -> str: - """ - Returns a unique version of a given string by appending an integer - - example: - tree: - /folder - /file.ext - /file_2.ext - - >>> nameSpacer('file', sep='-', start=2) - file-2.ext - >>> nameSpacer('file', sep='_', start=2) - file_3.ext - >>> nameSpacer('file', sep='_', start=0) - file_0.ext - """ - id = start - oldPath = path[:] - while os.path.exists(path): ##for general use - newPath = list(os.path.splitext(path)) - if sep in newPath[0]: - if newPath[0].split(sep)[-1].isnumeric(): - # print('case1a') - id = newPath[0].split(sep)[-1] - newPath[0] = newPath[0].replace(f'{sep}{id}', f'{sep}{str(int(id)+1)}') - path = ''.join(newPath) - else: - # print('case1b') - newPath[0] += f'{sep}{id}' - path = ''.join(newPath) - id += 1 - else: - # print('case2') - newPath[0] += f'{sep}{id}' - path = ''.join(newPath) - id += 1 - return path -nameUpdater = nameSpacer = name_spacer = namespacer - - -def mcd(args:Iterable[str], go_back:bool=False, recursive:bool=True, overwrite:bool=False) -> str: - """ - recursively create and enter directories. - - go_back: - if set to True, the process will return to the starting directory - recursive: - if set to false, all directories will be created in the starting directory - - eg - each of the following calls create the following tree: - dir-1 - dir0: starting directory - dir1 - dir2 - dir3 - dir4 - dir5 - - >>> mcd('dir1 dir2 .. dir3 .. .. dir4 .. .. dir5'.split()) - >>> mcd('dir1/dir2 ../dir3 ../../dir4 ../../dir5'.split()) - """ - home = os.getcwd() - for arg in args: - arg = nameSpacer(arg) if arg!='..' and not overwrite else arg - os.makedirs(arg, exist_ok=True) - os.chdir(arg if recursive else home) - last_stop = home if go_back else os.getcwd() - os.chdir(last_stop) - return last_stop - - -if __name__ == "__main__": - here, this = os.path.split(__file__) - this_2 = nameSpacer(this) - if not os.path.exists(this_2): - with open(this_2, 'x') as f: - print(nameUpdater(this, '-', 2)) - print(nameUpdater(this, '_', 2)) - print(nameUpdater(this, '_', 0)) - os.remove(this_2) - # [*map(os.remove, (this, this_2))] \ No newline at end of file diff --git a/sl4ng/files/tasks.py b/sl4ng/files/tasks.py deleted file mode 100644 index 69f3475..0000000 --- a/sl4ng/files/tasks.py +++ /dev/null @@ -1,185 +0,0 @@ -from typing import Sequence, Iterable -import re, os, subprocess - -import filetype as ft - -from ..types import generator -from ..iteration import shuffle - - -def search(keyword:str, directory:str, extension:str='', walk=True, **kwargs) -> generator: - """ - Find files matching the given keyword within a directory - """ - if re.search('\s', keyword): - keyword = map(lambda x: x.strip(), keyword.split()) - if not isinstance(keyword, str): - keyword = '|'.join(map(re.escape, keyword)) - else: - keyword = re.escape(keyword) - return filter( - lambda file:re.search(keyword, file.split(os.sep)[-1], re.I), - gather(directory, ext=extension, names=False, walk=walk, **kwargs) - ) - - -def sortbysize(files:Iterable[str]=None) -> list: - """ - Sort a collection of file paths by the size of the corresponding files (largest to smallest) - """ - files = [os.getcwd(), list(files)][bool(files)] - size = lambda file: os.stat(file).st_size - out = [] - while len(files)>0: - sizes = set(size(file) for file in files) - for file in files: - if size(file) == max(sizes): - out.append(file) - files.remove(file) - return out - - -def gather(folder:str=None, names:bool=False, walk:bool=True, ext:str=None) -> generator: - """ - Generate an iterable of the files rooted in a given folder - """ - folder = [os.getcwd(), folder][bool(folder)] - if walk: - if ext: - ext = ext.replace('.', '') - pred = [i for i in ',`* ' if i in ext] - pattern = '|'.join(f'\.{i}$' for i in ext.split(pred[0] if pred else None)) - pat = re.compile(pattern, re.I) - for root, folders, files in os.walk(folder): - for file in files: - if os.path.isfile(p:=os.path.join(root, file)) and pat.search(file) and file!='NTUSER.DAT': - yield file if names else p - else: - for root, folders, titles in os.walk(folder): - for t in titles: - if os.path.exists(p:=os.path.join(root, t)): - yield t if names else p - else: - if ext: - ext = ext.replace('.', '') - pred = [i for i in ',`* ' if i in ext] - pattern = '|'.join(f'\.{i}$' for i in ext.split(pred[0] if pred else None)) - pat = re.compile(pattern, re.I) - for file in os.listdir(folder): - if os.path.isfile(p:=os.path.join(folder, file)) and pat.search(file) and file!='NTUSER.DAT': - yield file if names else p - else: - for file in os.listdir(folder): - if os.path.isfile(p:=os.path.join(folder, file)): - yield file if names else p - - -def straw(path:str, text:bool=True, lines:bool=False) -> [str, list]: - """ - Extract the text, or bytes if the keyword is set to false, from a file - ::text:: - text or bytes? - ::lines:: - split text by line or return raw? - """ - if os.path.isfile(path): - mode = "rb r".split()[text] - with open(path, mode) as f: - return f.readlines() if lines else f.read() - - -def unarchive(path:str, destination:str=None, app:str='rar') -> None: - """ - Extract an archive to a chosen destination, or one generated based on the name of the archive - App refers to the comandlet you wish to invoke via subprocess.run - """ - options = { - 'tar':'-x -f', - 'rar':'e -or -r', - 'winrar':'', - } - if destination != None: - os.makedirs(destination, exist_ok=True) - os.chdir(destination) - subprocess.run(f'{app} {options[app]} "{path}" ') - else: - destination = os.path.splitext(path)[0] - src = os.path.join('.', os.path.split(path)[1]) - print(destination, path, src) - os.makedirs(destination, exist_ok=True) - os.chdir(destination) - subprocess.run(f'{app} {options[app]} "{src}" ') - -extractRar = unarchive - - -def empty(path:str, make:bool=False) -> bool: - """ - Check if a given file or folder is empty or not with the option to create it if it doesn't exit - """ - if os.path.exists(path): - if os.path.isfile(path): - with open(path, 'rb') as f: - return not bool(len(tuple(i for i in f))) - elif os.path.isdir(path): - return not bool(len(os.listdir(file))) - elif make: - if os.path.splitext(path)[-1]: - x = open(path, 'x') - x.close() - else: - os.makedirs(path, exist_ok=True) - return True - - -def ffplay(files:Sequence[str], hide:bool=True, fullscreen:bool=True, loop:bool=True, quiet:bool=True, randomize:bool=True, silent:bool=False) -> None: - """ - Play a collection of files using ffmpeg's "ffplay" cli - - If entering files as a string, separate each path by an asterisk (*), othewise feel free to use any iterator - -loop {f"-loop {loop}" if loop else ""} - """ - - # fullscreen = False if hide else fullscreen - namext = lambda file: os.path.split(file)[1] - nome = lambda file: os.path.splitext(namext(file))[0] - ext = lambda file: os.path.splitext(file)[1] - isvid = lambda file: ft.match(file) in ft.video_matchers - vidtitle = lambda vid: '-'.join(i.strip() for i in vid.split('-')[:-1]) - albumtrack = lambda file: bool(re.search(f'\d+\s.+{ext(file)}', file, re.I)) - attitle = lambda file: ' '.join(i.strip() for i in nome(file).split(' ')[1:]) - aov = lambda file: albumtrack(file) or isvid(file) - title = lambda file: ''.join(i for i in os.path.splitext(namext(file)[1])[0] if i not in '0123456789').strip() - windowtitle = lambda file: [namext(file), [attitle(file), vidtitle(file)][isvid(file)]][aov(file)] - play = lambda file: subprocess.run(f'ffplay {("", "-nodisp")[hide]} -window_title "{windowtitle(file)}" -autoexit {"-fs" if fullscreen else ""} {"-v error" if quiet else ""} "{file}"') - files = files.split('*') if isinstance(files,str) else files - if loop: - while (1 if loop==True else loop+1): - files = shuffle(files) if randomize else files - for i,f in enumerate(files, 1): - if os.path.isdir(f): - fls = [os.path.join(f, i) for i in gather(f, names=False)] - for j,file in enumerate(fls, 1): - name = os.path.split(file)[1] - print(f'{j} of {len(fls)}:\t{name}') if not silent else None - ffplay(file, hide, fullscreen, False, quiet, randomize, True) - else: - folder,name = os.path.split(f) - print(f'{i} of {len(files)}:\t{name}') if not silent else None - play(f) - loop -= 1 - else: - files = shuffle(files) if randomize else files - for i, f in enumerate(files, 1): - if os.path.isdir(f): - fls = [os.path.join(f, i) for i in gather(f, names=False)] - for j, file in enumerate(fls, 1): - name = os.path.split(file)[1] - print(f'{j} of {len(fls)}:\t{name}') if not silent else None - ffplay(file, hide, fullscreen, False, quiet, randomize, True) - else: - print(f'{i} of {len(files)}:\t{title(f)}') if not silent else None - play(f) - - -size = lambda file: os.stat(file).st_size \ No newline at end of file diff --git a/sl4ng/iteration.py b/sl4ng/iteration.py index 371a87f..ca1b852 100644 --- a/sl4ng/iteration.py +++ b/sl4ng/iteration.py @@ -1,19 +1,211 @@ # Statisticals checked against: http://www.alcula.com/calculators/statistics/ -from itertools import tee, islice, chain, combinations -from typing import Iterable, Any, Sequence, List, Callable -from copy import deepcopy +from itertools import tee, _tee, islice, chain, combinations +from numbers import Number, Real +from typing import Iterable, Any, Sequence, List, Callable, Generator, Hashable from math import log -import random +import random, time -from .types import generator, regurge, regenerator -from .maths import binomial -from .functional import eq -def xrange(stop:complex, start:complex=0, step:complex=1, reverse:bool=False) -> generator: + +class __regen: + """ + A namespace for utilities used in creating regenerators + """ + @staticmethod + def choose(iterable:Iterable[Any], *indices:Sequence[int]) -> Generator: + """ + Yield specific elements from an iterable by index: + >>> [*choose(range(1, 10), 0, 3)] + [1, 4] + >>> [*choose(range(1, 10), (0, 3))] + [1, 4] + + """ + indices = [*flatten(indices)] + yielded = [] + for i, e in enumerate(iterable): + if i in indices: + yield e + yielded.append(e) + if len(yielded) == len(indices): + break + @staticmethod + def tipo(inpt:Any=type(lambda:0), keep_module:bool=False) -> str: + """ + Return the name of an object's type + Dependencies: None + In: object + Out: str + """ + if keep_module: + return str(type(inpt)).split("'")[1] + return str(type(inpt)).split("'")[1].split('.')[-1] + @staticmethod + def flatten(iterable:Iterable) -> Generator: + """ + Flatten a 2d iterable + Example: + >>> list(flatten([[1, 2], [3, 4]])) + [1, 2, 3, 4] + based on: + https://pythonprinciples.com/challenges/Flatten-a-list/ + """ + for i in iterable: + if hasattr(i, '__iter__') or hasattr(i, '__next__'): + yield from i + else: + yield i + +class regenerator: + """ + A self-replenishing (or non-consumable) iterator. All methods whose return value is iterable return regenerators + :args & kwargs: + Any arguments needed to initialize the Generator-type/function. Will not be used unless hasattr(iterable, '__call__') and iterable is not a regenerator. + eg: + >>> x = regurge(i for i in range(2)) + >>> [*x] + [0, 1] + >>> bytes(x) + b'\x00\x01' + >>> [*x] + [0, 1] + """ + def __init__(self, iterable, *args, **kwargs): + if hasattr(iterable, '__call__') and not isinstance(iterable, type(self)): + self.active, self._inert = tee(iterable(*args, **kwargs)) + else: + self.active, self._inert = tee(iterable) + def __next__(self): + return next(self.active) + def __iter__(self): + self.active, self._inert = tee(self._inert) + return self.active + def __getitem__(self, index:int): + if isinstance(index, int): + index = index if index >= 0 else len(self) + index + for i, e in enumerate(self): + if i == index: + return e + else: + raise + raise IndexError(f'{__regen.tipo(self)} contains fewer than {index} elements') + def __call__(self, *indices:Iterable[int]): + """ + Access particular indices of the underlying iterator + eg + >>> x = regen(range(3)) + >>> [*x(1)] + [1] + >>> [*x(1,2)] + [1, 2] + >>> [*x(1,2,3)] + [1, 2] + """ + return type(self)(__regen.choose(self.active, *__regen.flatten(indices))) + def __bool__(self): + """ + Returns True iff. the underlying iterator is non-empty. False otherwise. + """ + tmp, self._inert = tee(self._inert) + try: + next(tmp) + return True + except StopIteration: + return False + def __matmul__(self, other:Iterable): + if hasattr(other, '__iter__'): + return type(self)(product(self, other)) + raise TypeError(f'Matrix-multiplication is not defined between {__regen.tipo(self, True)} and "{__regen.tipo(other, True)}"-type. It must have an "__iter__" or "__index__" method.') + def __len__(self): + return sum(1 for i in self) + def __add__(self, other:Iterable): + """ + Create a new regenerator whose first elements come from self and remaining element(s) is/come-from other. + If other is not iterable it shall be added as the last element. + If you want to add an iterable as a single element, use self.append + eg + >>> x = regenerator(range(2)) + >>> y = x + 10 + >>> [*y] + [0, 1, 10] + >>> y += x + >>> [*y] + [0, 1, 10, 0, 1] + """ + other = other if hasattr(other, '__iter__') else [other] + return type(self)([*self, *other]) + def __radd__(self, other:Iterable): + """ + Swap the order of __add__ + """ + other = other if hasattr(other, '__iter__') else [other] + return type(self)(chain(other, self)) + def __mul__(self, value:int): + """ + Replicate the behaviour of multiplying lists by integers + """ + if hasattr(value, '__int__'): + return type(self)(chain.from_iterable(self for i in range(int(value)))) + raise TypeError(f'Multiplication is not defined for "{__regen.tipo(other, True)}". It must have an "__int__"') + def __rmul__(self, value:int): + """Commutative multiplication""" + return self.__mul__(value) + def __pow__(self, value:int): + """ + value-dimensional Cartesian product of self with itself + """ + if hasattr(value, '__int__'): + return type(self)(product(self, repeat=int(value))) + raise TypeError(f'Exponentiation is not defined for {type(other)}. It must have an "__int__" method.') + + def count(self, value:Any): + """ + how many copies of value? + """ + return sum(1 for i in self if i==value) + def scale(self, value:Any): + """ + Multiply every element of self by value. Done in place. + """ + self._inert = (i*value for i in self) + return self + def boost(self, value:Any): + """ + Add value to every element of self. Done in place. + """ + self._inert = (i+value for i in self) + return self + def indices(self, value:Any, shift:int=0): + """ + Similar to a list's index method, except that it returns every index whose element is a match + Works by calling enumerate and selecting at equivalent elements. + :shift: + kwarg for the "enumerate" call. + """ + return type(self)(i for i, e in enumerate(self, shift) if e == value) + def append(self, value:Any): + """ + add "value" as the last element of the array + """ + other = [value] + self._inert = chain(self._inert, other) + return self + def inject(self, value:Any): + """ + If "value" is an iterable: its elements will be added to the end of "self" + Otherwise: it is the same as append + """ + other = value if hasattr(value, '__iter__') else [value] + self._inert = chain(self._inert, *other) + return self +regen = regenerator + + +def xrange(stop:Number, start:Number=0, step:Number=1, reverse:bool=False) -> Generator: """ xrange(start, stop, step) - An implementation of the old xrange generator + An implementation of the old xrange Generator examples: >>> [*xrange(2)] [0, 1] @@ -39,21 +231,20 @@ def xrange(stop:complex, start:complex=0, step:complex=1, reverse:bool=False) -> start += step -def tight(iterable:Iterable[Any]) -> generator: +def tight(iterable:Iterable[Any], yielded:list=None) -> Generator: """ Produce a new iterator of unique elements from a given array - will consume a generator + will consume a Generator """ - consumable = list(regurge(iterable)) - # consumable = list(consumable) - yielded = [] + yielded = yielded if not isinstance(yielded, type(None)) else [] for i in iterable: if not i in yielded: yield i yielded.append(i) +unique = tight -def walks(iterable:Iterable[Any], length:int=2) -> generator: +def walks(iterable:Iterable[Any], length:int=2) -> Generator: """ Break an iterable into len(iterable)-length steps of the given length, with each step's starting point one after its predecessor example @@ -66,22 +257,21 @@ def walks(iterable:Iterable[Any], length:int=2) -> generator: Extended to generators with cues from more_itertools' "stagger" Extended to infinite generators by pedantry """ - consumable = regurge(iterable) + consumable = regenerator(iterable) t = tee(consumable, length) yield from zip(*(it if n==0 else islice(it, n, None) for n, it in enumerate(t))) -def flatten(iterable:Iterable) -> generator: +def flatten(iterable:Iterable) -> Generator: """ - Flatten a 2d iterable + Transform an N-dimensional array into a N-1-dimensional array. Example: >>> list(flatten([[1, 2], [3, 4]])) [1, 2, 3, 4] based on: https://pythonprinciples.com/challenges/Flatten-a-list/ """ - consumable = regurge(iterable) - for i in consumable: + for i in iterable: if hasattr(i, '__iter__') or hasattr(i, '__next__'): for j in i: yield j @@ -89,48 +279,76 @@ def flatten(iterable:Iterable) -> generator: yield i -def nopes(iterable:Iterable[Any], yeps:bool=False) -> generator: +def flat(iterable:Iterable, dict_keys:bool=False, strings:bool=False) -> Generator: + """ + Create a completely flat version of an iterable. + params: + strings + yield each character from each string if set to True + dict_keys + yield the keys instead of the values if set to True + """ + # if isinstance(iterable, fo;): + # yield iterable + if isinstance(iterable, str): + yield from iterable if strings else [iterable] + elif isinstance(iterable, dict): + itr = iterable.keys() if dict_keys else iterable.values() + for val in itr: + yield from flat(val, dict_keys, strings) + elif isinstance(iterable, (list, tuple, map, filter)): + for val in iterable: + if hasattr(val, '__iter__'): + yield from flat(val, dict_keys, strings) + else: + yield val + elif hasattr(iterable, '__iter__'): + yield from iterable + else: + yield iterable + + + +def nopes(iterable:Iterable[Any], yeps:bool=False) -> Generator: """ if yeps==False Return the indices of all false-like boolean values of an iterable Return indices of all true-like boolean values of an iterable example - t = (0,1,0,0,1) + >>> t = (0, 1, 0, 0, 1) >>> tuple(nopes(t)) - (0,2,3) - >>> tuple(nopes(t,True)) - (1,4) + (0, 2, 3) + >>> tuple(nopes(t, True)) + (1, 4) """ - consumable = regurge(iterable) - for i, j in enumerate(consumable): + for i, j in enumerate(iterable): if (not j, j)[yeps]: yield i -def pull(iterable:Iterable, stop:int, start:int=0, step:int=1) -> tuple: +def roll(iterable:Iterable, stop:int, start:int=0, step:int=1) -> tuple: """ Return 'stop' elements from an effective cycle of the iterable, using only those whose modulus is greater than 'start' Example: - pull('boris',5) - ('b', 'o', 'r', 'i', 's') - pull('boris',6) - ('b', 'o', 'r', 'i', 's', 'b') - pull('boris',6,1) - ('o', 'r', 'i', 's', 'b', 'o') - pull('boris',6,1,2) - ('r', 's', 'o') + >>> roll('boris', 5) + ('b', 'o', 'r', 'i', 's') + >>> roll('boris', 6) + ('b', 'o', 'r', 'i', 's', 'b') + >>> roll('boris', 6, 1) + ('o', 'r', 'i', 's', 'b', 'o') + >>> roll('boris', 6, 1, 2) + ('r', 's', 'o') """ - consumable = regurge(iterable) - consumable = list(consumable) - return tuple(consumable[i%len(consumable)] for i in range(0, stop+start, step) if i>=start) + consumable = regenerator(iterable) + return tuple(consumable[(i + 0) % len(consumable)] for i in range(0, stop + start, step) if i >= start) def deduplicate(unhashable:Iterable) -> dict: """ Because dictionaries seem to be less hashable than lists, which are also formally unhashable - This will consume a generator + This will consume a Generator """ if isinstance(unhashable, dict): trimmed = {} @@ -142,63 +360,55 @@ def deduplicate(unhashable:Iterable) -> dict: raise TypeError(f'Protocol for your {tipo(unhashable)} is pending') -def band(iterable:Iterable) -> float: +def band(iterable:Iterable[Real]) -> Real: """ Returns the extrema of the given iterable """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) return min(consumable), max(consumable) -def bandGap(iterable:Iterable) -> float: +def bandgap(iterable:Iterable[Real]) -> Real: """ Returns the breadth of a given iterable of elements overwhich subtraction, max, and min, are well defined """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) return max(consumable) - min(consumable) -def options(iterable:Iterable) -> int: - """ - Returns the number of ways to choose elements from the given iterable - This will consume a generator - """ - consumable = regurge(iterable) - consumable = list(consumable) - length = len(consumable) - return sum(binomial(length, i) for i in range(length)) - - def lispart(lis:Iterable, depth:int) -> list: """ Returns a collection of n*m elements as a list of m lists with n elements each devised by David C. Ullrich from stack exchange """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(consumable) assert(len(consumable)%depth == 0), f'The iterable cannot be factored into "{depth}" arrays' return [consumable[j*depth:(j+1)*depth] for j in range(int(len(consumable)/depth))] -def sigma(iterable:Iterable[complex], v0:Any=0) -> complex: +def cast(x:int, y:int, iterable:Iterable=None) -> list: + """ + Return a YxX matrix (y lists of length x) whose elements are from slices of the iterable + """ + base = regenerator(iterable) if iterable else tuple(range(x * y)) + assert x*y==len(list(tee(base)[0])), f"Iterable does not cast to (x, y) = {(x, y)}" + # return [base[slice(i*x, x+i*x)] for i in range(y)] + return [base[i * x: x + i * x] for i in range(y)] + +def sigma(iterable:Iterable[Any], v0:Any=0) -> Any: """ Returns the sum of a iterable """ - consumable = regurge(iterable) - consumable = list(consumable) - for i in consumable: + for i in iterable: v0 += i return v0 -def pipe(iterable:Iterable[complex]) -> complex: +def pipe(iterable:Iterable[Number]) -> Number: """ Returns the multiplicative product of the elements of a collection """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) if len(consumable) < 1: return "this list is empty" else: @@ -208,30 +418,29 @@ def pipe(iterable:Iterable[complex]) -> complex: return v0 -def powerset(iterable:Iterable) -> regenerator: +def powerset(iterable:Iterable[Any]) -> regenerator: """ Returns the powerset of an iterable as a list """ consumable = regenerator(iterable) - return chain.from_iterable(combinations(consumable, r) for r in range(sum(1 for i in consumable)+1)) + return regenerator(chain.from_iterable(combinations(consumable, r) for r in range(sum(1 for i in consumable)+1))) -def sample(iterable:Iterable, size:int) -> tuple: +def sample(iterable:Iterable[Any], size:int) -> tuple: """ Obtains a random sample of any length from any iterable and returns it as a tuple + Unlike random.sample, this may yield the same element several times. """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) choiceIndices = tuple(random.randint(0, len(consumable)-1) for i in range(size)) return tuple(consumable[i] for i in choiceIndices) -def shuffle(iterable:Iterable) -> tuple: +def shuffle(iterable:Iterable[Any]) -> tuple: """ Given an iterable, this function returns a new tuple of its elements in a new order """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) cache = [] pot = [] while len(cache) < len(consumable): @@ -249,13 +458,13 @@ def unzip(iterable:Iterable[Sequence[Any]]) -> List[list]: This is about the same as a clockwise rotation of a matrix by 90 degrees This will omit empty arrays examples - >>> m3ta.show(unzip(range(3) for i in range(3))) + >>> sl4ng.show(unzip(range(3) for i in range(3))) [0, 0, 0] [1, 1, 1] [2, 2, 2] - >>> m3ta.show(unzip(range(j) for j in range(10))) + >>> sl4ng.show(unzip(range(j) for j in range(10))) [0, 0, 0, 0, 0, 0, 0, 0, 0] [1, 1, 1, 1, 1, 1, 1, 1] [2, 2, 2, 2, 2, 2, 2] @@ -268,8 +477,8 @@ def unzip(iterable:Iterable[Sequence[Any]]) -> List[list]: """ - consumable = regurge(iterable) - str_escape = lambda string: string.replace("'", "\'") + consumable = regenerator(iterable) + str_escape = lambda string: string.replace("'", "\'").replace("\\", "\\\\") length = 0 racks = [] for i in consumable: @@ -278,11 +487,11 @@ def unzip(iterable:Iterable[Sequence[Any]]) -> List[list]: exec(f"x{j} = []") racks.append(eval(f'x{j}')) length += 1 - app_elem = f"x{j}.append('{str_escape(k)}')" if isinstance(k, str) else f"x{j}.append({k})" + app_elem = f"""x{j}.append('{(k, str_escape(k))[isinstance(k, str)]}')""" eval(app_elem) return racks -def diffs(iterable:Iterable[complex], flip:bool=False) -> generator: +def diffs(iterable:Iterable[Number], flip:bool=False) -> Generator: """ Yield the difference between each element of an iterable and its predecessor example: @@ -291,17 +500,17 @@ def diffs(iterable:Iterable[complex], flip:bool=False) -> generator: >>> [*diffs(range(3), True)] [-1, -1] """ - consumable = iter(regurge(iterable)) + consumable = iter(iterable) last = next(consumable) for i in consumable: yield i - last if not flip else last - i last = i -def discontinuities(iterable:Iterable[complex], delta:complex=1) -> generator: +def discontinuities(iterable:Iterable[Number], delta:Number=1) -> Generator: """ Obtain the ordinal positions of any elements who do not differ from their successors by delta Iterable must, at least, be a semigroupoid with subtraction - Not generator safe. + Not Generator safe. example: >>> [*discontinuities(range(3))] [] @@ -315,7 +524,7 @@ def discontinuities(iterable:Iterable[complex], delta:complex=1) -> generator: yield i last = e -def stationaries(iterable:Iterable[complex]) -> generator: +def stationaries(iterable:Iterable[Number]) -> Generator: """ Generate the indices of the stationary points of an iterable example: @@ -331,7 +540,7 @@ def stationaries(iterable:Iterable[complex]) -> generator: yield i last = e -def discontinuous(iterable:Iterable[complex], differential:complex=1) -> generator: +def discontinuous(iterable:Iterable[Number], differential:Number=1) -> Generator: """ Check if an additive semigroupoid has any jumps which are inequivalent to a given differential example: @@ -352,7 +561,7 @@ def discontinuous(iterable:Iterable[complex], differential:complex=1) -> generat -def sums(iterable:Iterable[complex], flip:bool=False) -> generator: +def sums(iterable:Iterable[Number], flip:bool=False) -> Generator: """ Yield the sum of each element of an iterable and its predecessor example: @@ -361,13 +570,13 @@ def sums(iterable:Iterable[complex], flip:bool=False) -> generator: >>> [*sums('abc', True)] ['ab', 'bc'] """ - consumable = iter(regurge(iterable)) + consumable = iter(iterable) last = next(consumable) for i in consumable: yield i + last if not flip else last + i last = i -def quots(iterable:Iterable[complex], flip:bool=False) -> generator: +def quots(iterable:Iterable[Number], flip:bool=False) -> Generator: """ Yield the quotient of each element of an iterable by its predecessor example: @@ -376,13 +585,13 @@ def quots(iterable:Iterable[complex], flip:bool=False) -> generator: >>> [*quots(range(1, 4), True)] [0.5, 0.6666666666666666] """ - consumable = iter(regurge(iterable)) + consumable = iter(iterable) last = next(consumable) for i in consumable: yield i / last if not flip else last / i last = i -def prods(iterable:Iterable[complex], flip:bool=False) -> generator: +def prods(iterable:Iterable[Number], flip:bool=False) -> Generator: """ Yield the product of each element of an iterable by its predecessor example: @@ -391,13 +600,13 @@ def prods(iterable:Iterable[complex], flip:bool=False) -> generator: >>> [*prods(range(1, 4), True)] [2, 6] """ - consumable = iter(regurge(iterable)) + consumable = iter(iterable) last = next(consumable) for i in consumable: yield i * last if not flip else last * i last = i -def logs(iterable:Iterable[complex], flip:bool=False) -> generator: +def logs(iterable:Iterable[Number], flip:bool=False) -> Generator: """ Yield the log of each element of an iterable in the base of its predecessor example: @@ -406,13 +615,13 @@ def logs(iterable:Iterable[complex], flip:bool=False) -> generator: >>> [*logs(range(2, 5), True)] [0.6309297535714574, 0.7924812503605781] """ - consumable = iter(regurge(iterable)) + consumable = iter(iterable) last = next(consumable) for i in consumable: yield log(i, last) if not flip else log(last, i) last = i -def cumsum(iterable:Iterable[complex], first:bool=True) -> generator: +def cumsum(iterable:Iterable[Number], first:bool=True) -> Generator: """ Yield the log of each element of an iterable in the base of its predecessor example: @@ -421,7 +630,7 @@ def cumsum(iterable:Iterable[complex], first:bool=True) -> generator: >>> [*cumsum(range(4), False)] [1, 3, 6] """ - consumable = iter(regurge(iterable)) + consumable = iter(iterable) last = 0 if first: yield last @@ -429,7 +638,7 @@ def cumsum(iterable:Iterable[complex], first:bool=True) -> generator: last += i yield last -def cumdif(iterable:Iterable[complex], first:bool=True) -> generator: +def cumdif(iterable:Iterable[Number], first:bool=True) -> Generator: """ Yield the log of each element of an iterable in the base of its predecessor example: @@ -438,7 +647,7 @@ def cumdif(iterable:Iterable[complex], first:bool=True) -> generator: >>> [*cumdif(range(4), False)] [-1, -3, -6] """ - consumable = iter(regurge(iterable)) + consumable = iter(iterable) last = next(consumable) if first: yield last @@ -446,7 +655,7 @@ def cumdif(iterable:Iterable[complex], first:bool=True) -> generator: last -= i yield last -def cumprod(iterable:Iterable[complex], first:bool=True) -> generator: +def cumprod(iterable:Iterable[Number], first:bool=True) -> Generator: """ Yield the cumulative product of the elemements of an iterable example: @@ -455,7 +664,7 @@ def cumprod(iterable:Iterable[complex], first:bool=True) -> generator: >>> [*cumprod(range(1, 4), False)] [2, 6] """ - consumable = iter(regurge(iterable)) + consumable = iter(iterable) last = next(consumable) if first: yield last @@ -463,7 +672,7 @@ def cumprod(iterable:Iterable[complex], first:bool=True) -> generator: last *= i yield last -def cumquot(iterable:Iterable[complex], first:bool=True) -> generator: +def cumquot(iterable:Iterable[Number], first:bool=True) -> Generator: """ Yield the cumulative quotient of the elemements of an iterable example: @@ -472,7 +681,7 @@ def cumquot(iterable:Iterable[complex], first:bool=True) -> generator: >>> [*cumquot(range(1, 4), False)] [0.5, 0.16666666666666666] """ - consumable = iter(regurge(iterable)) + consumable = iter(iterable) last = next(consumable) if first: yield last @@ -481,7 +690,7 @@ def cumquot(iterable:Iterable[complex], first:bool=True) -> generator: yield last -def choose(iterable:Iterable[Any], *indices:Sequence[int]) -> generator: +def choose(iterable:Iterable[Any], *indices:Iterable[int]) -> Generator: """ Yield specific elements from an iterable by index: >>> [*choose(range(1, 10), 0, 3)] @@ -490,7 +699,7 @@ def choose(iterable:Iterable[Any], *indices:Sequence[int]) -> generator: [1, 4] """ - indices = [*flatten(indices)] + indices = regenerator(flat(indices)) yielded = [] for i, e in enumerate(iterable): if i in indices: @@ -499,11 +708,16 @@ def choose(iterable:Iterable[Any], *indices:Sequence[int]) -> generator: if len(yielded) == len(indices): break +def skip(iterable:Iterable[Any], *indices:Iterable[int]) -> Generator: + """ + Skip specific elements from an iterable by index + """ + indices = tuple(flat(indices)) + for i, e in enumerate(iterable): + if not i in indices: + yield e -# def chooser(iterable:Iterable, *) - - -def dupes(array:Iterable, once:bool=True, key:Callable=lambda x:x) -> generator: +def dupers(array:Iterable, once:bool=True, key:Callable=lambda x:x) -> Generator: """ Yield the elements of a finite iterable whose frequency is greater than one :once: @@ -529,7 +743,7 @@ def dupes(array:Iterable, once:bool=True, key:Callable=lambda x:x) -> generator: last_yield = i last_seen = i -def slices(iterable:Iterable, length:int, fill:Any=None) -> generator: +def slices(iterable:Iterable, length:int, fill:Any=None) -> Generator: """ Yield the adjacent slices of a given length for the given iterable. Trailing values will be padded by copies of 'fill' use filter(all, slices(iterable, length)) to discard remainders @@ -546,8 +760,227 @@ def slices(iterable:Iterable, length:int, fill:Any=None) -> generator: main += [fill for i in range(length-len(main))] yield tuple(main) + +def repeat(func:Callable, inpt:Any, times:int=2, **kwargs): + """ + Get the output of recursive calls of a function some number of times + Dependencies: None + In: function,input,times[=1] + Out: Function call + + repeat(lambda x: x+1,0) + 2 + repeat(lambda x: x+1,0,1) + 1 + """ + for i in range(times): + inpt = func(inpt, **kwargs) + return inpt +recursor = repeat + + +def eq(*args:Iterable[Any]) -> bool: + """ + Check if arguments are equal + Will always return True if the only argument given is not an iterable + """ + if len(args) == 1: + if hasattr(arg:=args[0], '__iter__'): + return eq(*arg) + return True + + args = iter(args) + arg0 = next(args) + + for i in args: + if i != arg0: + return False + return True + + +def clock(func:Callable, value:bool=False, name:bool=False, verbose:bool=False) -> Callable: + """ + A decorator for benchmarking functions + + :name: + option to print the name of the function with the time taken to call it + :value: + return the value of the function instead of the period + + taken from + Mari Wahl, mari.wahl9@gmail.com + University of New York at Stony Brook + """ + def wrapper(*args:Any, **kwargs:Any) -> Any: + t = time.perf_counter() + res = func(*args, **kwargs) + delta = time.perf_counter()-t + if verbose: + if name: + print(f"{func.__name__}\n\t{delta}") + else: + print(f"d={delta}") + return res if value else delta + return wrapper +benchmark = clock + + +def imap(argument:Any, *functions:Callable) -> Generator: + """ + Converse of a map. Yield calls of the functions with the object as an argument + Generator safe + """ + iterates = hasattr(object, '__iter__') + for func in functions: + yield func((argument, regenerator(argument))[iterates]) + + +def rmap(argument:Any, *functions:Callable) -> Any: + """ + Recursively call functions on an argument. Return the result of the last call + Generator safe + """ + functions = iter(functions) + iterates = hasattr(object, '__iter__') + result = next(functions)((argument, regenerator(argument))[iterates]) + for func in functions: + result = func((result, regenerator(result))[iterates]) + return result + + +def dictate(dictionary:dict, *omissions:Hashable, include:bool=False): + """ + Create a selective clone of a dictionary + params: + omissions + the hashable values corresponding to keys you would like to remove + include + If true, only keys specified in "omissions" will persist in the final dictionary + + >>> dictate(dict(zip('abc', range(3))), 'a') + {'b': 1, 'c': 2} + >>> dictate(dict(zip('abc', range(3))), 'a', include=True) + {'a': 0} + >>> dictate(dict(zip('abc', range(3))), 'b c'.split(), include=True) + {'b': 1, 'c': 2} + + """ + if include: + return {key: dictionary.get(key) for key in dictionary if key in flat(omissions)} + return {key: dictionary.get(key) for key in dictionary if not key in flat(omissions)} + + +def itersplit(iterable:Iterable[Any], *indices:int, cumulative:bool=False): + """ + Split an iterable at the given indices or their cumulative sums + + examples: + >>> sl4ng.show(itersplit('gravitation', 3, 6, 8)) + ('g', 'r', 'a') + ('v', 'i', 't') + ('a', 't') + ('i', 'o', 'n') + >>> sl4ng.show(itersplit('gravitation', 3, 3, 2, cumulative=True)) + ('g', 'r', 'a') + ('v', 'i', 't') + ('a', 't') + ('i', 'o', 'n') + """ + indices = flat(indices) + indices = regenerator(cumsum(indices, first=False)) if cumulative else sorted(indices) + assert all(isinstance(i, int) for i in indices), "Some indices aren't integers" + + boxes = tuple([] for i in indices) + index = 0 + iterable = iter(iterable) + for i, e in enumerate(iterable): + if i >= indices[index]: + yield tuple(boxes[index]) + index += 1 + + try: + boxes[index].append(e) + except IndexError: + break + + + if (last := tuple([e, *iterable])): + yield tuple(last) + + +def interval(start:float, stop:float, step:float, reverse=False) -> Generator: + """ + Compute an [x0, x1, dx)-interval and its cardinality + Params + start: + least element of the interval + stop: + cutoff element + step: + difference between consecutive elements + reverse: + traverse the interval in reverse order + + """ + # ctr = 0 + # itr = [] + while start + step < stop: + # itr.append(start) + yield start + start += step + # ctr += 1 + # return ctr, itr + +class Interval: + def __init__(self, start:float, stop:float, step:float): + """ + Compute a yielding [)-interval and access useful properties such as its cardinality. + This is essentially equivalent to "range", for non-integer aritmetics + Params + start: + least element of the interval + stop: + cutoff element + step: + difference between consecutive elements + reverse: + traverse the interval in reverse order + + """ + self.start = start + self.stop = stop + self.step = step + def __iter__(self): + start = self.start + stop = self.stop + step = self.step + while start + step < stop: + yield start + start += step + def __len__(self): + return sum(1 for i in self) + def __reversed__(self): + start = self.start + stop = self.stop + step = self.step + while stop - step > start: + yield start + stop -= step + def __getitem__(self, key:int): + for index, item in enumerate(self): + if index==key: + return item + raise IndexError + if __name__ == "__main__": + from statistics import median # pass - for i in range(1,10): - print([*range(i)]) - print(median(range(i))) \ No newline at end of file + # for i in range(1,10): + # print([*range(i)]) + # print(median(range(i))) + n = 6 + for i in range(n): + print(roll(range(3), i)) + for i in range(n): + print(roll(range(3), 3, i)) + \ No newline at end of file diff --git a/sl4ng/magnitudes.py b/sl4ng/magnitudes.py new file mode 100644 index 0000000..2123e52 --- /dev/null +++ b/sl4ng/magnitudes.py @@ -0,0 +1,215 @@ +""" +Making numbers readable and dimensional one order of magnitude at a time +""" +__all__ = 'nice_size nice_duration MemorySize Duration'.split() + +from .debug import show +from .iteration import sample, nopes + + +orders = { + 0: 'mono', + 1: 'deca', + 2: 'hecto', + 3: 'kilo', + 6: 'mega', + 9: 'giga', + 12: 'tera', + 15: 'peta', + 18: 'exa', + 21: 'zetta', + 24: 'yotta', +} + +sredro = {v:k for k, v in orders.items()} + +def lasso(number:complex, unit:str='') -> str: + """ + Make a large number more readable by inserting commas before every third power of 10 and adding units + """ + return f'{number:,} {unit}'.strip() + +def set_case(unit:str, lower:bool=False) -> str: + """ + Set the case of a number's unit string + """ + return [unit.upper().strip(), unit.lower().strip()][lower] + +def set_length(mag:str, unit:str, long:bool=False, sep:str='-') -> str: + """ + Set the length of a number's unit string + """ + return ('', sep)[long].join(((mag[0], unit[0]), (mag, unit))[long]) + +def magnitude(value:complex, omissions:list='mono deca hecto'.split()): + """ + Determine the best order of magnitude for a given number + """ + mags = tuple(sorted(i for i in orders.keys() if not orders[i] in omissions)) + booly = lambda i: len(str(int(value))) < len(str(10**mags[i+1])) + fits = tuple(nopes((booly(i) for i in range(len(mags)-1)),True)) + fit = mags[min(fits) if fits else len(mags)-1] + return orders[fit] + +def nice_size(self:complex, unit:str='bytes', long:bool=False, lower:bool=False, precision:int=2, sep:str='-', omissions:list='mono deca hecto'.split()): + """ + This should behave well on int subclasses + """ + mag = magnitude(self, omissions) + precision = sredro[mag] if self<5 else precision + unit = set_case(set_length(mag, unit, long, sep), lower) + val = round(self*10**-(sredro[mag]), precision) + return lasso(val, unit) + +class MemorySize(int): + """ + Why should you have to sacrifice utility for readability? + """ + def __repr__(self): + return nice_size(self) + + + +second = 1 +minute = 60 * second +hour = 60 * minute +day = 24 * hour +week = 7 * day +feb = 4 * week +year = 365.25 * day +# year = 13.044642857142858 * feb + + +def time_dict(duration): + periods = { + # 'years': int(duration // year), + 'febs': int(duration // feb), + # 'febs': int(duration % year // feb), + 'weeks': int(duration % feb // week), + # 'weeks': int(duration % feb // week), + 'days': int(duration % week // day), + # 'days': int(duration % week // day), + 'hours': int(duration % day // hour), + # 'hours': int(duration % day // hour), + 'minutes': int(duration % hour // minute), + # 'minutes': int(duration % hour // minute), + + 'seconds': round(duration % minute), + } + return periods + + +def length(period): + return eval(period[:-1]) + + +def timer_duration(periods, peak, base, watch): + string = '' + if watch: + peak = 'hours' + base = 'minutes' + elif length(base) > second: + base = 'seconds' + for k,v in periods.items(): + if length(peak) >= length(k) >= length(base): + if k == peak: + string += f'{v:02d}' + else: + string += f':{v:02d}' + return string + + +def verbose_duration(periods, peak, base, short=False): + string = '' + for k, v in periods.items(): + if v: + if length(peak) >= length(k) >= length(base): + if k == peak: + string += f'{v} {k[:1 if short else None]}' + else: + string += f', {v} {k[:1 if short else None]}' + return string + + +def nice_duration(duration, verbose=False, short=False, watch=False): + """ + Given a number of seconds, return a string which indicates the amount of time in: + Months (28 days), weeks, days, hours, minutes, seconds + + parameters: + duration + number of seconds + verbose + include units in result + short + abbreviate units + watch + only "hours:minutes" + + examples: + >>> humanize_duration(62) + '01:02' + + >>> t = 12532510 + + >>> humanize_duration(t) + 20:05:01:15:10 + + >>> humanize_duration(t, verbose=True) + 20 weeks, 5 days, 1 hours, 15 months, 10 seconds + + >>> humanize_duration(t,verbose=True, short=True) + 20 w, 5 d, 1 h, 15 m, 10 s + + >>> humanize_duration(t, watch=True) # equates to "what time will it be after this many seconds have passed" + 01:15 + """ + periods = time_dict(duration) + significant = [i for i in periods.keys() if periods.get(i)] + base = min(significant, key=length) + peak = max(significant, key=length) + if not verbose: + return timer_duration(periods, peak, base, watch) + return verbose_duration(periods, peak, base, short) + +class Duration(float): + """ + Why should you have to sacrifice utility for readability? + """ + def __repr__(self): + return nice_duration(self) + + + +if __name__ == '__main__': + band = lambda level, base=10, shift=0, root=1: tuple(root+i+shift for i in ((level+1)*base, level*base))[::-1] + format = lambda selection: int(''.join(str(i) for i in selection)) + + digits = range(*band(0)) + + l = 0 + s = 0 + b = 30 + r = 1 + x,y = band(l, b, s, r) + # print(x,y) + val = sample(digits,y) + sizes = [format(val[:i][::-1]) for i in range(x, y)] + # show(sizes) + # sizes = [44259260028,315436] + + for size in sizes[::-1]: + string = f"""{(len(lasso(max(sizes)))-len(lasso(size)))*' '+lasso(size)} + {size = } + {len(str(size)) = } + {nice_size(size) = } + {magnitude(size) = } + + + """.splitlines() + print(*((x.strip(), x)[i<1] for i, x in enumerate(string)), sep='\n\t', end='\n\n') + print(lasso(sizes[-1])) + print(band(l,s,b,r)) + print(x,y) + print(f'{format(digits):,}') + # print(all(rep(size)==rep2(size) for size in sizes)) \ No newline at end of file diff --git a/sl4ng/maths.py b/sl4ng/maths.py index 57ec654..c313b31 100644 --- a/sl4ng/maths.py +++ b/sl4ng/maths.py @@ -3,78 +3,72 @@ from functools import lru_cache, reduce from itertools import chain, combinations, count from math import pi, ceil -from typing import Iterable +from typing import Iterable, Generator +from numbers import Number, Real, Complex, Integral -from .types import generator +# from .types import regenerator +from .iteration import flat, regenerator -def pyramid(length:int, shift:int=0) -> float: +def sign(number:Real) -> str: + return '-+'[number >= 0] + +def pyramid(length:Integral, shift:Integral=0) -> Real: """ Returns the product of dividing 1 by each intger in the range(2+shift,2+int(length)+shift) (aka - the number of poops in your pocket) if the length is not an integer it will be converted to one - Dependencies: N/A - In: length, shift=0 (integers) - Out: N/A """ val = 1 - for i in range(2+shift, 2+int(length)+shift): + for i in range(2 + shift, 2 + int(length) + shift): val /= i return val -def primes1(n:int) -> list: - """Generates a list of the first n primes. A cast will be used if the input is not an integer - Dependencies: N/A - In: (number) - Out: list""" - # pwimes = list(x for x in range(n) if x > 1 and all(x%y for y in range(2, min(x, 11)))) +def first_primes(n:Integral) -> list: + """ + Generates a list of the first n primes. A cast will be used if the input is not an integer + """ n = int(n) - 1 bank = [] track = 2 - while len(bank) list: - """Generates a list of primes with value lower than the input integer - Dependencies: N/A - In: (integer) - Out: list""" - pwimes = list({x for x in range(n) if x > 1 and all(x%y for y in range(2, min(x, 11)))}) - return list(pwimes) +def primeslt(n:Integral) -> Generator: + """ + Generates a list of primes with value lower than the input integer + """ + return (x for x in range(n) if x > 1 and all(x % y for y in range(2, min(x, 11)))) +primes_lower_than = primeslt + +def succ(n:Integral) -> Integral: + """ + Returns the successor of the input number + """ + return n + 1 -def succ(n:int) -> int: - """Returns the successor of the input number - Dependencies: N/A - In: (number) - Out: number""" - return n+1 +def pred(n:Integral) -> Integral: + """ + Returns the predecessor of the input number + """ + return n - 1 -def pred(n:int) -> int: - """Returns the predecessor of the input number - Dependencies: N/A - In: (number) - Out: number""" - return n-1 +def rt(x:Integral, n:Complex=2) -> Complex: + """ + Returns the n-th root of the input number, x + """ + return x ** (1 / n) -def rt(x:int, n:complex) -> complex: - """Returns the nth root of the input number, x - Dependencies: N/A - In: (x, n) - Out: float(number)""" - return x**(1/n) - - -def gcd0(a:int, b:int) -> int: - """The famous euclidean algorithm for computing the greatest common divisor of a pair of numbers a and b - Dependencies: N/A - In: (a: first number, b: second number) - Out: int""" +def gcd0(a:Integral, b:Integral) -> Integral: + """ + The famous euclidean algorithm for computing the greatest common divisor of a pair of numbers a and b + """ while a != b: if a > b: a -= b @@ -83,30 +77,24 @@ def gcd0(a:int, b:int) -> int: return a -def gcd(*args:[int, tuple]) -> int: +def gcd(*args:[int, tuple]) -> Integral: """ Compute the gcd for more than two integers at a time. Returns input if only one argument is given and it is greater than zero - Dependencies: itertools.combinations - In: subscriptable - Out: int """ - if any(i<=0 for i in args): + if any(i <= 0 for i in args): return None - if len(args)>1: - gcds = {d for pair in combinations(args, 2) if all(i%(d:=gcd0(*pair))==0 for i in args)} - return max(gcds) - elif sum(args)>0: - return args[0] + if len(args) > 1: + gcds = {d for pair in combinations(args, 2) if all(i % (d := gcd0(*pair)) == 0 for i in args)} + return max(gcds) if gcds else 1 + elif sum(args) > 0: + return max(args) -def eratosthenes(n:int, imaginarypart:bool=False) -> generator: +def eratosthenes(n:Integral, imaginarypart:bool=False) -> Generator: """ - Implements eratothenes' sieve as a generator. + Implements eratothenes' sieve as a Generator. If the input is not an int it will be rounded to one. Imaginary-part-based rounding optionable - Dependencies: None - In: int - Out: generator """ iscomp = isinstance(n, complex) or issubclass(type(n), complex) n = round(n.imag) if imaginarypart and iscomp else round(n.real) if iscomp else round(n) @@ -115,18 +103,15 @@ def eratosthenes(n:int, imaginarypart:bool=False) -> generator: primes = set() for i in rack: if i not in marked: - multiples = {j for j in rack if j%i==0 and j>i} + multiples = {j for j in rack if j % i == 0 and j > i} marked.update(multiples) yield i -def factors0(n:int) -> generator: - ''' +def _factors(n:Integral) -> Generator: + """ Compute the factors of an integer - Dependencies: itertools.(chain, combinations) - In: int, or sequence of ints - Out: generator - ''' + """ pipe = lambda array: reduce(lambda x, y: x*y, array, 1) primes = tuple(eratosthenes(n)) facts = {n, 1} if n!= 0 else {} @@ -145,34 +130,26 @@ def factors0(n:int) -> generator: yield from facts -def factors(*args:[int, tuple]) -> generator: - ''' +def factors(*args:[int, tuple]) -> Generator: + """ Compute the common factors of any finite number of integers - Dependencies: m3ta.factors0 - In: int, or sequence of ints - Out: generator - ''' - if len(args) == 1 and hasattr(args[0], '__iter__'): - args = tuple(args[0]) + """ + args = regenerator(flat(args)) if all(isinstance(i, int) or i==int(i) for i in args): - # facs = [] + yielded = set() for i in args: - for j in factors0(i): - # facs.append(j) - if all(not arg%j for arg in args): - yield j - # for i in facs: - # if freq(i, facs)==len(args): - # yield i + if not i in yielded: + for j in _factors(i): + if not j in yielded: + yielded.add(j) + if all(not arg%j for arg in args): + yield j @lru_cache(maxsize = 500) -def factorial(n:int) -> int: +def factorial(n:Integral) -> Integral: """ Return n! for any integer - Dependencies: lru_cache(from functools) - In: (int) - Out: int """ if n>=0: k = 1 @@ -184,124 +161,133 @@ def factorial(n:int) -> int: return -factorial(abs(n)) -def binomial(n:int, k:int) -> int: - '''Returns the n choose k for any k in range(n) - Dependencies: factorial - In: (integer) - Out: float''' - return round(factorial(n)/(factorial(n-k)*factorial(k))) +def binomial(n:Integral, k:Integral) -> Integral: + """ + Returns the n choose k for any k in range(n) + """ + return int(factorial(n)/(factorial(n-k)*factorial(k))) -def isHarmoDiv(n:int) -> bool: - """Computes a boolean whose value corresponds to the statement 'the number n is a Harmonic Divisor Number' - Dependencies: harmean (from meta) - In: Number - Out: Boolean""" +def options(iterable:Iterable) -> Integral: + """ + Returns the number of ways to choose elements from the given iterable + This will consume a Generator + """ + consumable = regenerator(iterable) + length = sum(1 for i in consumable) + return sum(binomial(length, i) for i in range(length)) + + +def isHarmoDiv(n:Integral) -> bool: + """ + Computes a boolean whose value corresponds to the statement 'the number n is a Harmonic Divisor Number' + """ facts = factors(n) return int(harmean(facts)) == harmean(facts) -def isFactor(divisor:int, predicate:int) -> bool: - """Determines if a Number is a multiple of a Divisor - Dependencies: N/A - In: (Divisor, Number) - Out: Boolean""" - return predicate % divisor == 0 +def isfactor(divisor:Integral, divisee:Integral) -> bool: + """ + Determines if a Number is a multiple of a Divisor + """ + return divisee % divisor == 0 -def isprime(n:int) -> bool: - """Determines if a Number is a multiple of a Divisor - Dependencies: N/A - In: (Divisor, Number) - Out: Boolean""" - return n in eratosthenes(n+1) +def isprime(n:Integral) -> bool: + """ + Confirm that an integer has no factors other than 1 and itself + """ + if n < 2: + return False + tried = [] + for i in range(2, int(n/2)+1): + if any(not i%j for j in tried): + continue + else: + tried.append(i) if i > 1 else None + if not n%i: + return False + return True -def isPerfect(n:int) -> bool: - """Returns a Boolean evaluation of an integer's Arithmetic Perfection - Dependencies: sigma, factors (both from meta) - In: Integer - Out: Boolean""" +def isperfect(n:Integral) -> bool: + """ + Check if an integer is equal to the sum of its factors + """ return n == sum(factors(n)) -def isAbundant(n:int) -> bool: - """Returns a Boolean evaluation of an integer's Arithmetic Abundance - Dependencies: sigma, factors (both from meta) - In: Integer - Out: Boolean""" +def isabundant(n:Integral) -> bool: + """ + Check if an integer is greater than the sum of its factors + """ return n > sigma(factors(n)) -def isDeficient(n:int) -> bool: - """Returns a Boolean evaluation of an integer's Arithmetic Deficience - Dependencies: sigma, factors (both from meta) - In: Integer - Out: Boolean""" +def isdeficient(n:Integral) -> bool: + """ + Check if an integer is smaller than the sum of its factors + """ return n < sigma(factors(n)) -def isFilial(n:int) -> bool: - fctrs = factors(n) - lace = [int(i) for i in str(n)] - return sigma(lace) in fctrs +def isfilial(n:Integral) -> bool: + """ + Check if an integer is divisible by the sum of its digits + """ + return not n % sum(eval(i) for i in str(n)) -def mulPer(n:int) -> int: - """Computes the Multiplicative Persistence of an int or float in base-10 positional notation - Dependencies: pipe (from meta) - In: Integer - Out: Integer""" +def mulper(n:Integral) -> Integral: + """ + Computes the Multiplicative Persistence of an int or float in base-10 positional notation + If the number is a float the decimal will be removed + """ # Exclusions if len(str(n)) == 1: return 0 elif (str(0) in str(n)) or ((len(str(n)) == 2) and (str(1) in str(n))): return 1 else: - cache = [] + ctr = 0 while len(str(n)) > 1: - digitList = [int(i) for i in "".join(str(n).split('.'))] - n = pipe(digitList) - cache.append(n) - return len(cache) + # digitList = [int(i) for i in "".join(str(n).split('.'))] + digitList = map(int, str(i).replace('.', '')) + n = reduce(lambda x, y: x*y, digitList, 1) + ctr += 1 + return ctr -def addPer(n:int) -> int: - """Computes the Additive Persistence of an int or float in base-10 positional notation - Dependencies: sigma (from meta) - In: Integer - Out: Integer""" +def addper(n:Integral) -> Integral: + """ + Computes the Additive Persistence of an int or float in base-10 positional notation + """ if len(str(n)) == 1: return 0 elif len(str(n)) < 1: return ValueError("Your number of choice has less than one digit") else: - cache = [] + ctr = 0 while len(str(n)) > 1: digitList = [int(i) for i in "".join(str(n).split('.'))] - n = sigma(digitList) - cache.append(n) - return len(cache) + n = sum(digitList) + ctr += 1 + return ctr -def triangular(a:int, b:int) -> int: - """Returns the triangular number of an interval [a,b] - dependencies: none - In: integers a and b - Out: integer""" +def triangular(a:Integral, b:Integral) -> Integral: + """ + Returns the triangular number of an interval [a,b] + """ interval = [i for i in range(b) if i > a] for num in interval: a += num return a+b -def rationability(v:complex) -> complex: +def rationability(v:Complex) -> Complex: """ Get the subtractive series of the digits of a float, or that of an integer's reciprocal, measured from its math.ceil value - Dependencies: math.ceil - In: int/float - Out: float - rationability(math.pi) 0.8584073464102069 rationability(3) @@ -315,7 +301,7 @@ def rationability(v:complex) -> complex: return n -def root(value:complex, power:complex=2) -> complex: +def root(value:Complex, power:Complex=2) -> Complex: """ Get the root of a number @@ -327,7 +313,7 @@ def root(value:complex, power:complex=2) -> complex: return value**(1/power) -def odds(n:int=-1) -> generator: +def odds(n:Integral=-1) -> Generator: """ Yield the first n odd numbers, use a negative value for all of them """ @@ -338,7 +324,7 @@ def odds(n:int=-1) -> generator: yield m n -= 1 -def evens(n:int=-1) -> generator: +def evens(n:Integral=-1) -> Generator: """ Yield the first n even numbers, use a negative value for all of them """ @@ -349,14 +335,14 @@ def evens(n:int=-1) -> generator: yield m n -= 1 -def congrues(n:int, modulus:int=6, cls:int=1) -> bool: +def congrues(n:Integral, modulus:Integral=6, cls:Integral=1) -> bool: """ Check if n is equal to +-cls modulo modulus """ return n % modulus in {cls, modulus-cls} -def mulseq(root:int, base:int=2, terms:int=-1, start:int=0, step:int=1) -> generator: +def mulseq(root:Integral, base:Integral=2, terms:Integral=-1, start:Integral=0, step:Integral=1) -> Generator: """ Generate a sequence of multiples of a root and a base. By default it will yield the doubles sequence of the root. """ diff --git a/sl4ng/persistance.py b/sl4ng/persistance.py deleted file mode 100644 index e18a125..0000000 --- a/sl4ng/persistance.py +++ /dev/null @@ -1,71 +0,0 @@ -from typing import Any -import os, pickle, re - -import dill - -from .iteration import deduplicate -from .debug import tryimport -from .files import nameSpacer - -def dillsave(obj:Any, filename:str, overwrite:bool=True) -> Any: - """ - Pickles the given object to a file with the given path/name. - If the object cannot be serialized with pickle, we shall use dill instead - Returns the object - """ - if overwrite: - with open(filename, 'wb') as fob: - dill.dump(obj, fob, protocol=dill.HIGHEST_PROTOCOL) - else: - with open(nameSpacer(filename), 'wb') as fob: - dill.dump(obj, fob, protocol=dill.HIGHEST_PROTOCOL) - -def save(obj:Any, filename:str, overwrite:bool=True) -> Any: - """ - Pickles the given object to a file with the given path/name. - If the object cannot be serialized with pickle, we shall use dill instead - Returns the object - """ - try: - if overwrite: - with open(filename, 'wb') as fob: - pickle.dump(obj, fob, protocol=pickle.HIGHEST_PROTOCOL) - else: - with open(nameSpacer(filename), 'wb') as fob: - pickle.dump(obj, fob, protocol=pickle.HIGHEST_PROTOCOL) - except pickle.PicklingError: - dillsave(obj, filename, overwrite) - except TypeError: - dillsave(obj, filename, overwrite) - return obj - -def load(filename:str) -> Any: - """ - Return unpickled data from a chosen file - If the file doesn't exist it will be created - """ - - if os.path.exists(filename): - with open(filename, 'rb') as fob: - var = pickle.load(fob) - return var - else: - x = open(filename) - x.close() - return - -def jar(file, duplicates:bool=False, flags:int=re.I): - """ - Consolidate your pickles and eat them, discard the clones by default though - """ - trash = tryimport('send2trash', 'send2trash', 'remove', 'os') - name, ext = os.path.splitext(file) - pattern = f'^{name}|{name}_\d+\.{ext}$' - p = re.compile(pattern) - folder = None if not os.sep in file else os.path.split(file)[0] - os.chdir(folder) if folder else None - matches = deduplicate({f: load(f) for f in os.listdir(folder) if p.search(f, flags)}) - results = list(matches.values())[:] - for i in matches: print(i) - [trash(file) for file in matches] - save(results, file) \ No newline at end of file diff --git a/sl4ng/stats.py b/sl4ng/stats.py index 6711152..6bc915e 100644 --- a/sl4ng/stats.py +++ b/sl4ng/stats.py @@ -4,14 +4,14 @@ from functools import lru_cache, reduce from itertools import tee from .strings import alphabet -from .types import regurge +from .iteration import regenerator +# from .types import regurge def shannonEntropy(iterable:Iterable[Any]) -> float: """ Returns the Information, or Shannon, Entropy of an iterable """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) weights = {j: freq(j, iterable)/len(consumable) for j in set(consumable)} return -sigma([val*log(val, 2) for val in weights.values()]) @@ -26,7 +26,7 @@ def entropy(iterable:Iterable[Any], base:int=2, mode:str='kbdUS', space:str=None digits = ''.join(i for i in abc if i.isnumeric() or i=='.' or i=='-') prob = lambda x,y: freq(x, y)/len(y) - consumable = regurge(iterable) + consumable = regenerator(iterable) iterates = hasattr(consumable,'__iter__') @@ -60,8 +60,7 @@ def probability(item:Any, iterable:Iterable) -> float: Returns the quotient of the frequency with which an item occurs in an iterable by the length of said iterable """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) return freq(item, consumable) / len(consumable) @@ -73,8 +72,7 @@ def freq(element:Any, iterable:Iterable[Any], overlap:bool=False) -> int: say you're looking for 00 and there's a "000" you may only get one of what could be two matches """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) if type(element)==type(consumable)==str: if not overlap: return len(consumable.split(element))-1 @@ -87,8 +85,7 @@ def expectation(iterable:Iterable[complex]) -> complex: """ Returns the expectation value of a collection """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) exVal = 0 for i in consumable: exVal += (i*(freq(i, consumable))/len(consumable)) @@ -99,8 +96,7 @@ def mean(iterable:Iterable[complex]) -> complex: """ Returns the mean value of a collection """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) meanVal = sum(consumable) meanVal /= len(consumable) return meanVal @@ -111,8 +107,8 @@ def median(iterable:Iterable[complex]) -> complex: Returns the median value of a collection Will avoid consuming a generator/map/filter """ - consumable = regurge(iterable) - consumable = sorted(consumable) + # consumable = regurge(iterable) + consumable = regenerator(sorted(iterable)) if len(consumable)%2: index = round((len(consumable)-1)/2) middle = consumable[index] @@ -127,8 +123,7 @@ def midpoint(iterable:Iterable[complex]) -> complex: Returns the midpoint of a collection Will avoid consuming a generator/map/filter """ - consumable = regurge(iterable) - consumable = sorted(consumable) + consumable = regenerator(iterable) return sum(max(consumable), min(consumable))/2 @@ -137,8 +132,7 @@ def central_deviation(iterable:Iterable[complex]) -> complex: Returns the difference between an iterable's midpoint and its median Will avoid consuming a generator/map/filter """ - consumable = regurge(iterable) - consumable = (consumable) + consumable = regenerator(iterable) return abs(median(consumable) - midpoint(consumable)) @@ -146,8 +140,7 @@ def geomean(iterable:Iterable[complex]) -> complex: """ Returns the geometric mean of a collection """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) geoMean = (sigma(iterable))**(1/len(iterable)) return geoMean @@ -156,8 +149,7 @@ def harmean(iterable:Iterable[complex]) -> complex: """ Returns the harmonic mean of a collection """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) assert 0 not in [i for i in consumable], "Input contains a zero, try a different one." reciprocals = [1/i for i in consumable] return len(consumable)/sigma(reciprocals) @@ -167,8 +159,7 @@ def popdev(iterable:Iterable[complex]) -> complex: """ Returns the population standard deviation of a collection """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) avg = mean(consumable) v1 = [(i-avg)**2 for i in consumable] v2 = ((reduce(lambda x, y: x+y, v1))/len(v1))**(1/2) @@ -179,8 +170,7 @@ def samdev(iterable:Iterable[complex]) -> complex: """ Returns the sample standard deviation of a collection """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) avg = mean(consumable) v1 = [(i-avg)**2 for i in consumable] v2 = ((sigma(v1))/(len(v1)-1))**(1/2) @@ -191,8 +181,7 @@ def popvar(iterable:Iterable[complex]) -> complex: """ Returns the population variance for a collection """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) avg = mean(consumable) deviations = [(i-avg)**2 for i in consumable] pv = sum(deviations)/len(deviations) @@ -203,8 +192,7 @@ def samvar(iterable:Iterable[complex]) -> complex: """ Returns the sample variance for a collection """ - consumable = regurge(iterable) - consumable = list(consumable) + consumable = regenerator(iterable) avg = mean(consumable) v2 = [(i-v1)**2 for i in consumable] v3 = sigma(v2)/len(v2) diff --git a/sl4ng/strings.py b/sl4ng/strings.py index 51025b8..ab5276e 100644 --- a/sl4ng/strings.py +++ b/sl4ng/strings.py @@ -1,15 +1,18 @@ -from typing import Iterable, Any +from typing import Iterable, Any, Generator, Callable, Iterator from itertools import chain import string, random import pyperclip -from .types import generator, regurge +from .iteration import regenerator +# from .types import function +from .debug import tipo + def join(iterable:Iterable[Any]=None, sep:str='', head:str='', tail:str='') -> str: """ Cast elements of an array to string and concatenate them. - This will consume a generator + This will consume a Generator Examples: >>> m3ta.show( map( @@ -34,7 +37,7 @@ def join(iterable:Iterable[Any]=None, sep:str='', head:str='', tail:str='') -> s [0, 1, 2] [0, 1, 2, 3] """ - if iterable: + if not isinstance(iterable, type(None)): return head + sep.join(map(str, iterable)) + tail else: def wrapper(iterable:Iterable[Any]): @@ -42,7 +45,7 @@ def join(iterable:Iterable[Any]=None, sep:str='', head:str='', tail:str='') -> s return head + sep.join(iterable) + tail return wrapper -def ascii(omissions:str='', include:bool=False) -> str: +def ascii(omissions:str='w', include:bool=False) -> str: """ Return the ascii character base excluding the given omissions: "p" -> ' ' + punctuation @@ -58,8 +61,9 @@ def ascii(omissions:str='', include:bool=False) -> str: "u":string.ascii_uppercase, "l":string.ascii_lowercase, "d":string.digits, + "w":string.whitespace, } - return "".join(d[k] for k in d if k in omissions) if include else "".join(d[k] for k in d if not k in omissions) + return "".join(d[key] for key in d if key in omissions) if include else "".join(d[key] for key in d if not key in omissions) asciis = kbd = abc = ascii @@ -106,28 +110,34 @@ def monoalphabetic(message:str, shift:int, alphabet:str=kbd(), space:str=None) - caesar = monoalphabetic -def multisplit(splitters:Iterable[str], target:str=None) -> generator: - """ - Split a string by a the elements of an iterable - >>> splitter = multisplit('word1 word2 word3'.split()) - >>> map(splitter, texts) - - OR - - >>> [*multisplit('breakfast', 'green eggs and ham')] - ['g', 'n ', 'gg', ' ', 'nd h', 'm'] - - OR - - >>> [*multisplit('breakfast'.split(), 'green eggs and ham')] - ['green eggs and ham'] - - """ - def wrapper(target): - nonlocal splitters - splitters = iter(splitters) - result = target.split(next(splitters)) - for splitter in splitters: - result = [*chain.from_iterable(i.split(splitter) for i in result)] - yield from filter(None, result) - return wrapper(target) if target else wrapper +def splitall(splitters:Iterable[str], target:str) -> regenerator: + """ + >>> [*splitall('-_.', 'author-file_name.ext')] == 'author file name ext'.split() + True + """ + splitters = iter(splitters) + result = target.split(next(splitters)) + for splitter in splitters: + result = [*chain.from_iterable(i.split(splitter) for i in result)] + yield from filter(None, result) +class splitter: + """ + Callable which splits a string by a the elements of an iterable. Ignore any empty strings. + >>> [*splitter('-_.')('author-file_name.ext')] == 'author file name ext'.split() + True + """ + def __init__(self, splitters:Iterable[str]): + self.splitters = regenerator(splitters) + def __call__(self, argument) -> regenerator: + return splitall(self.splitters, argument) + def __repr__(self): + return f"{tipo(self)}{tuple(self.splitters)}" +def multisplit(splitters:Iterable[str], target:str=None) -> (Callable, regenerator): + """ + Wrapper on sl4ng.strings.splitall and sl4ng.strings.splitter + """ + return splitall(splitters, target) if not isinstance(target, type(None)) else splitter(splitters) def memespace(string:str, spaces:int=1, keep_spaces:bool=False, copy:bool=False): """ @@ -172,18 +182,36 @@ def sinusize(string:str, copy:bool=False): """ mat = [[] for i in range(3)] for i, j in enumerate(string): - if not i%2: - mat[1].append(j) + if not i % 2: mat[0].append(' ') + mat[1].append(j) mat[2].append(' ') - elif not (i%4) - 1: + elif not (i % 4) - 1: mat[0].append(j) mat[1].append(' ') mat[2].append(' ') else: - mat[2].append(j) mat[0].append(' ') mat[1].append(' ') + mat[2].append(j) out = join([join(line) for line in mat], '\n') pyperclip.copy(out) if copy else None + return out + +def clean_url(url, root=True, protocol='http'): + """ + Remove trailing/leading slashes from a url + params + root + if the url starts at the domain level + protocol + ignored if root=False + what protocol should be added to the start of a url + """ + out = '/'.join(filter(None, url.split('/'))) + if root: + if ':/' in out: + out = out.replace(':/', '://') + else: + out = '://'.join((protocol, out)) return out \ No newline at end of file diff --git a/sl4ng/system.py b/sl4ng/system.py index 8a476fb..53600a8 100644 --- a/sl4ng/system.py +++ b/sl4ng/system.py @@ -1,12 +1,12 @@ import time, random, string import pyperclip, psutil +from .iteration import shuffle - -def agora(string:bool=False): +def agora(string:bool=False) -> str | tuple: """ - Here's the local time, mr wolf! + Here's the local time, Mrs. Wolf! Dependencies: time In: string=False Out: tuple/string(HH:MM:SS) @@ -15,9 +15,9 @@ def agora(string:bool=False): return now if not string else ':'.join(str(i) for i in now) -def kill(process:str, casefold=True): +def kill(process:str, casefold:bool=True): """ - Kill all processes of the argued process name + Kill all instances of the argued process name Dependencies: psutil In: "process_name.exe"[str],casefold=True[bool] Out: None @@ -31,15 +31,17 @@ def kill(process:str, casefold=True): [p.kill() for p in psutil.process_iter() if p.name()==process+('.exe', '')[process.endswith('.exe')]] -def guid(format:tuple=[8, 4, 4, 4, 12], sep:str='-', copy:bool=True) -> str: +def guid(format:tuple=[8, 4, 4, 4, 12], sep:str='-', copy:bool=True, braces:bool=True) -> str: """ Generate a GUID for different resources Dependencies: random,string,pyperclip.copy,m3ta.shuffle In: format{length of the intervals of the guid you want} Out: str """ - chars = ''.join(i for i in shuffle(string.ascii_uppercase + string.digits+string.ascii_lowercase) if i!=sep) + # chars = ''.join(i for i in shuffle(string.ascii_uppercase + string.digits+string.ascii_lowercase) if i!=sep) + chars = ''.join(i for i in shuffle(string.hexdigits) if i != sep) ranstr = [random.choice(chars) for i in range(sum(format))] [ranstr.insert(i + sum(format[:i+1]), sep) for i, j in enumerate(format[:-1])] - pyperclip.copy(''.join(ranstr)) if copy else None - return ''.join(ranstr) \ No newline at end of file + result = "{%s}" % (s := ''.join(ranstr)) if braces else s + pyperclip.copy(result) if copy else None + return result \ No newline at end of file diff --git a/sl4ng/types.py b/sl4ng/types.py index 35a1e98..7d41166 100644 --- a/sl4ng/types.py +++ b/sl4ng/types.py @@ -1,217 +1,33 @@ -__all__ = 'generator module function SineWave defaultdict'.split() +__all__ = 'generator module function DDict defaultdict'.split() from collections import defaultdict -from typing import Iterable, Any, Iterator, Sequence -from copy import deepcopy -from itertools import tee, _tee +from typing import Tuple +from warnings import warn import os +from .iteration import unique, flat +# from .persistance import save, load +from .debug import tipo generator = type(i for i in range(0)) module = type(os) function = type(lambda x: x) -class _regen: - @staticmethod - def choose(iterable:Iterable[Any], *indices:Sequence[int]) -> generator: - """ - Yield specific elements from an iterable by index: - >>> [*choose(range(1, 10), 0, 3)] - [1, 4] - >>> [*choose(range(1, 10), (0, 3))] - [1, 4] - - """ - indices = [*flatten(indices)] - yielded = [] - for i, e in enumerate(iterable): - if i in indices: - yield e - yielded.append(e) - if len(yielded) == len(indices): - break - @staticmethod - def tipo(inpt:Any=type(lambda:0), keep_module:bool=False) -> str: - """ - Return the name of an object's type - Dependencies: None - In: object - Out: str - """ - if keep_module: - return str(type(inpt)).split("'")[1] - return str(type(inpt)).split("'")[1].split('.')[-1] - @staticmethod - def flatten(iterable:Iterable) -> generator: - """ - Flatten a 2d iterable - Example: - >>> list(flatten([[1, 2], [3, 4]])) - [1, 2, 3, 4] - based on: - https://pythonprinciples.com/challenges/Flatten-a-list/ - """ - consumable = regurge(iterable) - for i in consumable: - if hasattr(i, '__iter__') or hasattr(i, '__next__'): - for j in i: - yield j - else: - yield i - - -class regenerator: +class DDict(defaultdict): """ - A self-replenishing (or non-consumable) iterator. All methods whose return value is iterable return regenerators - :args & kwargs: - Any arguments needed to initialize the generator-type/function. Will not be used unless hasattr(iterable, '__call__') and iterable is not a regenerator. - eg: - >>> x = regurge(i for i in range(2)) - >>> [*x] - [0, 1] - >>> bytes(x) - b'\x00\x01' - >>> [*x] - [0, 1] + the standard collections.defaultdict with a less obscure __repr__ method """ - def __init__(self, iterable, *args, **kwargs): - if hasattr(iterable, '__call__') and not isinstance(iterable, type(self)): - self.active, self._inert = tee(iterable(*args, **kwargs)) - else: - self.active, self._inert = tee(iterable) - def __next__(self): - return next(self.active) - def __iter__(self): - self.active, self._inert = tee(self._inert) - return self.active - def __call__(self, *indices:Iterable[int]): - """ - Access particular indices of the underlying iterator - eg - >>> x = regen(range(3)) - >>> [*x(1)] - [1] - >>> [*x(1,2)] - [1, 2] - >>> [*x(1,2,3)] - [1, 2] - """ - return type(self)(_regen.choose(self.active, *_regen.flatten(indices))) - def __bool__(self): - """ - Returns True iff. the underlying iterator is non-empty. False otherwise. - """ - tmp, self._inert = tee(self._inert) - try: - next(tmp) - return True - except StopIteration: - return False - def __matmul__(self, other:Iterable): - if hasattr(other, '__iter__'): - return type(self)(product(self, other)) - raise TypeError(f'Matrix-multiplication is not defined for "{_regen.tipo(other, True)}". It must have an "__iter__" or "__index__" method.') - def __len__(self): - return sum(1 for i in self) - def __add__(self, other:Any): - """ - Create a new regenerator whose first elements come from self and remaining element(s) is/come-from other. - If other is not iterable it shall be added as the last element. - If you want to add an iterable as a single element, use self.append - eg - >>> x = regenerator(range(2)) - >>> y = x + 10 - >>> [*y] - [0, 1, 10] - >>> y += x - >>> [*y] - [0, 1, 10, 0, 1] - """ - other = other if hasattr(other, '__iter__') else [other] - return type(self)([*self, *other]) - def __radd__(self, other): - """ - Swap the order of __add__ - """ - other = other if hasattr(other, '__iter__') else [other] - return type(self)(chain(other, self)) - def __mul__(self, value): - """ - Replicate the behaviour of multiplying lists by integers - """ - if hasattr(value, '__int__'): - return type(self)(chain.from_iterable(self for i in range(int(value)))) - raise TypeError(f'Multiplication is not defined for "{_regen.tipo(other, True)}". It must have an "__int__"') - def __rmul__(self, value): - """Commutative multiplication""" - return self.__mul__(value) - def __pow__(self, value): - """ - value-dimensional Cartesian product of self with itself - """ - if hasattr(value, '__int__'): - return type(self)(product(self, repeat=int(value))) - raise TypeError(f'Exponentiation is not defined for {type(other)}. It must have an "__int__" method.') - def scale(self, value): - """ - Multiply every element of self by value. Done in place. - """ - self._inert = (i*value for i in self) - return self - def boost(self, value): - """ - Add value to every element of self. Done in place. - """ - self._inert = (i+value for i in self) - return self - def indices(self, value, shift:int=0): - """ - Similar to a list's index method, except that it returns every index whose element is a match - Works by calling enumerate and selecting at equivalent elements. - :shift: - kwarg for the "enumerate" call. - """ - return type(self)(i for i, e in enumerate(self, shift) if e == value) - def append(self, value): - """ - add "value" as the last element of the array - """ - other = [value] - self._inert = chain(self._inert, other) - return self - def inject(self, value): - """ - If "value" is an iterable: its elements will be added to the end of "self" - Otherwise: it is the same as append - """ - other = value if hasattr(value, '__iter__') else [value] - self._inert = chain(self._inert, *other) - return self -regen = regenerator - -def regurge(iterable:Iterable[Any]) -> Iterable: - """ - Secure your generators by passing them through this function in order to produce a copy. - Your generator will be replaced with a tee object. - If your iterable is not a generator the function shall return a copy.deepcopy of it - """ - if issubclass(type(iterable), (map, filter, generator, _tee)): - iterable, consumable = tee(iterable) - else: - consumable = iterable - return consumable - -class defaultdict(defaultdict): - """""" + @property + def __rack(self): + pairs = (': '.join(map(repr, pair)) for pair in self.items()) + r = ',\n '.join(pairs) + return '{\n ' + r + '\n}' def __repr__(self): - pairs = (': '.join(map(str, pair)) for pair in self.items()) - rack = ',\n '.join(pairs) - return "{ " + rack + "\n}" + return tipo(self) + self.__rack def __str__(self): - return self.__repr__() + return ' '.join(self.__rack.split()) +defaultdict = DDict -class SineWave: - pass \ No newline at end of file