I want to time how long my code takes to perform on average over many test runs. In each test run, doWork()
performs the work that I want to time. But I also want to checkWork()
in each test run without it counting towards the time. I'm going to have many similar Exercise# classes, so I'd like to abstract the testing via a TestInterface. Is my current way a reasonable approach? Or is there a better design pattern / standard approach? Thanks in advance.
@FunctionalInterface
public interface TestInterface {
void test(final int NUM_TESTS);
}
public class TimeTests {
public static void test(TestInterface ti, final int NUM_TESTS, String testName) {
DecimalFormat df = new DecimalFormat("#.####");
long start = System.nanoTime();
ti.test(NUM_TESTS);
System.out.println("DEBUG: " + testName + " took "
+ df.format((System.nanoTime() - start) * 1.0 / NUM_TESTS)
+ " nanoseconds on average for " + NUM_TESTS + " tests");
}
}
public class Exercise1 {
private static final int NUM_TESTS = (int) Math.pow(10, 6);
private static void mainWork(List<Integer> A) {
// do stuff and time it
}
private static void checkWork(List<Integer> A) {
// do stuff but don't count it towards the time
}
public static void main(String[] args) {
TimeTests.test((NUM_TESTS_LOCAL) -> {
for (int i = 0; i < NUM_TESTS_LOCAL; ++i) {
List<Integer> A = new ArrayList<>();
// add random elements to A
mainWork(A);
checkWork(A);
}
}, NUM_TESTS, "Exercise1");
}
}
Okay, I think I managed to put together a decent framework (is this the right word?) for this task. If anybody could chime in to let me know if my approach is any good, I'd really appreciate it.
While my code seems to work fine for my use cases so far, I have a few questions:
public interface CloneableTestInput<T extends CloneableTestInput<T>>
, how is the type template <T extends CloneableTestInput<T>
not a circular definition? I'm not sure I fully understand what that type template is saying.CloneableList
class that implements CloneableTestInput<List>
? Currently, I need to make a separate implementation for each Collection type (e.g. ArrayList, LinkedList, ...). Similarly, is it possible to make a generic CloneableSet
class that implements CloneableTestInput<Set>
?Thanks in advance :)
This allows TimeTests.java
to work for generic input types.
public interface CloneableTestInput<T extends CloneableTestInput<T>> extends Cloneable {
T clone();
}
public class CloneableString implements CloneableTestInput<CloneableString> {
public String data;
public CloneableString() {}
public CloneableString(String input) { data = input; }
public CloneableString clone() { return new CloneableString(String.valueOf(data)); }
}
public class CloneableArrayList extends ArrayList implements CloneableTestInput<CloneableArrayList> {
public CloneableArrayList(ArrayList input) {
this.addAll(input);
}
@Override
public CloneableArrayList clone() {
return new CloneableArrayList(this);
}
}
@FunctionalInterface
public interface TimeTestsInterface<outputType> {
void test(Callable<CloneableTestInput> formInput
, Function<CloneableTestInput, outputType> runAlgorithm
, Function<CloneableTestInput, outputType> getKnownOutput
, BiFunction<outputType, outputType, Boolean> checkResults
, final int NUM_TESTS, String testName);
}
public class TimeTests<outputType> implements TimeTestsInterface<outputType> {
public void test(Callable<CloneableTestInput> formInput
, Function<CloneableTestInput, outputType> runAlgorithm
, Function<CloneableTestInput, outputType> getKnownOutput
, BiFunction<outputType, outputType, Boolean> checkResults
, final int NUM_TESTS, String testName) {
try {
DecimalFormat df = new DecimalFormat("#.####");
long total = 0, start;
for (int i=0; i < NUM_TESTS; ++i) {
CloneableTestInput input = formInput.call();
CloneableTestInput orig_input = input.clone();
start = System.nanoTime();
outputType algorithmResult = runAlgorithm.apply(input);
total += System.nanoTime() - start;
outputType expectedResult = getKnownOutput.apply(orig_input);
assert(checkResults.apply(algorithmResult, expectedResult));
}
System.out.println("DEBUG: " + testName + " took "
+ df.format(total * 1.0 / NUM_TESTS)
+ " nanoseconds on average for " + NUM_TESTS + " tests");
} catch (Exception|AssertionError e) {
System.out.println(e.toString() + " - " + e.getMessage() + " - ");
e.printStackTrace();
}
}
}
/**
* Problem 6.2 from EPI
* Given an array A of digits encodiing a decimal number D,
* with MSD at A[0]. Update A to hold D + 1.
*/
public class PlusOne {
private static final int NUM_TESTS = (int) Math.pow(10, 5);
private static final int ARR_LENGTH = (int) Math.pow(10, 2);
private static ArrayList<Integer> plusOne(ArrayList<Integer> A) {
int n = A.size() - 1;
A.set(n, A.get(n) + 1);
for (int i = n; i > 0 && A.get(i) == 10; --i) {
A.set(i, 0);
A.set(i-1, A.get(i-1) + 1);
}
if (A.get(0) == 10) {
// Need additional digit up front as MSD
A.set(0,0);
A.add(0,1);
}
return A;
}
private static ArrayList<Integer> randArray(int len) {
ArrayList<Integer> A = new ArrayList<>();
if (len == 0) return A;
Random rgen = new Random();
A.add(rgen.nextInt(9) + 1);
--len;
while (len != 0) {
A.add(rgen.nextInt(10));
--len;
}
return A;
}
public static void main(String[] args) {
Callable<CloneableTestInput> formInput = () -> new CloneableArrayList(randArray(ARR_LENGTH));
Function<CloneableTestInput, ArrayList<Integer>> runAlgorithm =
(input) -> plusOne((ArrayList<Integer>) input);
Function<CloneableTestInput, ArrayList<Integer>> getKnownOutput =
(orig_input) -> {
BigInteger B = new BigInteger(Joiner.on("").join((ArrayList<Integer>) orig_input));
B = B.add(BigInteger.valueOf(1));
ArrayList<Integer> expectedOutput = new ArrayList<>();
while (B.compareTo(BigInteger.valueOf(0)) > 0) {
expectedOutput.add(0, B.mod(BigInteger.valueOf(10)).intValue());
B = B.divide(BigInteger.valueOf(10));
}
return expectedOutput;
};
BiFunction<ArrayList<Integer>, ArrayList<Integer>, Boolean> checkResults = List::equals;
TimeTests<ArrayList<Integer>> algTimer = new TimeTests<>();
algTimer.test(formInput, runAlgorithm, getKnownOutput, checkResults, NUM_TESTS, "PlusOne");
}
}
public class CanStringBePalindrome {
private static final int INPUT_STRING_LENGTH = (int) Math.pow(10, 2);
private static final int NUM_TESTS = (int) Math.pow(10, 6);
private static boolean canFormPalindromeHash(final String s) {
Map<Character, Integer> charFreqs = new HashMap<>();
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (!charFreqs.containsKey(c))
charFreqs.put(c, 1);
else
charFreqs.put(c, charFreqs.get(c) + 1);
}
int oddFreqCount = 0;
for (Map.Entry<Character, Integer> entry : charFreqs.entrySet()) {
if ((entry.getValue() % 2) != 0 && (++oddFreqCount > 1))
return false;
}
return true;
}
private static boolean canFormPalindromeSorting(final String s) {
// TODO : find faster/simpler way of getting frequency counts
char[] a = s.toCharArray();
Arrays.sort(a);
int oddFreqCount = 0;
int numCurrChar =1;
for (int i = 1; i < a.length && oddFreqCount <= 1; ++i) {
if(a[i] != a[i-1]) {
if ((numCurrChar & 1) != 0)
++oddFreqCount;
numCurrChar = 1;
} else
++numCurrChar;
}
if ((numCurrChar & 1) != 0)
++oddFreqCount;
return oddFreqCount <= 1;
}
private static String randString(int len) {
StringBuilder sb = new StringBuilder();
Random rgen = new Random();
while (len-- > 0)
sb.append((char)(rgen.nextInt(26) + 'A'));
return sb.toString();
}
public static void main(String[] args) {
Callable<CloneableTestInput> formInput = () -> new CloneableString(randString(INPUT_STRING_LENGTH));
Function<CloneableTestInput, Boolean > runAlgorithm =
(input) -> canFormPalindromeHash(((CloneableString)input).data);
Function<CloneableTestInput, Boolean> getKnownOutput =
(orig_input) -> canFormPalindromeSorting(((CloneableString)orig_input).data);
BiFunction<Boolean, Boolean, Boolean> checkResults = Boolean::equals;
TimeTests<Boolean> algTimer = new TimeTests<>();
algTimer.test(formInput, runAlgorithm, getKnownOutput, checkResults
, NUM_TESTS, "CanStringBePalindrome");
}
}