5
\$\begingroup\$

I've found that it's a huge pain to have to write the boilerplate code for each Project Euler problem I write (timing code, main method, what about running every problem instead of just one?). So I decided to create a program to run the problems for me.

Main.java

package projecteuler;

import projecteuler.autorun.Executor;

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        Executor executor = new Executor();
        executor.addAll(IntStream.range(1, 500).boxed()::iterator);
        executor.run();
    }
}

Problem.java

package projecteuler.autorun;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.Callable;

public class Problem implements Runnable {
    private final String name;
    private final Callable<String> problem;
    private String result;
    private double time;

    private Problem(String name, Callable<String> problem) {
        this.name = name;
        this.problem = problem;
    }

    /**
     * Runs the problem, timing it and storing the result. This must
     * be called before the getters on <code>result</code> and <code>time</code>
     */
    @Override
    public void run() {
        try {
            long start = System.nanoTime();
            result = problem.call();
            time = (System.nanoTime() - start) * 1e-9;
        } catch (Exception e) {
            e.printStackTrace(); // Swallow; we want to continue executing problems even if this problem failed.
                                 // I think that better would be to use a logger to log
                                 // this, but I'm unsure how to go about doing so.
        }
    }

    /**
     * Attempts to create a problem for the given number. If no suitable problem exists, throws an Exception.
     * <p>
     * The given Problem must exist under the package <code>projecteuler.problems</code> under the name
     * <code>ProblemNUM</code>, where <code>NUM</code> is the problem's number, prefixed by zeros if the
     * number is less than 3 digits long. The problem must only have a <code>problem()</code> method, it
     * need not be public. It's return value is the assumed result of the function. Any return value is
     * valid, as long as it can be converted to a String via <code>java.util.Objects.toString</code>
     *
     * @param num
     * @return A Problem for the given number.
     * @throws ClassNotFoundException   If the given class doesn't exist.
     * @throws IllegalArgumentException If the given problem doesn't support a problem method.
     */
    public static Problem number(int num) throws ClassNotFoundException {
        Class problem = Class.forName(String.format("projecteuler.problems.Problem%03d", num));
        if (!Arrays.stream(problem.getDeclaredMethods())
                .map(Method::getName)
                .anyMatch(s -> s.equals("problem"))) {
            throw new IllegalArgumentException(String.format("Problem%03d must contain a method called `problem`", num));
        }
        Method method = null;
        try {
            method = problem.getDeclaredMethod("problem");
        } catch (NoSuchMethodException e) {
            throw new InternalError("Should not be able to reach this state; we already checked that the method exists," +
                    "but now we find that it doesn't.");
        }
        method.setAccessible(true);

        Object toInvokeOn = null;
        if (!Modifier.isStatic(method.getModifiers())) {
            try {
                toInvokeOn = problem.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                throw new IllegalArgumentException(
                        String.format("Problem%03d's `problem` method is not static, " +
                                        "and the constructor can't be accessed", num
                        ), e);
            }
        }

        final Method finalMethod = method;
        final Object finalToInvokeOn = toInvokeOn;
        return new Problem("Problem " + num, () -> Objects.toString(finalMethod.invoke(finalToInvokeOn)));
    }

    public String getName() {
        return this.name;
    }

    public String getResult() {
        return this.result;
    }

    public double getTime() {
        return this.time;
    }
}

Executor.java

package projecteuler.autorun;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * A class which runs the problems given to it, timing each one and printing the result
 */
public class Executor implements Runnable {
    private final List<Problem> problems = new ArrayList<>();

    private static final int MAX_NAME_LENGTH = "Problem 000".length();
    private static final int MAX_RESULT_LENGTH = 50;
    private static final String FORMAT_STRING = "%-" + MAX_NAME_LENGTH + "s %-" + MAX_RESULT_LENGTH + "s %s%n";

    /**
     * Adds the given problem to the list of problems to run.
     * @param problem
     */
    public void add(Problem problem) {
        problems.add(problem);
    }

    /**
     * Adds all the given problems to the list of problems to run.
     * @param problems The problems to add.
     */
    public void addAll(Problem... problems) {
        for (Problem problem : problems) {
            add(problem);
        }
    }

