diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4bdbd7c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: Python package + +on: + - push + - pull_request + +jobs: + build: + strategy: + matrix: + platform: [ubuntu-20.04, windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.6 + uses: actions/setup-python@v4 + with: + python-version: '3.6' + - name: Set up Python 3.8 + uses: actions/setup-python@v4 + with: + python-version: '3.8' + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Set up Python 3.12 + uses: actions/setup-python@v4 + with: + python-version: '3.12' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Test with tox + run: python -m tox diff --git a/.gitignore b/.gitignore index 94e2e44..bf4997a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ config.py *.in *.out +*.exe # Created by .ignore support plugin (hsz.mobi) ### Python template @@ -131,4 +132,11 @@ docs/_build/ target/ # Pycharm -venv \ No newline at end of file +venv/ + +*.DS_Store + +# VS Code +.vscode + +.python-version diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..de2c0b1 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,3 @@ +[MASTER] +py-version=3.5 +disable=R0902,R0903,R0913,R0917,R0912 \ No newline at end of file diff --git a/cyaron/__init__.py b/cyaron/__init__.py index c9b81a1..ec5f271 100644 --- a/cyaron/__init__.py +++ b/cyaron/__init__.py @@ -5,17 +5,19 @@ """ from __future__ import absolute_import + +from random import choice, randint, random, randrange, uniform + +#from .visual import visualize +from . import log +from .compare import Compare +from .consts import * +from .graph import Edge, Graph from .io import IO -from .graph import Graph, Edge -from .string import String +from .math import * +from .merger import Merger +from .polygon import Polygon from .sequence import Sequence +from .string import String from .utils import * -from .consts import * from .vector import Vector -from .polygon import Polygon -from .compare import Compare -from .math import * -from .merger import Merger -#from .visual import visualize -from . import log -from random import randint, randrange, uniform, choice, random diff --git a/cyaron/compare.py b/cyaron/compare.py index ad613a0..7363a9b 100644 --- a/cyaron/compare.py +++ b/cyaron/compare.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, print_function -from cyaron import IO, log +from .io import IO +from . import log from cyaron.utils import * from cyaron.consts import * from cyaron.graders import CYaRonGraders @@ -17,7 +18,7 @@ def __init__(self, name, mismatch): self.mismatch = mismatch def __str__(self): - return 'In program: \'{}\'. {}'.format(self.name,self.mismatch) + return "In program: '{}'. {}".format(self.name, self.mismatch) class Compare: @@ -37,7 +38,7 @@ def __process_file(file): file.output_file.seek(0) return file.output_filename, file.output_file.read() else: - with open(file, "r", newline='\n') as f: + with open(file, "r", newline="\n") as f: return file, f.read() @staticmethod @@ -50,26 +51,43 @@ def __normal_max_workers(workers): @classmethod def output(cls, *files, **kwargs): - kwargs = unpack_kwargs('output', kwargs, ('std', ('grader', DEFAULT_GRADER), ('max_workers', -1), - ('job_pool', None), ('stop_on_incorrect', None))) - std = kwargs['std'] - grader = kwargs['grader'] - max_workers = kwargs['max_workers'] - job_pool = kwargs['job_pool'] - if kwargs['stop_on_incorrect'] is not None: + kwargs = unpack_kwargs( + "output", + kwargs, + ( + "std", + ("grader", DEFAULT_GRADER), + ("max_workers", -1), + ("job_pool", None), + ("stop_on_incorrect", None), + ), + ) + std = kwargs["std"] + grader = kwargs["grader"] + max_workers = kwargs["max_workers"] + job_pool = kwargs["job_pool"] + if kwargs["stop_on_incorrect"] is not None: log.warn("parameter stop_on_incorrect is deprecated and has no effect.") if (max_workers is None or max_workers >= 0) and job_pool is None: max_workers = cls.__normal_max_workers(max_workers) try: from concurrent.futures import ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=max_workers) as job_pool: - return cls.output(*files, std=std, grader=grader, max_workers=max_workers, job_pool=job_pool) + return cls.output( + *files, + std=std, + grader=grader, + max_workers=max_workers, + job_pool=job_pool + ) except ImportError: pass def get_std(): return cls.__process_file(std)[1] + if job_pool is not None: std = job_pool.submit(get_std).result() else: @@ -86,61 +104,118 @@ def do(file): @classmethod def program(cls, *programs, **kwargs): - kwargs = unpack_kwargs('program', kwargs, ('input', ('std', None), ('std_program', None), - ('grader', DEFAULT_GRADER), ('max_workers', -1), - ('job_pool', None), ('stop_on_incorrect', None))) - input = kwargs['input'] - std = kwargs['std'] - std_program = kwargs['std_program'] - grader = kwargs['grader'] - max_workers = kwargs['max_workers'] - job_pool = kwargs['job_pool'] - if kwargs['stop_on_incorrect'] is not None: + kwargs = unpack_kwargs( + "program", + kwargs, + ( + "input", + ("std", None), + ("std_program", None), + ("grader", DEFAULT_GRADER), + ("max_workers", -1), + ("job_pool", None), + ("stop_on_incorrect", None), + ), + ) + input = kwargs["input"] + std = kwargs["std"] + std_program = kwargs["std_program"] + grader = kwargs["grader"] + max_workers = kwargs["max_workers"] + job_pool = kwargs["job_pool"] + if kwargs["stop_on_incorrect"] is not None: log.warn("parameter stop_on_incorrect is deprecated and has no effect.") if (max_workers is None or max_workers >= 0) and job_pool is None: max_workers = cls.__normal_max_workers(max_workers) try: from concurrent.futures import ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=max_workers) as job_pool: - return cls.program(*programs, input=input, std=std, std_program=std_program, grader=grader, max_workers=max_workers, job_pool=job_pool) + return cls.program( + *programs, + input=input, + std=std, + std_program=std_program, + grader=grader, + max_workers=max_workers, + job_pool=job_pool + ) except ImportError: pass if not isinstance(input, IO): - raise TypeError("expect {}, got {}".format(type(IO).__name__, type(input).__name__)) + raise TypeError( + "expect {}, got {}".format(type(IO).__name__, type(input).__name__) + ) input.flush_buffer() input.input_file.seek(0) if std_program is not None: + def get_std(): - with open(os.dup(input.input_file.fileno()), 'r', newline='\n') as input_file: - content = make_unicode(subprocess.check_output(std_program, shell=(not list_like(std_program)), stdin=input.input_file, universal_newlines=True)) + with open( + os.dup(input.input_file.fileno()), "r", newline="\n" + ) as input_file: + content = make_unicode( + subprocess.check_output( + std_program, + shell=(not list_like(std_program)), + stdin=input.input_file, + universal_newlines=True, + ) + ) input_file.seek(0) return content + if job_pool is not None: std = job_pool.submit(get_std).result() else: std = get_std() elif std is not None: + def get_std(): return cls.__process_file(std)[1] + if job_pool is not None: std = job_pool.submit(get_std).result() else: std = get_std() else: - raise TypeError('program() missing 1 required non-None keyword-only argument: \'std\' or \'std_program\'') + raise TypeError( + "program() missing 1 required non-None keyword-only argument: 'std' or 'std_program'" + ) def do(program_name): timeout = None - if list_like(program_name) and len(program_name) == 2 and int_like(program_name[-1]): + if ( + list_like(program_name) + and len(program_name) == 2 + and int_like(program_name[-1]) + ): program_name, timeout = program_name - with open(os.dup(input.input_file.fileno()), 'r', newline='\n') as input_file: + with open( + os.dup(input.input_file.fileno()), "r", newline="\n" + ) as input_file: if timeout is None: - content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True)) + content = make_unicode( + subprocess.check_output( + program_name, + shell=(not list_like(program_name)), + stdin=input_file, + universal_newlines=True, + ) + ) else: - content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True, timeout=timeout)) + content = make_unicode( + subprocess.check_output( + program_name, + shell=(not list_like(program_name)), + stdin=input_file, + universal_newlines=True, + timeout=timeout, + ) + ) input_file.seek(0) cls.__compare_two(program_name, content, std, grader) diff --git a/cyaron/graph.py b/cyaron/graph.py index 34e89fe..5748acc 100644 --- a/cyaron/graph.py +++ b/cyaron/graph.py @@ -1,9 +1,14 @@ from .utils import * import random +from typing import TypeVar, Callable + + +__all__ = ["Edge", "Graph"] class Edge: """Class Edge: A class of the edge in the graph""" + def __init__(self, u, v, w): """__init__(self, u, v, w) -> None Initialize a edge. @@ -26,11 +31,13 @@ def unweighted_edge(edge): """unweighted_edge(edge) -> str Return a string to output the edge without weight. The string contains the start vertex, end vertex(u,v) and splits with space. """ - return '%d %d'%(edge.start,edge.end) + return '%d %d' % (edge.start, edge.end) + class Graph: """Class Graph: A class of the graph """ + def __init__(self, point_count, directed=False): """__init__(self, point_count) -> None Initialize a graph. @@ -39,6 +46,26 @@ def __init__(self, point_count, directed=False): """ self.directed = directed self.edges = [[] for i in range(point_count + 1)] + + def edge_count(self): + """edge_count(self) -> int + Return the count of the edges in the graph. + """ + cnt = sum(len(node) for node in self.edges) + if not self.directed: + cnt //= 2 + return cnt + + def to_matrix(self, **kwargs): + """to_matrix(self, **kwargs) -> GraphMatrix + Convert the graph to adjacency matrix. + **kwargs(Keyword args): + int default = -1 -> the default value when the edge does not exist. + Any merge(Any, Edge) + = lambda val, edge: edge.weight + -> the mapping from the old values in matrix and the edges to the new values in matrix. + """ + return GraphMatrix(self, **kwargs) def to_str(self, **kwargs): """to_str(self, **kwargs) -> str @@ -50,7 +77,7 @@ def to_str(self, **kwargs): = lambda table: random.sample(table, k=len(table)) -> the random function which shuffles the vertex sequence. Note that this function will actually be passed in a `range`! - list[Edge] edge_shuffler(list[int]) + list[Edge] edge_shuffler(list[Edge]) -> a random function. the default is to shuffle the edge sequence, also, if the graph is undirected, it will swap `u` and `v` randomly. """ @@ -163,9 +190,10 @@ def tree(point_count, chain=0, flower=0, **kwargs): if not list_like(weight_limit): weight_limit = (1, weight_limit) weight_gen = kwargs.get( - "weight_gen", lambda: random.randint( - weight_limit[0], weight_limit[1])) - father_gen = kwargs.get("father_gen", lambda cur: random.randrange(1, cur)) + "weight_gen", + lambda: random.randint(weight_limit[0], weight_limit[1])) + father_gen = kwargs.get("father_gen", + lambda cur: random.randrange(1, cur)) if not 0 <= chain <= 1 or not 0 <= flower <= 1: raise Exception("chain and flower must be between 0 and 1") @@ -212,33 +240,35 @@ def binary_tree(point_count, left=0, right=0, **kwargs): if not list_like(weight_limit): weight_limit = (1, weight_limit) weight_gen = kwargs.get( - "weight_gen", lambda: random.randint( - weight_limit[0], weight_limit[1])) + "weight_gen", + lambda: random.randint(weight_limit[0], weight_limit[1])) if not 0 <= left <= 1 or not 0 <= right <= 1: raise Exception("left and right must be between 0 and 1") if left + right > 1: raise Exception("left plus right must be smaller than 1") - - can_left=[1] - can_right=[1] + + can_left = [1] + can_right = [1] graph = Graph(point_count, directed) for i in range(2, point_count + 1): edge_pos = random.random() node = 0 # Left - if edge_pos < left or left + right < edge_pos <= (1.0 - left - right) / 2: - point_index = random.randint(0,len(can_left)-1) + if edge_pos < left or left + right < edge_pos <= (1.0 - left - + right) / 2: + point_index = random.randint(0, len(can_left) - 1) node = can_left[point_index] - del_last_node = can_left.pop() # Save a copy of the last element + del_last_node = can_left.pop( + ) # Save a copy of the last element if point_index < len(can_left): # If the chosen element isn't the last one, # Copy the last one to the position of the chosen one can_left[point_index] = del_last_node # Right else: - # elif left <= edge_pos <= left + right or (1.0 - left - right) / 2 < edge_pos < 1: - point_index = random.randint(0,len(can_right)-1) + # elif left <= edge_pos <= left + right or (1.0 - left - right) / 2 < edge_pos < 1: + point_index = random.randint(0, len(can_right) - 1) node = can_right[point_index] del_last_node = can_right.pop() if point_index < len(can_right): @@ -268,12 +298,17 @@ def graph(point_count, edge_count, **kwargs): directed = kwargs.get("directed", False) self_loop = kwargs.get("self_loop", True) repeated_edges = kwargs.get("repeated_edges", True) + if not repeated_edges: + max_edge = Graph._calc_max_edge(point_count, directed, self_loop) + if edge_count > max_edge: + raise Exception("the number of edges of this kind of graph which has %d vertexes must be less than or equal to %d." % (point_count, max_edge)) + weight_limit = kwargs.get("weight_limit", (1, 1)) if not list_like(weight_limit): weight_limit = (1, weight_limit) weight_gen = kwargs.get( - "weight_gen", lambda: random.randint( - weight_limit[0], weight_limit[1])) + "weight_gen", + lambda: random.randint(weight_limit[0], weight_limit[1])) graph = Graph(point_count, directed) used_edges = set() i = 0 @@ -281,7 +316,8 @@ def graph(point_count, edge_count, **kwargs): u = random.randint(1, point_count) v = random.randint(1, point_count) - if (not self_loop and u == v) or (not repeated_edges and (u, v) in used_edges): + if (not self_loop and u == v) or (not repeated_edges and + (u, v) in used_edges): # Then we generate a new pair of nodes continue @@ -298,7 +334,7 @@ def graph(point_count, edge_count, **kwargs): @staticmethod def DAG(point_count, edge_count, **kwargs): """DAG(point_count, edge_count, **kwargs) -> Graph - Factory method. Return a graph with point_count vertexes and edge_count edges. + Factory method. Return a directed connected graph with point_count vertexes and edge_count edges. int point_count -> the count of vertexes int edge_count -> the count of edges **kwargs(Keyword args): @@ -312,30 +348,38 @@ def DAG(point_count, edge_count, **kwargs): -> the generator of the weights. It should return the weight. The default way is to use the random.randint() """ if edge_count < point_count - 1: - raise Exception("the number of edges of connected graph must more than the number of nodes - 1") + raise Exception( + "the number of edges of connected graph must more than the number of nodes - 1" + ) - self_loop = kwargs.get("self_loop", False) # DAG default has no loop + self_loop = kwargs.get("self_loop", False) # DAG default has no loop repeated_edges = kwargs.get("repeated_edges", True) loop = kwargs.get("loop", False) + if not repeated_edges: + max_edge = Graph._calc_max_edge(point_count, not loop, self_loop) + if edge_count > max_edge: + raise Exception("the number of edges of this kind of graph which has %d vertexes must be less than or equal to %d." % (point_count, max_edge)) + weight_limit = kwargs.get("weight_limit", (1, 1)) if not list_like(weight_limit): weight_limit = (1, weight_limit) weight_gen = kwargs.get( - "weight_gen", lambda: random.randint( - weight_limit[0], weight_limit[1])) - + "weight_gen", + lambda: random.randint(weight_limit[0], weight_limit[1])) + used_edges = set() - edge_buf = list(Graph.tree(point_count, weight_gen=weight_gen).iterate_edges()) + edge_buf = list( + Graph.tree(point_count, weight_gen=weight_gen).iterate_edges()) graph = Graph(point_count, directed=True) for edge in edge_buf: if loop and random.randint(1, 2) == 1: edge.start, edge.end = edge.end, edge.start graph.add_edge(edge.start, edge.end, weight=edge.weight) - + if not repeated_edges: used_edges.add((edge.start, edge.end)) - + i = point_count - 1 while i < edge_count: u = random.randint(1, point_count) @@ -344,7 +388,8 @@ def DAG(point_count, edge_count, **kwargs): if not loop and u > v: u, v = v, u - if (not self_loop and u == v) or (not repeated_edges and (u, v) in used_edges): + if (not self_loop and u == v) or (not repeated_edges and + (u, v) in used_edges): # Then we generate a new pair of nodes continue @@ -360,7 +405,7 @@ def DAG(point_count, edge_count, **kwargs): @staticmethod def UDAG(point_count, edge_count, **kwargs): """UDAG(point_count, edge_count, **kwargs) -> Graph - Factory method. Return a graph with point_count vertexes and edge_count edges. + Factory method. Return a undirected connected graph with point_count vertexes and edge_count edges. int point_count -> the count of vertexes int edge_count -> the count of edges **kwargs(Keyword args): @@ -372,18 +417,25 @@ def UDAG(point_count, edge_count, **kwargs): = lambda: random.randint(weight_limit[0], weight_limit[1]) -> the generator of the weights. It should return the weight. The default way is to use the random.randint() """ - if edge_count < point_count - 1: - raise Exception("the number of edges of connected graph must more than the number of nodes - 1") + if edge_count < point_count - 1: + raise Exception( + "the number of edges of connected graph must more than the number of nodes - 1" + ) self_loop = kwargs.get("self_loop", True) repeated_edges = kwargs.get("repeated_edges", True) + if not repeated_edges: + max_edge = Graph._calc_max_edge(point_count, False, self_loop) + if edge_count > max_edge: + raise Exception("the number of edges of this kind of graph which has %d vertexes must be less than or equal to %d." % (point_count, max_edge)) + weight_limit = kwargs.get("weight_limit", (1, 1)) if not list_like(weight_limit): weight_limit = (1, weight_limit) weight_gen = kwargs.get( - "weight_gen", lambda: random.randint( - weight_limit[0], weight_limit[1])) - + "weight_gen", + lambda: random.randint(weight_limit[0], weight_limit[1])) + used_edges = set() graph = Graph.tree(point_count, weight_gen=weight_gen, directed=False) @@ -391,13 +443,14 @@ def UDAG(point_count, edge_count, **kwargs): if not repeated_edges: used_edges.add((edge.start, edge.end)) used_edges.add((edge.end, edge.start)) - + i = point_count - 1 while i < edge_count: u = random.randint(1, point_count) v = random.randint(1, point_count) - if (not self_loop and u == v) or (not repeated_edges and (u, v) in used_edges): + if (not self_loop and u == v) or (not repeated_edges and + (u, v) in used_edges): # Then we generate a new pair of nodes continue @@ -410,6 +463,18 @@ def UDAG(point_count, edge_count, **kwargs): i += 1 return graph + + @staticmethod + def connected(point_count, edge_count, directed=False, **kwargs): + """connected(point_count, edge_count, **kwargs) -> Graph + Factory method. Return a connected graph with point_count vertexes + int point_count -> the count of vertexes + bool directed -> whether the graph is directed + """ + if directed: + return Graph.DAG(point_count, edge_count, **kwargs) + else: + return Graph.UDAG(point_count, edge_count, **kwargs) @staticmethod def hack_spfa(point_count, **kwargs): @@ -431,8 +496,8 @@ def hack_spfa(point_count, **kwargs): if not list_like(weight_limit): weight_limit = (1, weight_limit) weight_gen = kwargs.get( - "weight_gen", lambda: random.randint( - weight_limit[0], weight_limit[1])) + "weight_gen", + lambda: random.randint(weight_limit[0], weight_limit[1])) point_to_skip = point_count + 3 graph = Graph(point_count, directed) @@ -442,15 +507,18 @@ def hack_spfa(point_count, **kwargs): for i in range(1, half): (x, y) = (i, i + 1) - graph.add_edge(x + (x >= point_to_skip), y + - (y >= point_to_skip), weight=weight_gen()) + graph.add_edge(x + (x >= point_to_skip), + y + (y >= point_to_skip), + weight=weight_gen()) (x, y) = (i + half, i + half + 1) - graph.add_edge(x + (x >= point_to_skip), y + - (y >= point_to_skip), weight=weight_gen()) + graph.add_edge(x + (x >= point_to_skip), + y + (y >= point_to_skip), + weight=weight_gen()) for i in range(1, half + 1): (x, y) = (i, i + half) - graph.add_edge(x + (x >= point_to_skip), y + - (y >= point_to_skip), weight=weight_gen()) + graph.add_edge(x + (x >= point_to_skip), + y + (y >= point_to_skip), + weight=weight_gen()) for i in range(extraedg): u = random.randint(1, point_count) @@ -458,3 +526,48 @@ def hack_spfa(point_count, **kwargs): graph.add_edge(u, v, weight=weight_gen()) return graph + + @staticmethod + def _calc_max_edge(point_count, directed, self_loop): + max_edge = point_count * (point_count - 1) + if not directed: + max_edge //= 2 + if self_loop: + max_edge += point_count + return max_edge + + +class GraphMatrix: + """ + Class GraphMatrix: A class of the graph represented by adjacency matrix. + + *Deprecation warning: This class may be removed after a generic matrix class is implemented in the project.* + """ + + T = TypeVar('T') + + def __init__(self, + graph: Graph, + default: T = -1, + merge: Callable[[T, Edge], + T] = lambda val, edge: edge.weight): + """ + Args: + graph: the graph to convert, + default: the default value when the edge does not exist, + merge: the mapping from the old values in matrix and the edges to the new values in matrix. + """ + n = len(graph.edges) + self.matrix = [[default for _ in range(n)] for _ in range(n)] + for edge in graph.iterate_edges(): + self.matrix[edge.start][edge.end] = merge( + self.matrix[edge.start][edge.end], edge) + + def __str__(self): + return '\n'.join([' '.join(map(str, row[1:])) for row in self.matrix[1:]]) + + def __call__(self, u: int, v: int): + return self.matrix[u][v] + + def __iter__(self): + return self.matrix.__iter__() diff --git a/cyaron/io.py b/cyaron/io.py index 1880143..1ddb9ee 100644 --- a/cyaron/io.py +++ b/cyaron/io.py @@ -1,88 +1,132 @@ +""" +A module that provides a class IO to process the input and output files. +Classes: + IO: IO tool class. It will process the input and output files. +""" from __future__ import absolute_import -from .utils import * -from . import log -from io import open, IOBase -import subprocess -import tempfile import os import re +import subprocess +import tempfile +from typing import Union, overload, Optional +from io import IOBase +from . import log +from .utils import list_like, make_unicode + + +class IO: + """IO tool class. It will process the input and output files.""" + + @overload + def __init__(self, + input_file: Optional[Union[IOBase, str, int]] = None, + output_file: Optional[Union[IOBase, str, int]] = None, + data_id: Optional[int] = None, + disable_output: bool = False): + ... + @overload + def __init__(self, + data_id: Optional[int] = None, + file_prefix: Optional[str] = None, + input_suffix: str = '.in', + output_suffix: str = '.out', + disable_output: bool = False): + ... -class IO(object): - """Class IO: IO tool class. It will process the input and output files.""" - def __init__(self, input_file=None, output_file=None, data_id=None, file_prefix=None, input_suffix='.in', output_suffix='.out', disable_output=False): - """__init__(self, input_file=None, output_file=None, data_id=None, file_prefix=None, input_suffix='.in', output_suffix='.out', disable_output=False) -> None - input_file, output_file overload: - None -> make a temp file (if file_prefix is None) - file object -> treat the file-like object as in/output file - int -> open file by file descriptor - str -> a filename or filename template like 'awd{}.in'. ``{}`` will be replaced by ``data_id`` - int data_id -> the id of the data. if it's None, the file names will not contain the id. - legacy argumants: - str file_prefix -> the prefix for the input and output files - str input_suffix = ".in" -> the suffix of the input file - str output_suffix = ".out" -> the suffix of the output file - disable_output -> bool, set to True to disable output - Examples: - IO("a","b") -> create input file "a" and output file "b" - IO("a.in","b.out") -> create input file "a.in" and output file "b.out" - IO(file_prefix="data") -> create input file "data.in" and output file "data.out" - IO(file_prefix="data",data_id=1) -> create input file "data1.in" and output file "data1.out" - IO(file_prefix="data",input_suffix=".input") -> create input file "data.input" and output file "data.out" - IO(file_prefix="data",output_suffix=".output") -> create input file "data.in" and output file "data.output" - IO(file_prefix="data",data_id=2,input_suffix=".input") -> create input file "data2.input" and output file "data2.out" - IO("data{}.in","data{}.out",data_id=2) -> create input file "data2.in" and output file "data2.out" - IO(open('data.in', 'w+'), open('data.out', 'w+')) -> input file "data.in" and output file "data.out" + def __init__( # type: ignore + self, + input_file: Optional[Union[IOBase, str, int]] = None, + output_file: Optional[Union[IOBase, str, int]] = None, + data_id: Optional[int] = None, + file_prefix: Optional[str] = None, + input_suffix: str = '.in', + output_suffix: str = '.out', + disable_output: bool = False): + """ + Args: + input_file (optional): input file object or filename or file descriptor. + If it's None, make a temp file. Defaults to None. + output_file (optional): input file object or filename or file descriptor. + If it's None, make a temp file. Defaults to None. + data_id (optional): the id of the data. It will be add after + `input_file` and `output_file` when they are str. + If it's None, the file names will not contain the id. Defaults to None. + file_prefix (optional): the prefix for the input and output files. Defaults to None. + input_suffix (optional): the suffix of the input file. Defaults to '.in'. + output_suffix (optional): the suffix of the output file. Defaults to '.out'. + disable_output (optional): set to True to disable output file. Defaults to False. + Examples: + >>> IO("a","b") + # create input file "a" and output file "b" + >>> IO("a.in","b.out") + # create input file "a.in" and output file "b.out" + >>> IO(file_prefix="data") + # create input file "data.in" and output file "data.out" + >>> IO(file_prefix="data",data_id=1) + # create input file "data1.in" and output file "data1.out" + >>> IO(file_prefix="data",input_suffix=".input") + # create input file "data.input" and output file "data.out" + >>> IO(file_prefix="data",output_suffix=".output") + # create input file "data.in" and output file "data.output" + >>> IO(file_prefix="data",data_id=2,input_suffix=".input") + # create input file "data2.input" and output file "data2.out" + >>> IO("data{}.in","data{}.out",data_id=2) + # create input file "data2.in" and output file "data2.out" + >>> IO(open('data.in', 'w+'), open('data.out', 'w+')) + # input file "data.in" and output file "data.out" """ if file_prefix is not None: # legacy mode - input_file = '{}{{}}{}'.format(self.__escape_format(file_prefix), self.__escape_format(input_suffix)) - output_file = '{}{{}}{}'.format(self.__escape_format(file_prefix), self.__escape_format(output_suffix)) + input_file = '{}{{}}{}'.format(self.__escape_format(file_prefix), + self.__escape_format(input_suffix)) + output_file = '{}{{}}{}'.format( + self.__escape_format(file_prefix), + self.__escape_format(output_suffix)) self.input_filename, self.output_filename = None, None self.__input_temp, self.__output_temp = False, False - self.__init_file(input_file, data_id, 'i') + self.__init_file(input_file, data_id, "i") if not disable_output: - self.__init_file(output_file, data_id, 'o') + self.__init_file(output_file, data_id, "o") else: self.output_file = None self.__closed = False self.is_first_char = {} - def __init_file(self, f, data_id, file_type): - try: - is_file = isinstance(f, file) - except NameError: - is_file = False - if isinstance(f, IOBase) or is_file: + def __init_file(self, f: Union[IOBase, str, int, None], + data_id: Union[int, None], file_type: str): + if isinstance(f, IOBase): # consider ``f`` as a file object - if file_type == 'i': + if file_type == "i": self.input_file = f else: self.output_file = f elif isinstance(f, int): # consider ``f`` as a file descor - self.__init_file(open(f, 'w+', newline='\n'), data_id, file_type) + self.__init_file(open(f, 'w+', encoding="utf-8", newline='\n'), + data_id, file_type) elif f is None: # consider wanna temp file fd, self.input_filename = tempfile.mkstemp() self.__init_file(fd, data_id, file_type) - if file_type == 'i': + if file_type == "i": self.__input_temp = True else: self.__output_temp = True else: # consider ``f`` as filename template - filename = f.format(data_id or '') - if file_type == 'i': + filename = f.format(data_id or "") + if file_type == "i": self.input_filename = filename - log.debug("Processing %s" % self.input_filename) else: self.output_filename = filename - self.__init_file(open(filename, 'w+', newline='\n'), data_id, file_type) + self.__init_file( + open(filename, 'w+', newline='\n', encoding='utf-8'), data_id, + file_type) - def __escape_format(self, st): + def __escape_format(self, st: str): """replace "{}" to "{{}}" """ - return re.sub(r'\{', '{{', re.sub(r'\}', '}}', st)) + return re.sub(r"\{", "{{", re.sub(r"\}", "}}", st)) def __del_files(self): """delete files""" @@ -99,7 +143,8 @@ def close(self): deleted = False try: # on posix, one can remove a file while it's opend by a process - # the file then will be not visable to others, but process still have the file descriptor + # the file then will be not visable to others, + # but process still have the file descriptor # it is recommand to remove temp file before close it on posix to avoid race # on nt, it will just fail and raise OSError so that after closing remove it again self.__del_files() @@ -123,12 +168,10 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.close() - def __write(self, file, *args, **kwargs): - """__write(self, file, *args, **kwargs) -> None - Write every element in *args into file. If the element isn't "\n", insert a space. It will convert every element into str - file file -> the file object to write - **kwargs: - str separator = " " -> a string used to separate every element + def __write(self, file: IOBase, *args, **kwargs): + """ + Write every element in *args into file. If the element isn't "\n", insert `separator`. + It will convert every element into str. """ separator = kwargs.get("separator", " ") for arg in args: @@ -143,53 +186,88 @@ def __write(self, file, *args, **kwargs): self.is_first_char[file] = True def input_write(self, *args, **kwargs): - """input_write(self, *args, **kwargs) -> None - Write every element in *args into the input file. Splits with spaces. It will convert every element into string - **kwargs: - str separator = " " -> a string used to separate every element + """ + Write every element in *args into the input file. Splits with `separator`. + It will convert every element into str. + Args: + *args: the elements to write + separator: a string used to separate every element. Defaults to " ". """ self.__write(self.input_file, *args, **kwargs) def input_writeln(self, *args, **kwargs): - """input_writeln(self, *args, **kwargs) -> None - Write every element in *args into the input file and turn to a new line. Splits with spaces. It will convert every element into string - **kwargs: - str separator = " " -> a string used to separate every element + """ + Write every element in *args into the input file and turn to a new line + Splits with `separator`. + It will convert every element into str. + Args: + *args: the elements to write + separator: a string used to separate every element. Defaults to " ". """ args = list(args) args.append("\n") self.input_write(*args, **kwargs) - def output_gen(self, shell_cmd): - """output_gen(self, shell_cmd) -> None - Run the command shell_cmd(usually the std programme) and send it the input file as stdin. Write its output to the output file. - str shell_cmd -> the command to run, usually the std programme + def output_gen(self, shell_cmd, time_limit=None): """ + Run the command `shell_cmd` (usually the std program) and send it the input file as stdin. + Write its output to the output file. + Args: + shell_cmd: the command to run, usually the std program. + time_limit: the time limit (seconds) of the command to run. + None means infinity. Defaults to None. + """ + if self.output_file is None: + raise ValueError("Output file is disabled") self.flush_buffer() origin_pos = self.input_file.tell() self.input_file.seek(0) - subprocess.check_call(shell_cmd, shell=True, stdin=self.input_file, stdout=self.output_file, universal_newlines=True) + if time_limit is not None: + subprocess.check_call( + shell_cmd, + shell=True, + timeout=time_limit, + stdin=self.input_file.fileno(), + stdout=self.output_file.fileno(), + universal_newlines=True, + ) + else: + subprocess.check_call( + shell_cmd, + shell=True, + stdin=self.input_file.fileno(), + stdout=self.output_file.fileno(), + universal_newlines=True, + ) self.input_file.seek(origin_pos) log.debug(self.output_filename, " done") def output_write(self, *args, **kwargs): - """output_write(self, *args, **kwargs) -> None - Write every element in *args into the output file. Splits with spaces. It will convert every element into string - **kwargs: - str separator = " " -> a string used to separate every element """ + Write every element in *args into the output file. Splits with `separator`. + It will convert every element into str. + Args: + *args: the elements to write + separator: a string used to separate every element. Defaults to " ". + """ + if self.output_file is None: + raise ValueError("Output file is disabled") self.__write(self.output_file, *args, **kwargs) def output_writeln(self, *args, **kwargs): - """output_writeln(self, *args, **kwargs) -> None - Write every element in *args into the output file and turn to a new line. Splits with spaces. It will convert every element into string - **kwargs: - str separator = " " -> a string used to separate every element + """ + Write every element in *args into the output file and turn to a new line. + Splits with `separator`. + It will convert every element into str. + Args: + *args: the elements to write + separator: a string used to separate every element. Defaults to " ". """ args = list(args) args.append("\n") self.output_write(*args, **kwargs) def flush_buffer(self): + """Flush the input file""" self.input_file.flush() diff --git a/cyaron/math.py b/cyaron/math.py index eb7379d..dce3102 100644 --- a/cyaron/math.py +++ b/cyaron/math.py @@ -1,201 +1,304 @@ -#coding=utf8 -''' +""" +This is a module that includes some useful math functions. + +Functions: + factorial(n): The factorial of n + is_perm(a,b): Check if number a and b share the same digits + is_palindromic(n): Check if n is palindromic(i.e. The number does not change if you reverse it) + is_pandigital(n,s=9): Check if number n is made from sequence 1 to s + d(n): Calculate the sum of proper divisors for n + pal_list(k): Create a list of all palindromic numbers with k digits + sof_digits(n): Sum of factorial's digits + fibonacci(n): Find the nth Fibonacci number + sos_digits(n): Sum of squares of digits + pow_digits(n,e): Sum of the digits to a power e + is_prime(n): Check n for prime + miller_rabin(n): Miller-Rabin primality test + factor(n): Factor a number into primes and frequency + perm(n,s): Find the nth pemutation of string s + binomial(n,k): Calculate C(n,k) + catalan_number(n): Calculate the nth Catalan number + prime_sieve(n): Return a list of prime numbers from 2 to a prime < n + exgcd(a,b): Bézout coefficients. Returns (u, v, gcd(a,b)) + mod_inverse(a,b): returns u of exgcd(a,b) + phi(x): The PHI function of x + miu(x): The MIU function of x + dec2base(n,base): Number base conversion + n2words(num,join=True): Number to words + forked from https://blog.dreamshire.com/common-functions-routines-project-euler/ -''' +""" + from __future__ import absolute_import -from math import sqrt, ceil, gcd -from functools import reduce +from math import sqrt, factorial import random import itertools - +from typing import Union, Tuple, List fact = (1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880) -def help(): #Give help informations - help_txt=""" -Welcome to CYaRon/math.py help! -Functions are: -| factorial(n) - The factorial of n -| is_perm(a,b) - Check if number a and b share the same digits -| is_palindromic(n) - Check if n is palindromic(i.e. The number does not change if you reverse it) -| is_pandigital(n,s=9) - Check if number n is made from sequence 1 to s -| d(n) - Calculate the sum of proper divisors for n -| pal_list(k) - Create a list of all palindromic numbers with k digits -| sof_digits(n) - Sum of factorial's digits -| fibonacci(n) - Find the nth Fibonacci number -| sos_digits(n) - Sum of squares of digits -| pow_digits(n,e) - Sum of the digits to a power e -| is_prime(n) - Check n for prime -| miller_rabin(n) - Miller-Rabin primality test -| factor(n) - Factor a number into primes and frequency -| perm(n,s) - Find the nth pemutation of string s -| binomial(n,k) - Calculate C(n,k) -| catalan_number(n) - Calculate the nth Catalan number -| prime_sieve(n) - Return a list of prime numbers from 2 to a prime < n -| exgcd(a,b) - Bézout coefficients. Returns (u, v, gcd(a,b)) -| mod_inverse(a,b) - returns u of exgcd(a,b) -| phi(x) - The PHI function of x -| miu(x) - The MIU function of x -| dec2base(n,base) - Number base conversion -| n2words(num,join=True) - Number to words - """ - print(help_txt) - -def factorial(n): return reduce(lambda x,y:x*y,range(1,n+1),1) - -def is_perm(a,b): return sorted(str(a))==sorted(str(b)) - -def is_palindromic(n): n=str(n); return n==n[::-1] - -def is_pandigital(n, s=9): n=str(n); return len(n)==s and not '1234567890'[:s].strip(n) - -#--- Calculate the sum of proper divisors for n-------------------------------------------------- -def d(n): + + +def is_perm(a: int, b: int): + """ + Check if two numbers are permutations of each other. + + This function takes two numbers, converts them to strings, sorts the characters + in each string, and then compares the sorted strings to determine if the numbers + are permutations of each other. + Args: + a: The first number to compare. + b: The second number to compare. + Returns: + bool: True if the numbers are permutations of each other, False otherwise. + """ + + return sorted(str(a)) == sorted(str(b)) + + +def is_palindromic(n: Union[int, str]): + """ + Check if a given number or string is palindromic. + + A palindromic number or string is one that reads the same backward as forward. + Parameters: + n: The number or string to check. + Returns: + True if the input is palindromic, False otherwise. + """ + n = str(n) + return n == n[::-1] + + +def is_pandigital(n: Union[int, str], s: int = 9): + """ + Check if a number is pandigital. + + A number is considered pandigital if it contains each digit from 1 to s exactly once. + Parameters: + n: The number to check. + s (optional): The length of the pandigital number. Default is 9. + Returns: + True if the number is pandigital, False otherwise. + """ + + n = str(n) + return len(n) == s and not '1234567890'[:s].strip(n) + + +def d(n: int): + """ + Calculate the sum of the divisors of a given number `n`. + + This function computes the sum of all divisors of `n`, including 1 and `n` itself. + + It iterates from 2 to the square root of `n`, adding both the divisor and its + corresponding quotient to the sum. If `n` is a perfect square, the square root + is subtracted from the sum to correct for double-counting. + Parameters: + n: The number for which to calculate the sum of divisors. + Returns: + The sum of the divisors of `n`. + """ s = 1 t = sqrt(n) - for i in range(2, int(t)+1): - if n % i == 0: s += i + n/i - if t == int(t): s -= t #correct s if t is a perfect square + for i in range(2, int(t) + 1): + if n % i == 0: + s += i + n // i + if t == int(t): + s -= int(t) #correct s if t is a perfect square return s -#--- Create a list of all palindromic numbers with k digits-------------------------------------- -def pal_list(k): + +def pal_list(k: int): + """ + Generate a list of palindromic numbers of length `k`. + Parameters: + k: The length of the palindromic numbers to generate. + Returns: + A list of palindromic numbers of length `k`. + """ if k == 1: return [1, 2, 3, 4, 5, 6, 7, 8, 9] - return [sum([n*(10**i) for i,n in enumerate(([x]+list(ys)+[z]+list(ys)[::-1]+[x]) if k%2 - else ([x]+list(ys)+list(ys)[::-1]+[x]))]) - for x in range(1,10) - for ys in itertools.product(range(10), repeat=int(k/2)-1) - for z in (range(10) if k%2 else (None,))] - + return [ + sum(n * (10**i) + for i, n in enumerate(([x] + list(ys) + [z] + list(ys)[::-1] + + [x]) if k % 2 else ([x] + list(ys) + + list(ys)[::-1] + [x]))) + for x in range(1, 10) + for ys in itertools.product(range(10), repeat=int(k / 2) - 1) + for z in (range(10) if k % 2 else (None, )) + ] + + +def sof_digits(n: int): + """ + Calculate the sum of the factorials of the digits of a given number. + Args: + n: The input number. + Returns: + The sum of the factorials of the digits of the input number. + """ -#--- sum of factorial's digits------------------------------------------------------------------- -def sof_digits(n): - if n==0: return 1 + if n == 0: + return 1 s = 0 while n > 0: s, n = s + fact[n % 10], n // 10 return s - -#--- find the nth Fibonacci number--------------------------------------------------------------- -def fibonacci(n): +def fibonacci(n: int): """ - Find the nth number in the Fibonacci series. Example: - - >>>fibonacci(100) - 354224848179261915075 - - Algorithm & Python source: Copyright (c) 2013 Nayuki Minase - Fast doubling Fibonacci algorithm - http://nayuki.eigenstate.org/page/fast-fibonacci-algorithms + Find the nth number in the Fibonacci series. + Args: + n: The position in the Fibonacci series to retrieve. + Returns: + The nth Fibonacci number. + Raises: + ValueError: If the input is a negative integer. + Example: + >>> fibonacci(100) + 354224848179261915075 + References: + Algorithm & Python source: Copyright (c) 2013 Nayuki Minase + Fast doubling Fibonacci algorithm + http://nayuki.eigenstate.org/page/fast-fibonacci-algorithms """ if n < 0: raise ValueError("Negative arguments not implemented") return _fib(n)[0] -# Returns a tuple (F(n), F(n+1)) -def _fib(n): + +def _fib(n: int) -> Tuple[int, int]: + """Returns a tuple of fibonacci (F(n), F(n+1)).""" if n == 0: return (0, 1) - else: - a, b = _fib(n // 2) - c = a * (2 * b - a) - d = b * b + a * a - if n % 2 == 0: - return (c, d) - else: - return (d, c + d) + a, b = _fib(n // 2) + c = a * (2 * b - a) + e = b * b + a * a + return (c, e) if n % 2 == 0 else (e, c + e) -#--- sum of squares of digits------------------------------------------------------------------- -def sos_digits(n): - s = 0 - while n > 0: - s, n = s + (n % 10)**2, n // 10 - return s +def sos_digits(n: int): + """ + Calculate the sum of squares of the digits of a given number. + Args: + n: The number whose digits' squares are to be summed. + Returns: + The sum of the squares of the digits of the number. + """ + return pow_digits(n, 2) -#--- sum of the digits to a power e------------------------------------------------------------- -def pow_digits(n, e): - s = 0 + +def pow_digits(n: int, e: int): + """ + Calculate the sum of each digit of a number raised to a specified power. + Args: + n: The number whose digits will be processed. + e: The exponent to which each digit will be raised. + Returns: + The sum of each digit of the number raised to the specified power. + """ + s: int = 0 while n > 0: s, n = s + (n % 10)**e, n // 10 return s +def is_prime(n: int): + """ + Check if a number is a prime number. -#--- check n for prime-------------------------------------------------------------------------- -def is_prime(n): - if n <= 1: return False - if n <= 3: return True - if n%2==0 or n%3 == 0: return False + This function uses a trial division method to determine if the number is prime. + Parameters: + n: The number to check for primality. + Returns: + True if the number is prime, False otherwise. + """ + if n <= 1: + return False + if n <= 3: + return True + if n % 2 == 0 or n % 3 == 0: + return False r = int(sqrt(n)) f = 5 while f <= r: - if n%f == 0 or n%(f+2) == 0: return False - f+= 6 + if n % f == 0 or n % (f + 2) == 0: + return False + f += 6 return True - - -#--- Miller-Rabin primality test---------------------------------------------------------------- -def miller_rabin(n): +def miller_rabin(n: int, repeat_time: int = 20): """ - Check n for primalty: Example: - - >miller_rabin(162259276829213363391578010288127) #Mersenne prime #11 - True - - Algorithm & Python source: - http://en.literateprograms.org/Miller-Rabin_primality_test_(Python) + Check if a number is prime using the Miller-Rabin primality test. + Args: + n: The number to be tested for primality. + repeat_time: The number of iterations to perform. Default is 20. + Returns: + True if n is a probable prime, False if n is composite. + Example: + >>> miller_rabin(162259276829213363391578010288127) # Mersenne prime #11 + True + Note: + This function uses a probabilistic algorithm to determine primality. + It performs 20 iterations to reduce the probability of a false positive. + References: + Algorithm & Python source: + http://en.literateprograms.org/Miller-Rabin_primality_test_(Python) """ - d = n - 1 + if (n & 1) == 0 or n < 3: + return n == 2 + if (n % 3) == 0: + return n == 3 + f = n - 1 s = 0 - while d % 2 == 0: - d >>= 1 + while (f & 1) == 0: + f >>= 1 s += 1 - for repeat in range(20): - a = 0 - while a == 0: - a = random.randrange(n) - if not miller_rabin_pass(a, s, d, n): + for _ in range(repeat_time): + a = random.randint(2, n - 1) + if not _miller_rabin_pass(a, s, f, n): return False return True -def miller_rabin_pass(a, s, d, n): - a_to_power = pow(a, d, n) + +def _miller_rabin_pass(a: int, s: int, f: int, n: int): + a_to_power = pow(a, f, n) if a_to_power == 1: return True - for i in range(s-1): + for _ in range(s - 1): if a_to_power == n - 1: return True a_to_power = (a_to_power * a_to_power) % n return a_to_power == n - 1 - -#--- factor a number into primes and frequency---------------------------------------------------- -def factor(n): +def factor(n: int) -> List[Tuple[int, int]]: """ - find the prime factors of n along with their frequencies. Example: - + Find the prime factors of a given number along with their frequencies. + Args: + n: The number to factorize. + Returns: + A list of tuples where each tuple contains a prime factor and its frequency. + Example: >>> factor(786456) - [(2,3), (3,3), (11,1), (331,1)] - + [(2, 3), (3, 3), (11, 1), (331, 1)] + Note: + This function uses a specific sequence of prime gaps to optimize the factorization process. Source: Project Euler forums for problem #3 """ - f, factors, prime_gaps = 1, [], [2, 4, 2, 4, 6, 2, 6, 4] + f = 1 + factors = [] + prime_gaps = [2, 4, 2, 4, 6, 2, 6, 4] if n < 1: return [] while True: for gap in ([1, 1, 2, 2, 4] if f < 11 else prime_gaps): f += gap if f * f > n: # If f > sqrt(n) - if n == 1: - return factors - else: - return factors + [(n, 1)] + return factors + ([] if n == 1 else [(n, 1)]) if not n % f: e = 1 n //= f @@ -205,136 +308,206 @@ def factor(n): factors.append((f, e)) -#--- generate permutations----------------------------------------------------------------------- -def perm(n, s): +def perm(n: int, s: str) -> str: """ - requires function factorial() - Find the nth permutation of the string s. Example: - - >>>perm(30, 'abcde') - bcade + Find the nth permutation of the string s. + Parameters: + n: The permutation index (0-based). + s: The string for which the permutation is to be found. + Returns: + The nth permutation of the string s. + Example: + >>> perm(30, 'abcde') + 'bcade' """ - if len(s)==1: return s - q, r = divmod(n, factorial(len(s)-1)) - return s[q] + perm(r, s[:q] + s[q+1:]) - + if len(s) == 1: + return s + q, r = divmod(n, factorial(len(s) - 1)) + return s[q] + perm(r, s[:q] + s[q + 1:]) - -#--- binomial coefficients----------------------------------------------------------------------- -def binomial(n, k): +def binomial(n: int, k: int): """ - Calculate C(n,k), the number of ways can k be chosen from n. Example: - - >>>binomial(30,12) - 86493225 + Calculate C(n, k), the number of ways to choose k elements from a set of n elements. + + This function computes the binomial coefficient, which is the number of ways to choose + k elements from a set of n elements without regard to the order of selection. + Parameters: + n: The total number of elements. + k: The number of elements to choose. + Returns: + The binomial coefficient C(n, k). + Example: + >>> binomial(30, 12) + 86493225 """ nt = 1 - for t in range(min(k, n-k)): - nt = nt * (n-t) // (t+1) + for t in range(min(k, n - k)): + nt = nt * (n - t) // (t + 1) return nt -#--- catalan number------------------------------------------------------------------------------ -def catalan_number(n): +def catalan_number(n: int): """ - Calculate the nth Catalan number. Example: - - >>>catalan_number(10) - 16796 + Calculate the nth Catalan number. + + The Catalan numbers are a sequence of natural numbers that occur in various counting problems, + often involving recursively defined objects. + Parameters: + n: The index of the Catalan number to calculate. + Returns: + The nth Catalan number. + Example: + >>> catalan_number(10) + 16796 """ nm = dm = 1 - for k in range(2, n+1): - nm, dm = (nm*(n+k), dm*k) - return nm / dm + for k in range(2, n + 1): + nm, dm = (nm * (n + k), dm * k) + return nm // dm - -#--- generate prime numbers---------------------------------------------------------------------- def prime_sieve(n): """ - Return a list of prime numbers from 2 to a prime < n. Very fast (n<10,000,000) in 0.4 sec. - + Return a list of prime numbers from 2 to a prime < n. + Args: + n: The upper limit (exclusive) for generating prime numbers. + Returns: + A list of prime numbers less than n. Example: - >>>prime_sieve(25) - [2, 3, 5, 7, 11, 13, 17, 19, 23] - + >>> prime_sieve(25) + [2, 3, 5, 7, 11, 13, 17, 19, 23] Algorithm & Python source: Robert William Hanks http://stackoverflow.com/questions/17773352/python-sieve-prime-numbers """ - sieve = [True] * (n//2) - for i in range(3,int(n**0.5)+1,2): - if sieve[i//2]: - sieve[i*i//2::i] = [False] * ((n-i*i-1)//(2*i)+1) - return [2] + [2*i+1 for i in range(1,n//2) if sieve[i]] + sieve = [True] * (n // 2) + for i in range(3, int(n**0.5) + 1, 2): + if sieve[i // 2]: + sieve[i * i // 2::i] = [False] * ((n - i * i - 1) // (2 * i) + 1) + return [2] + [2 * i + 1 for i in range(1, n // 2) if sieve[i]] - -#--- bezout coefficients-------------------------------------------------------------------------- -def exgcd(a,b): +def exgcd(a: int, b: int): """ - Bézout coefficients (u,v) of (a,b) as: - - a*u + b*v = gcd(a,b) - - Result is the tuple: (u, v, gcd(a,b)). Examples: - - >>> exgcd(7*3, 15*3) - (-2, 1, 3) - >>> exgcd(24157817, 39088169) #sequential Fibonacci numbers - (-14930352, 9227465, 1) - + Bézout coefficients (u, v) of (a, b) as: + a * u + b * v = gcd(a, b) + + Result is the tuple: (u, v, gcd(a, b)). + Parameters: + a: First integer. + b: Second integer. + Returns: + A tuple containing the Bézout coefficients (u, v) and gcd(a, b). + Examples: + >>> exgcd(7*3, 15*3) + (-2, 1, 3) + >>> exgcd(24157817, 39088169) # sequential Fibonacci numbers + (-14930352, 9227465, 1) Algorithm source: Pierre L. Douillet http://www.douillet.info/~douillet/working_papers/bezout/node2.html """ - u, v, s, t = 1, 0, 0, 1 - while b !=0: - q, r = divmod(a,b) + u, v, s, t = 1, 0, 0, 1 + while b != 0: + q, r = divmod(a, b) a, b = b, r - u, s = s, u - q*s - v, t = t, v - q*t + u, s = s, u - q * s + v, t = t, v - q * t return (u, v, a) - - -def mod_inverse(a,b): - x,y,z = exgcd(a,b) - return x; - -def phi(x): - if x==1: - return 1; - factors = factor(x); - ans = x; + +def mod_inverse(a: int, b: int): + """ + Calculate the modular inverse of a with respect to b. + + The modular inverse of a is the number x such that (a * x) % b == 0. + + Parameters: + a: The number for which to find the modular inverse. + b: The modulus. + Returns: + The modular inverse of a with respect to b. + Raises: + ValueError: If the modular inverse does not exist. + """ + return pow(a, -1, b) + + +def phi(x: int): + """ + Calculate Euler's Totient function for a given integer x. + + Euler's Totient function, φ(x), is defined as the number of positive integers + less than or equal to x that are relatively prime to x. + Args: + x: The integer for which to calculate the Totient function. + Returns: + The value of Euler's Totient function for the given integer x. + """ + if x == 1: + return 1 + factors = factor(x) + ans = x for prime in factors: - ans=int(ans / prime[0]*(prime[0]-1)) + ans = ans // prime[0] * (prime[0] - 1) return ans -def miu(x): - if x==1: - return 1; + +def miu(x: int): + """ + Calculate the Möbius function value for a given integer x. + + The Möbius function μ(x) is defined as: + - μ(1) = 1 + - μ(x) = 0 if x has a squared prime factor + - μ(x) = (-1)^k if x is a product of k distinct prime factors + Args: + x: The integer for which to calculate the Möbius function. + Returns: + The Möbius function value for the given integer x. + """ + if x == 1: + return 1 factors = factor(x) for prime in factors: - if prime[1]>1: - return 0; - return 1-(len(factors) and 1)*2 - - -#--- number base conversion ------------------------------------------------------------------- -#source: http://interactivepython.org/runestone/static/pythonds/Recursion/pythondsConvertinganIntegertoaStringinAnyBase.html -def dec2base(n,base): - convertString = "0123456789ABCDEF" - if n < base: - return convertString[n] - else: - return dec2base(n//base,base) + convertString[n%base] - -#--- number to words ---------------------------------------------------------------------------- + if prime[1] > 1: + return 0 + return 1 - (len(factors) and 1) * 2 + + +#source: +# http://interactivepython.org/runestone/static/pythonds/Recursion/pythondsConvertinganIntegertoaStringinAnyBase.html +def dec2base(n: int, base: int) -> str: + """ + Convert a decimal number to a specified base. + Args: + n: The decimal number to convert. + base: The base to convert the number to. Must be between 2 and 16. + Returns: + The number represented in the specified base. + Raises: + ValueError: If the base is not between 2 and 16. + """ + convert_string = "0123456789ABCDEF" + if n < base: + return convert_string[n] + return dec2base(n // base, base) + convert_string[n % base] + + #this function copied from stackoverflow user: Developer, Oct 5 '13 at 3:45 -def n2words(num,join=True): - '''words = {} convert an integer number into words''' - units = ['','One','Two','Three','Four','Five','Six','Seven','Eight','Nine'] +def n2words(num: int, join: bool = True): + """ + Convert a number into words. + Args: + num: The number to convert. + join (bool, optional): If True, join the words with spaces. Defaults to True. + Returns: + The number in words as a single string if join is True, otherwise as a list of words. + """ + units = [ + '', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', + 'Nine' + ] teens = ['','Eleven','Twelve','Thirteen','Fourteen','Fifteen','Sixteen', \ 'Seventeen','Eighteen','Nineteen'] tens = ['','Ten','Twenty','Thirty','Forty','Fifty','Sixty','Seventy', \ @@ -345,27 +518,34 @@ def n2words(num,join=True): 'Tredecillion','Quattuordecillion','Sexdecillion', \ 'Septendecillion','Octodecillion','Novemdecillion', \ 'Vigintillion'] - words = [] - if num==0: words.append('zero') + words: List[str] = [] + if num == 0: + words.append('zero') else: - numStr = '%d'%num - numStrLen = len(numStr) - groups = int((numStrLen+2)/3) - numStr = numStr.zfill(groups*3) - for i in range(0,groups*3,3): - h,t,u = int(numStr[i]),int(numStr[i+1]),int(numStr[i+2]) - g = groups-(int(i/3)+1) - if h>=1: + num_str = '%d' % num + num_str_len = len(num_str) + groups = int((num_str_len + 2) / 3) + num_str = num_str.zfill(groups * 3) + for i in range(0, groups * 3, 3): + h, t, u = int(num_str[i]), int(num_str[i + 1]), int(num_str[i + 2]) + g = groups - (int(i / 3) + 1) + if h >= 1: words.append(units[h]) words.append('Hundred') - if t>1: + if t > 1: words.append(tens[t]) - if u>=1: words.append(units[u]) - elif t==1: - if u>=1: words.append(teens[u]) - else: words.append(tens[t]) + if u >= 1: + words.append(units[u]) + elif t == 1: + if u >= 1: + words.append(teens[u]) + else: + words.append(tens[t]) else: - if u>=1: words.append(units[u]) - if (g>=1) and ((h+t+u)>0): words.append(thousands[g]+'') - if join: return ' '.join(words) + if u >= 1: + words.append(units[u]) + if (g >= 1) and ((h + t + u) > 0): + words.append(thousands[g] + '') + if join: + return ' '.join(words) return words diff --git a/cyaron/sequence.py b/cyaron/sequence.py index 625456e..4cf6ba3 100644 --- a/cyaron/sequence.py +++ b/cyaron/sequence.py @@ -1,33 +1,73 @@ -from .utils import * +""" +This module provides a `Sequence` class for generating sequences +based on a given formula and initial values. + +Classes: + Sequence: A class for creating and managing sequences. +""" + +from typing import Callable, Dict, List, Optional, Tuple, TypeVar, Union +from typing import cast as typecast + +from .utils import list_like + +T = TypeVar('T') + class Sequence: - """Class Sequence: the tool class for sequences. - """ + """A class for creating and managing sequences.""" - def __init__(self, formula, initial_values=()): - """__init__(self, formula, initial_values=() -> None - Create a sequence object. - int formula(int, function) -> the formula function ... + def __init__(self, + formula: Callable[[int, Callable[[int], T]], T], + initial_values: Union[List[T], Tuple[T, ...], Dict[int, + T]] = ()): + """ + Initialize a sequence object. + Parameters: + formula: A function that defines the formula for the sequence. + initial_values (optional): Initial values for the sequence. + Can be a list, tuple, or dictionary. Defaults to an empty tuple. """ if not callable(formula): - raise Exception("formula must be a function") + raise TypeError("formula must be a function") self.formula = formula if list_like(initial_values): - self.values = dict(enumerate(initial_values)) + self.values = dict( + enumerate( + typecast(Union[List[T], Tuple[T, ...]], initial_values))) elif isinstance(initial_values, dict): self.values = initial_values else: - raise Exception("Initial_values must be either a list/tuple or a dict.") + raise TypeError( + "Initial_values must be either a list/tuple or a dict.") - def __get_one(self, i): + def get_one(self, i: int): + """ + Retrieve the value at the specified index, computing it if necessary. + Args: + i (int): The index of the value to retrieve. + Returns: + The value at the specified index. + If the value at the specified index is not already computed, it will be + calculated using the provided formula and stored for future access. + """ if i in self.values: return self.values[i] - - self.values[i] = self.formula(i, self.__get_one) + self.values[i] = self.formula(i, self.get_one) return self.values[i] - def get(self, left_range, right_range=None): + def get(self, left_range: int, right_range: Optional[int] = None): + """ + Retrieve a sequence of elements within the specified range. + If only `left_range` is provided, a single element is retrieved. + If both `left_range` and `right_range` are provided, a list of elements + from `left_range` to `right_range` (inclusive) is retrieved. + Args: + left_range: The starting index or the single index to retrieve. + right_range (optional): The ending index for the range retrieval. Defaults to None. + Returns: + A single element if `right_range` is None, otherwise a list of elements. + """ if right_range is None: - return self.__get_one(left_range) - - return [self.__get_one(i) for i in range(left_range, right_range+1)] + return self.get_one(left_range) + return [self.get_one(i) for i in range(left_range, right_range + 1)] diff --git a/cyaron/tests/__init__.py b/cyaron/tests/__init__.py index dabf4f9..328a930 100644 --- a/cyaron/tests/__init__.py +++ b/cyaron/tests/__init__.py @@ -4,3 +4,4 @@ from .polygon_test import TestPolygon from .compare_test import TestCompare from .graph_test import TestGraph +from .vector_test import TestVector diff --git a/cyaron/tests/graph_test.py b/cyaron/tests/graph_test.py index 976930a..1e3f1db 100644 --- a/cyaron/tests/graph_test.py +++ b/cyaron/tests/graph_test.py @@ -151,3 +151,24 @@ def test_DAG_boundary(self): with self.assertRaises(Exception, msg="the number of edges of connected graph must more than the number of nodes - 1"): Graph.DAG(8, 6) Graph.DAG(8, 7) + + def test_GraphMatrix(self): + g = Graph(3, True) + edge_set = [(2, 3, 3), (3, 3, 1), (2, 3, 7), (2, 3, 4), (3, 2, 1), (1, 3, 3)] + for u, v, w in edge_set: + g.add_edge(u, v, weight=w) + self.assertEqual(str(g.to_matrix()), "-1 -1 3\n-1 -1 4\n-1 1 1") + self.assertEqual(str(g.to_matrix(default=0)), "0 0 3\n0 0 4\n0 1 1") + # lambda val, edge: edge.weight + gcd = lambda a, b: (gcd(b, a % b) if b else a) + lcm = lambda a, b: a * b // gcd(a, b) + merge1 = lambda v, e: v if v != -1 else e.weight + merge2 = lambda val, edge: max(edge.weight, val) + merge3 = lambda val, edge: min(edge.weight, val) + merge4 = lambda val, edge: gcd(val, edge.weight) + merge5 = lambda val, edge: lcm(val, edge.weight) if val else edge.weight + self.assertEqual(str(g.to_matrix(merge=merge1)), "-1 -1 3\n-1 -1 3\n-1 1 1") + self.assertEqual(str(g.to_matrix(merge=merge2)), "-1 -1 3\n-1 -1 7\n-1 1 1") + self.assertEqual(str(g.to_matrix(default=9, merge=merge3)), "9 9 3\n9 9 3\n9 1 1") + self.assertEqual(str(g.to_matrix(default=0, merge=merge4)), "0 0 3\n0 0 1\n0 1 1") + self.assertEqual(str(g.to_matrix(default=0, merge=merge5)), "0 0 3\n0 0 84\n0 1 1") diff --git a/cyaron/tests/io_test.py b/cyaron/tests/io_test.py index 1da50e4..b83dc03 100644 --- a/cyaron/tests/io_test.py +++ b/cyaron/tests/io_test.py @@ -2,6 +2,7 @@ import os import shutil import tempfile +import subprocess from cyaron import IO from cyaron.output_capture import captured_output @@ -26,7 +27,12 @@ def test_create_files_simple(self): def test_create_files_prefix_id(self): with captured_output() as (out, err): - IO(file_prefix="test_prefix", data_id=233, input_suffix=".inp", output_suffix=".ans") + IO( + file_prefix="test_prefix", + data_id=233, + input_suffix=".inp", + output_suffix=".ans", + ) self.assertTrue(os.path.exists("test_prefix233.inp")) self.assertTrue(os.path.exists("test_prefix233.ans")) @@ -50,8 +56,8 @@ def test_write_stuff(self): input = f.read() with open("test_write.out") as f: output = f.read() - self.assertEqual(input.split(), ['1', '2', '3', '4', '5', '6', '7', '8', '9']) - self.assertEqual(output.split(), ['9', '8', '7', '6', '5', '4', '3', '2', '1']) + self.assertEqual(input.split(), ["1", "2", "3", "4", "5", "6", "7", "8", "9"]) + self.assertEqual(output.split(), ["9", "8", "7", "6", "5", "4", "3", "2", "1"]) self.assertEqual(input.count("\n"), 2) self.assertEqual(output.count("\n"), 2) @@ -64,15 +70,44 @@ def test_output_gen(self): output = f.read() self.assertEqual(output.strip("\n"), "233") + def test_output_gen_time_limit_exceeded(self): + time_limit_exceeded = False + with captured_output() as (out, err): + with open("long_time.py", "w") as f: + f.write("import time\ntime.sleep(10)\nprint(1)") + + try: + with IO("test_gen.in", "test_gen.out") as test: + test.output_gen("python long_time.py", time_limit=1) + except subprocess.TimeoutExpired: + time_limit_exceeded = True + self.assertEqual(time_limit_exceeded, True) + + def test_output_gen_time_limit_not_exceeded(self): + time_limit_exceeded = False + with captured_output() as (out, err): + with open("short_time.py", "w") as f: + f.write("import time\ntime.sleep(0.2)\nprint(1)") + + try: + with IO("test_gen.in", "test_gen.out") as test: + test.output_gen("python short_time.py", time_limit=1) + except subprocess.TimeoutExpired: + time_limit_exceeded = True + with open("test_gen.out") as f: + output = f.read() + self.assertEqual(output.strip("\n"), "1") + self.assertEqual(time_limit_exceeded, False) + def test_init_overload(self): - with IO(file_prefix='data{', data_id=5) as test: - self.assertEqual(test.input_filename, 'data{5.in') - self.assertEqual(test.output_filename, 'data{5.out') - with IO('data{}.in', 'data{}.out', 5) as test: - self.assertEqual(test.input_filename, 'data5.in') - self.assertEqual(test.output_filename, 'data5.out') - with open('data5.in', 'w+') as fin: - with open('data5.out', 'w+') as fout: + with IO(file_prefix="data{", data_id=5) as test: + self.assertEqual(test.input_filename, "data{5.in") + self.assertEqual(test.output_filename, "data{5.out") + with IO("data{}.in", "data{}.out", 5) as test: + self.assertEqual(test.input_filename, "data5.in") + self.assertEqual(test.output_filename, "data5.out") + with open("data5.in", "w+") as fin: + with open("data5.out", "w+") as fout: with IO(fin, fout) as test: self.assertEqual(test.input_file, fin) self.assertEqual(test.output_file, fout) diff --git a/cyaron/tests/vector_test.py b/cyaron/tests/vector_test.py new file mode 100644 index 0000000..1380de4 --- /dev/null +++ b/cyaron/tests/vector_test.py @@ -0,0 +1,33 @@ +import unittest +from cyaron.vector import * + + +def has_duplicates(lst: list): + return len(lst) != len(set(lst)) + + +class TestVector(unittest.TestCase): + def test_unique_vector(self): + v = Vector.random(10**5, [10**6]) + self.assertFalse(has_duplicates(list(map(lambda tp: tuple(tp), v)))) + self.assertTrue(all(map(lambda v: 0 <= v[0] <= 10**6, v))) + v = Vector.random(1000, [(10**5, 10**6)]) + self.assertTrue(all(map(lambda v: 10**5 <= v[0] <= 10**6, v))) + with self.assertRaises( + Exception, + msg="1st param is so large that CYaRon can not generate unique vectors", + ): + v = Vector.random(10**5, [10**4]) + + def test_repeatable_vector(self): + v = Vector.random(10**5 + 1, [10**5], VectorRandomMode.repeatable) + self.assertTrue(all(map(lambda v: 0 <= v[0] <= 10**5, v))) + self.assertTrue(has_duplicates(list(map(lambda tp: tuple(tp), v)))) + v = Vector.random(1000, [(10**5, 10**6)], VectorRandomMode.repeatable) + self.assertTrue(all(map(lambda v: 10**5 <= v[0] <= 10**6, v))) + + def test_float_vector(self): + v = Vector.random(10**5, [10**5], VectorRandomMode.float) + self.assertTrue(all(map(lambda v: 0 <= v[0] <= 10**5, v))) + v = Vector.random(10**5, [(24, 25)], VectorRandomMode.float) + self.assertTrue(all(map(lambda v: 24 <= v[0] <= 25, v))) diff --git a/cyaron/utils.py b/cyaron/utils.py index 4ef7358..5b7cfd5 100644 --- a/cyaron/utils.py +++ b/cyaron/utils.py @@ -10,7 +10,7 @@ def list_like(data): Judge whether the object data is like a list or a tuple. object data -> the data to judge """ - return isinstance(data, tuple) or isinstance(data, list) + return isinstance(data, (tuple, list)) def int_like(data): @@ -34,10 +34,8 @@ def strtolines(str): def make_unicode(data): - try: - return unicode(data) - except NameError: - return str(data) + return str(data) + def unpack_kwargs(funcname, kwargs, arg_pattern): rv = {} @@ -58,7 +56,12 @@ def unpack_kwargs(funcname, kwargs, arg_pattern): except KeyError as e: error = True if error: - raise TypeError('{}() missing 1 required keyword-only argument: \'{}\''.format(funcname, tp)) + raise TypeError( + '{}() missing 1 required keyword-only argument: \'{}\''. + format(funcname, tp)) if kwargs: - raise TypeError('{}() got an unexpected keyword argument \'{}\''.format(funcname, next(iter(kwargs.items()))[0])) + raise TypeError( + '{}() got an unexpected keyword argument \'{}\''.format( + funcname, + next(iter(kwargs.items()))[0])) return rv diff --git a/cyaron/vector.py b/cyaron/vector.py index c6b70e4..5814fc9 100644 --- a/cyaron/vector.py +++ b/cyaron/vector.py @@ -1,8 +1,13 @@ -# coding=utf8 +""" + +""" -from .utils import * import random from enum import IntEnum +from typing import Sequence, Union, Tuple, List, Set +from typing import cast as typecast + +from .utils import list_like class VectorRandomMode(IntEnum): @@ -11,55 +16,79 @@ class VectorRandomMode(IntEnum): float = 2 +_Number = Union[int, float] + + class Vector: + """A class for generating random vectors""" + + IntVector = List[List[int]] + FloatVector = List[List[float]] @staticmethod - def random(num: int = 5, position_range: list = None, mode: VectorRandomMode = 0, **kwargs): + def random( + num: int = 5, + position_range: Union[List[Union[_Number, Tuple[_Number, _Number]]], + None] = None, + mode: VectorRandomMode = VectorRandomMode.unique, + ) -> Union[IntVector, FloatVector]: """ - brief : generating n random vectors in limited space - param : - # num : the number of vectors - # position_range : a list of limits for each dimension - # single number x represents range (0, x) - # list [x, y] or tuple (x, y) represents range (x, y) - # mode : the mode vectors generate, see Enum Class VectorRandomMode + Generate `num` random vectors in limited space + Args: + num: the number of vectors + position_range: a list of limits for each dimension + single number x represents range (0, x) + list [x, y] or tuple (x, y) represents range (x, y) + mode: the mode vectors generate, see Enum Class VectorRandomMode """ if position_range is None: position_range = [10] - if num > 1000000: - raise Exception("num no more than 1e6") if not list_like(position_range): - raise Exception("the 2nd param must be a list or tuple") + raise TypeError("the 2nd param must be a list or tuple") dimension = len(position_range) - offset = [] - length = [] + offset: Sequence[_Number] = [] + length: Sequence[_Number] = [] vector_space = 1 for i in range(0, dimension): - if list_like(position_range[i]): - if position_range[i][1] < position_range[i][0]: - raise Exception("upper-bound should larger than lower-bound") - offset.append(position_range[i][0]) - length.append(position_range[i][1] - position_range[i][0]) + now_position_range = position_range[i] + if isinstance(now_position_range, tuple): + if now_position_range[1] < now_position_range[0]: + raise ValueError( + "upper-bound should be larger than lower-bound") + offset.append(now_position_range[0]) + length.append(now_position_range[1] - now_position_range[0]) else: offset.append(0) - length.append(position_range[i]) + length.append(now_position_range) vector_space *= (length[i] + 1) if mode == VectorRandomMode.unique and num > vector_space: - raise Exception("1st param is so large that CYaRon can not generate unique vectors") + raise ValueError( + "1st param is so large that CYaRon can not generate unique vectors" + ) - result = [] + result: Union[List[List[int]], List[List[float]]] if mode == VectorRandomMode.repeatable: - result = [[random.randint(x, y) for x, y in zip(offset, length)] for _ in range(num)] + offset = typecast(Sequence[int], offset) + length = typecast(Sequence[int], length) + result = [[ + random.randint(x, x + y) for x, y in zip(offset, length) + ] for _ in range(num)] elif mode == VectorRandomMode.float: - result = [[random.uniform(x, y) for x, y in zip(offset, length)] for _ in range(num)] + result = [[ + random.uniform(x, x + y) for x, y in zip(offset, length) + ] for _ in range(num)] elif mode == VectorRandomMode.unique and vector_space > 5 * num: # O(NlogN) - num_set = set() + offset = typecast(Sequence[int], offset) + length = typecast(Sequence[int], length) + vector_space = typecast(int, vector_space) + num_set: Set[int] = set() + result = typecast(List[List[int]], []) for i in range(0, num): while True: rand = random.randint(0, vector_space - 1) @@ -72,9 +101,14 @@ def random(num: int = 5, position_range: list = None, mode: VectorRandomMode = 0 result.append(tmp) else: # generate 0~vector_space and shuffle + offset = typecast(Sequence[int], offset) + length = typecast(Sequence[int], length) + vector_space = typecast(int, vector_space) rand_arr = list(range(0, vector_space)) random.shuffle(rand_arr) - result = [Vector.get_vector(dimension, length, x) for x in rand_arr[:num]] + result = [ + Vector.get_vector(dimension, length, x) for x in rand_arr[:num] + ] for x in result: for i in range(dimension): @@ -83,8 +117,19 @@ def random(num: int = 5, position_range: list = None, mode: VectorRandomMode = 0 return result @staticmethod - def get_vector(dimension: int, position_range: list, hashcode: int): - tmp = [] + def get_vector(dimension: int, position_range: Sequence[int], + hashcode: int): + """ + Generates a vector based on the given dimension, position range, and hashcode. + Args: + dimension: The number of dimensions for the vector. + position_range: A list of integers specifying the range for each dimension. + hashcode: A hashcode used to generate the vector. + Returns: + list: A list representing the generated vector. + """ + + tmp: List[int] = [] for i in range(0, dimension): tmp.append(hashcode % (position_range[i] + 1)) hashcode //= (position_range[i] + 1) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..b1294ef --- /dev/null +++ b/poetry.lock @@ -0,0 +1,41 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] + +[[package]] +name = "colorful" +version = "0.5.6" +description = "Terminal string styling done right, in Python." +optional = false +python-versions = "*" +files = [ + {file = "colorful-0.5.6-py2.py3-none-any.whl", hash = "sha256:eab8c1c809f5025ad2b5238a50bd691e26850da8cac8f90d660ede6ea1af9f1e"}, + {file = "colorful-0.5.6.tar.gz", hash = "sha256:b56d5c01db1dac4898308ea889edcb113fbee3e6ec5df4bacffd61d5241b5b8d"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "xeger" +version = "0.4.0" +description = "A library for generating random strings from a valid regular expression." +optional = false +python-versions = "*" +files = [ + {file = "xeger-0.4.0-py3-none-any.whl", hash = "sha256:a0f544faf45ac56a29af4e628bd1e6996334f090458d78a61581490df1aad252"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.6" +content-hash = "f61b42d8bd0c6814638b0f4d9b5afa1b049f7ab03fb55dbdceaceba39936d21c" diff --git a/pyproject.toml b/pyproject.toml index 52609f0..59ae302 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [tool.poetry] name = "cyaron" -version = "0.5.0" +version = "0.6.0" description = "CYaRon: Yet Another Random Olympic-iNformatics test data generator, A library for automatically generating test data for Online Judge, Olympic Informatics or automatic application testing" authors = ["Luogu Development Team "] license = "LGPL-3.0" readme = "README.md" [tool.poetry.dependencies] -python = ">=3.5" +python = ">=3.6" xeger = "^0.4.0" colorful = "^0.5.6" diff --git a/tox.ini b/tox.ini index ed1e45d..581148d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,2 +1,11 @@ +[tox] +envlist = py36, py37, py38, py39, py310, py311, py312 +requires = virtualenv < 20.22.0 +isolated_build = true + [testenv] -commands=python unit_test.py +deps = + xeger + colorful +commands = python unit_test.py +allowlist_externals = poetry