Project structure
eulertools can be invoked from everywhere inside the project, as it
recursively looks for the project root. The project root is marked by
the existence of a .euler directory. The required files and directories
inside it are:
.euler/euler.toml.euler/statements/<problem_name>.tomlfor every problem
The problem name
euler.toml
The project can be configured using a TOML file, called euler.toml,
inside the .euler directory.
Also, json and yaml file types are allowed, and the type is chosen from the suffix of the file.
Each language is a section [language.<language_name>], with the
following fields:
- extension: [optional] the filename extension for the problems solved
in this language. Defaults to
./<language_name> - path: [optional] the path (relative to the project root)
of the language solution. Defaults to
./<language_name> - runner: the path (relative to the project root) of the solution runner
There is a section called $meta, that allows to add some info for eulertools themselves.
At the moment, the only field that is used is __version__, and specifies the min eulertools
version to be used.
| euler.toml | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Problem runner
the runner is a cli app that runs the problems for a specific language. It should be able to run with the following positional arguments:
user@localhost $ <runner> <problem_name> <times>
This command runs the problem named problem_name <times> times.
It expects that each run returns a pair of lines in the following format:
1 2 | |
The response_key is used to differentiate between different test cases.
A simple program runner in python can look like:
| Python runner | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
Statements directory
The .euler directory should have a subdirectory named statements, and inside it, it should be a file
for each problem, using the name <problem_name>.toml. The runner will use this to find which problems
have been solved. The file must have a table (even empty) for every language that is needed. Furthermore,
there are two required sections if the euler generate or euler statement are to be used.
For euler statement, the following toml tables are required:
[common]
id = "1"
title = "Two Sum"
description = """
Given an array of integers, return indices of the two numbers such that they
add up to a specific target.
You may assume that each input would have exactly one solution, and you may not
use the same element twice.
"""
hint = """
For better than O(n^2) complexity, keep a dictionary of the complements.
"""
Generate-specific structure
eulertools can generate new solution files based on a template. In order to use this, the following structure is
needed.
There should be a .euler/templates/<language_name>.jinja file. Also, the .euler/statements/<problem_name>.toml
needs to have a toml table named <language_name> to that will be treated as the context of the solution. When the solution
is generated, it will create a file in the <language_path>/src/solutions with the name <language_name>.<language_extension>.
For the above problem an example python template and toml table are the following:
from time import perf_counter_ns
class Solution:
def {{method}}(self{% for arg in args %}, {{arg}}: {{types.get(arg)}}{% endfor %}) -> {{rtype}}:
pass
def proxy({% for arg in args %}{{arg}}: {{types.get(arg)}}, {% endfor %}response_key: int) -> None:
solution = Solution()
begin = perf_counter_ns()
answer = solution.{{method}}({{args|join(", ")}})
end = perf_counter_ns()
print(f"Time {response_key} {end - begin}")
print(f"Answer {response_key} {answer}")
def run() -> None:
proxy(..., response_key=1)
proxy(..., response_key=1)
[python]
method = "twoSum"
args = [
"nums",
"target",
]
types = { nums = "list[int]", target = "int" }
rtype = "list[int]"
General considerations
It is advised that the runner is importing the solution and runs it within the language, to have the best timings. This will allow some optimisations with jit compilers to happen, and can reveal issues with memory leaks.
A good idea is that there is a run method for each problem that is responsible for passing
the different test arguments to the actual runner. The run can be the only public method of the
problem. This will make it easier to have a similar format for all the languages that you'll attempt
to solve the problem.
The response_key can be any string that contains no spaces, but it's better to use an small integer. It makes the debugging of a failed case easier, as it's one of the most readable id formats.