    /**
     * Adds the given problem number to the list of problems to run.
     * @param num The problem number to run
     * @throws ClassNotFoundException If the given problem doesn't exist.
     */
    public void add(int num) throws ClassNotFoundException {
        add(Problem.number(num));
    }

    /**
     * Adds all the given problems to the list of problems to run. Any problem
     * which does not exist is silently ignored.
     * @param problems The problems to add.
     */
    public void addAll(int... problems) {
        addAll(Arrays.stream(problems).boxed()::iterator);
    }

    /**
     * Adds all the given problems to the list of problems to run. Any problem
     * which does not exist is silently ignored.
     * @param problems The problems to add.
     */
    public void addAll(Iterable<Integer> problems) {
        for (int problemNumber : problems) {
            try {
                add(Problem.number(problemNumber));
            } catch (ClassNotFoundException ignore) {
            }
        }
    }

    /**
     * Instructs the Executor to run its set of problems.
     */
    @Override
    public void run() {
        printHeader();
        for (Problem p : problems) {
            p.run();
            printData(p.getName(), p.getResult(), p.getTime());
        }
    }

    /**
     * Prints a header for the entire problem set.
     */
    private void printHeader() {
        System.out.printf(FORMAT_STRING, "Problem", "Result", "Time");
        System.out.println(
                String.format("%" + MAX_NAME_LENGTH + "s#%" + MAX_RESULT_LENGTH + "s#========", "", "")
                        .replace(' ', '=')
                        .replace('#', ' ')
        );
    }

    /**
     * Prints result data for the given problem.
     * @param name The name to display for the problem. Should look something like "Problem 1"
     * @param result The result of running the problem.
     * @param time The time it took to run the problem.
     */
    private void printData(String name, String result, double time) {
        System.out.printf(FORMAT_STRING, name, result, String.format("%f seconds", time));
    }
}

After having these, I can now write a file like as follows, and it will automatically run (please don't review this; this is given as an example usage, not as something I want reviewed):

Problem001.java

package projecteuler.problems;

import java.util.stream.IntStream;

public class Problem001 {
    int problem() {
        return IntStream.range(1, 1000)
                        .filter(number -> number % 3 == 0 || number % 5 == 0)
                        .sum();
    }
}

Result:

Problem     Result                                             Time
=========== ================================================== ========
Problem 1   ##HIDDEN##                                         0.006705 seconds
\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

Type safety

The problem implementations must follow several unwritten rules:

  • Class must be in package projecteuler.problems
  • Class must be named following the scheme Problem%03d
  • Class must have a parameterless public constructor
  • Class must have a public parameterless method named problem(), returning int

As these are unwritten rules, you might violate them without so much as a warning from the compiler. Depending on which Executor.add* method you call, any problems thrown while creating an instance may be silently ignored, which can be especially frustrating to debug. Another consequence of the current approach is that the implementation of Problem.number is non-trivial, using language features that are generally discouraged, such as Class.newInstance.

You can make it type-safe:

  • Create a Problem interface with a solve method
  • Make the Problem%03d classes implement Problem
    • Of course, you no longer need to follow the Problem%03d naming scheme
    • One simple reason for ditching the naming scheme is the ability for multiple alternative implementations of the same problem, for example Problem003WithSieveOfEratosthenes and Problem003WithSieveOfSundaram or Problem003WithBruteForce.
  • Add a register(Problem) method in Executor to register implementations by passing a single instance

This approach will solve the above issues:

  • Class must be in package projecteuler.problems -> restriction lifted, implementations of Problem can be anywhere

  • Class must be named following the scheme Problem%03d -> restriction lifted, classes can be named anything, multiple alternative implementations possible

  • Class must have a parameterless public constructor -> instantiation is no longer a concern, the responsibility is left up to callers of Executor.register

  • Class must have a public parameterless method named problem(), returning int -> now the compiler can enforce it

  • Tricky Problem.number -> instantiation is no longer a concern, the overall implementation can be greatly simplified

Lastly, I also concern the original requirements a usability issue (limiting reuse).

Utility

I can't help but wonder, what's the point of this? Why run many solutions at once? When I implement a solution for an online contest, For example, I have a very different need: writing in my local IDE (where it's the most comfortable), in a way that's easy to re-copy-paste to the contest web interface. My 2 cents.

\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.