Compare commits
10 Commits
45faf97cea
...
26ff1404a1
Author | SHA1 | Date | |
---|---|---|---|
|
26ff1404a1 | ||
|
152d9dc3ce | ||
|
8386ace107 | ||
|
562742206a | ||
|
6a89285dd6 | ||
|
66f05c56bf | ||
|
6782050414 | ||
|
72ae4c0136 | ||
|
499413f09a | ||
|
f005187a72 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -39,4 +39,5 @@ bin/
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
performance.txt
|
||||||
|
@ -2,8 +2,29 @@ image: azul/zulu-openjdk:8-latest
|
|||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
|
- analyze
|
||||||
|
|
||||||
|
# verify the padlock impl independently
|
||||||
unit-test-padlock-impl:
|
unit-test-padlock-impl:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- ./gradlew :padlock-impl:test
|
- ./gradlew :padlock-impl:test
|
||||||
|
|
||||||
|
unit-test-all:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- ./gradlew test
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
junit: build/test-results/test/**/TEST-*.xml
|
||||||
|
|
||||||
|
run-performance-analyze:
|
||||||
|
stage: analyze
|
||||||
|
needs:
|
||||||
|
- job: unit-test-all
|
||||||
|
script:
|
||||||
|
- ./gradlew runPerformanceAnalyze
|
||||||
|
- cat performance.txt
|
||||||
|
artifacts:
|
||||||
|
paths: ['performance.txt']
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleHome" value="" />
|
<option name="gradleJvm" value="azul-17" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
|
20
README.md
20
README.md
@ -48,6 +48,26 @@ You should design the proper architectures for your code.
|
|||||||
Last but not least, readability and maintainability are also important.
|
Last but not least, readability and maintainability are also important.
|
||||||
Take this project as a show off to your designing and coding skills.
|
Take this project as a show off to your designing and coding skills.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
This repo uses Gradle to build and test.
|
||||||
|
There is a unit test boilerplate and a gradle task configured to
|
||||||
|
automatically test and evaluate the code when you push your commits.
|
||||||
|
|
||||||
|
The `SolutionTestBase` is an abstract class for other tests.
|
||||||
|
It tests the correctness of your solution and don't care about the run time.
|
||||||
|
See `SolutionTest` for how to use that.
|
||||||
|
|
||||||
|
The `PerformanceAnalyze` is not a unit test, but it do analyze roughly how
|
||||||
|
fast your solution is. You need to fill in the `solve` method before you run it.
|
||||||
|
|
||||||
|
Use `./gradlew test` to run all unit test configured in the project,
|
||||||
|
and use `./gradlew runPerformanceTest` to get an analysis.
|
||||||
|
|
||||||
|
> Note: You don't have to have a local gradle installation.
|
||||||
|
> The `gradlew` script will download one for you.
|
||||||
|
> Just install a valid jdk (version >= 8) and very thing should be fine.
|
||||||
|
|
||||||
## Still have unclear problems?
|
## Still have unclear problems?
|
||||||
|
|
||||||
Feel free to contact Jeffrey Freeman (jeffrey.freeman@cleverthis.com).
|
Feel free to contact Jeffrey Freeman (jeffrey.freeman@cleverthis.com).
|
@ -1,3 +1,5 @@
|
|||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("java")
|
id("java")
|
||||||
}
|
}
|
||||||
@ -11,6 +13,7 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":padlock-impl"))
|
implementation(project(":padlock-impl"))
|
||||||
|
implementation("org.apache.commons", "commons-text", "1.11.0")
|
||||||
// if you ever need import more dependencies, following this format:
|
// if you ever need import more dependencies, following this format:
|
||||||
// implementation("group-id:project-id:version")
|
// implementation("group-id:project-id:version")
|
||||||
// just like the logback classic
|
// just like the logback classic
|
||||||
@ -27,4 +30,15 @@ java {
|
|||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
jvmArgs = listOf("-Dfast=true")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<JavaExec>("runPerformanceAnalyze")
|
||||||
|
tasks.named<JavaExec>("runPerformanceAnalyze") {
|
||||||
|
dependsOn("testClasses")
|
||||||
|
group = "verification"
|
||||||
|
classpath = sourceSets.test.get().runtimeClasspath
|
||||||
|
mainClass.set("com.cleverthis.interview.PerformanceAnalyze")
|
||||||
|
jvmArgs("-Dfast=true")
|
||||||
|
standardOutput = FileOutputStream("performance.txt")
|
||||||
}
|
}
|
@ -21,4 +21,5 @@ java {
|
|||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
jvmArgs = listOf("-Dfast=true")
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
import static com.cleverthis.interview.padlock.Utils.ensureSleep;
|
import static com.cleverthis.interview.padlock.Utils.ensureSleep;
|
||||||
|
|
||||||
@ -27,10 +28,12 @@ import static com.cleverthis.interview.padlock.Utils.ensureSleep;
|
|||||||
* After create, the input buffer is empty, you have to initialize.
|
* After create, the input buffer is empty, you have to initialize.
|
||||||
*/
|
*/
|
||||||
public class PadlockImpl {
|
public class PadlockImpl {
|
||||||
private final boolean debug;
|
|
||||||
private final int numpadSize;
|
private final int numpadSize;
|
||||||
private final Integer[] inputBuffer;
|
private final Integer[] inputBuffer;
|
||||||
private final Integer[] correctPasscode;
|
private final Integer[] correctPasscode;
|
||||||
|
// performance counter
|
||||||
|
private final AtomicLong writeCounter = new AtomicLong(0);
|
||||||
|
private final AtomicLong checkCounter = new AtomicLong(0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a padlock instance.
|
* Create a padlock instance.
|
||||||
@ -38,17 +41,6 @@ public class PadlockImpl {
|
|||||||
* @param numpadSize The number of buttons on the numpad of this lock.
|
* @param numpadSize The number of buttons on the numpad of this lock.
|
||||||
*/
|
*/
|
||||||
public PadlockImpl(int numpadSize) {
|
public PadlockImpl(int numpadSize) {
|
||||||
this(numpadSize, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a padlock instance.
|
|
||||||
*
|
|
||||||
* @param numpadSize The number of buttons on the numpad of this lock.
|
|
||||||
* @param debug Will skip sleep if is true
|
|
||||||
*/
|
|
||||||
PadlockImpl(int numpadSize, boolean debug) {
|
|
||||||
this.debug = debug;
|
|
||||||
if (numpadSize < 1) throw new IllegalArgumentException("numpadSize must be a positive number");
|
if (numpadSize < 1) throw new IllegalArgumentException("numpadSize must be a positive number");
|
||||||
this.numpadSize = numpadSize;
|
this.numpadSize = numpadSize;
|
||||||
this.inputBuffer = new Integer[numpadSize];
|
this.inputBuffer = new Integer[numpadSize];
|
||||||
@ -73,10 +65,11 @@ public class PadlockImpl {
|
|||||||
* @return The old value, null if not initialized.
|
* @return The old value, null if not initialized.
|
||||||
*/
|
*/
|
||||||
public synchronized Integer writeInputBuffer(int address, int keyIndex) {
|
public synchronized Integer writeInputBuffer(int address, int keyIndex) {
|
||||||
if (!debug) ensureSleep(1000);
|
ensureSleep(1000);
|
||||||
if (keyIndex < 0 || keyIndex >= numpadSize)
|
if (keyIndex < 0 || keyIndex >= numpadSize)
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"keyIndex out of range. Keypad size: " + numpadSize + ", keyIndex: " + keyIndex);
|
"keyIndex out of range. Keypad size: " + numpadSize + ", keyIndex: " + keyIndex);
|
||||||
|
writeCounter.incrementAndGet();
|
||||||
Integer oldValue = inputBuffer[address];
|
Integer oldValue = inputBuffer[address];
|
||||||
inputBuffer[address] = keyIndex;
|
inputBuffer[address] = keyIndex;
|
||||||
return oldValue;
|
return oldValue;
|
||||||
@ -98,10 +91,24 @@ public class PadlockImpl {
|
|||||||
"Passcode invalid: contain duplicated value. " + Arrays.toString(inputBuffer));
|
"Passcode invalid: contain duplicated value. " + Arrays.toString(inputBuffer));
|
||||||
uniqueTestArr[i] = true;
|
uniqueTestArr[i] = true;
|
||||||
}
|
}
|
||||||
|
checkCounter.incrementAndGet();
|
||||||
// if no exception, means:
|
// if no exception, means:
|
||||||
// every digit is unique, and every digit is initialized
|
// every digit is unique, and every digit is initialized
|
||||||
// aka this is a valid code
|
// aka this is a valid code
|
||||||
// now compare with our answer
|
// now compare with our answer
|
||||||
return Arrays.equals(correctPasscode, inputBuffer);
|
return Arrays.equals(correctPasscode, inputBuffer);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public long getWriteCounter() {
|
||||||
|
return writeCounter.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCheckCounter() {
|
||||||
|
return checkCounter.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetCounter() {
|
||||||
|
writeCounter.set(0);
|
||||||
|
checkCounter.set(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,12 +1,24 @@
|
|||||||
package com.cleverthis.interview.padlock;
|
package com.cleverthis.interview.padlock;
|
||||||
|
|
||||||
class Utils {
|
class Utils {
|
||||||
|
/**
|
||||||
|
* Check if the sleep is disabled.
|
||||||
|
* User can use `-Dfast=true` in the jvm args,
|
||||||
|
* or change it on the fly.
|
||||||
|
* Might waste sometime on checking this flag, but the effect should be minor.
|
||||||
|
* */
|
||||||
|
private static boolean shouldSkipSleep() {
|
||||||
|
return Boolean.parseBoolean(System.getProperty("fast"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure we will wait a given amount of time even if there are interruptions.
|
* Ensure we will wait a given amount of time even if there are interruptions.
|
||||||
|
* Property `-Dfast=true` can disable the sleep.
|
||||||
*
|
*
|
||||||
* @param millis The time you want to sleep, measure in millisecond.
|
* @param millis The time you want to sleep, measure in millisecond.
|
||||||
*/
|
*/
|
||||||
public static void ensureSleep(long millis) {
|
public static void ensureSleep(long millis) {
|
||||||
|
if (shouldSkipSleep()) return;
|
||||||
long endTime = System.currentTimeMillis() + millis;
|
long endTime = System.currentTimeMillis() + millis;
|
||||||
while (endTime > System.currentTimeMillis()) {
|
while (endTime > System.currentTimeMillis()) {
|
||||||
try {
|
try {
|
||||||
|
@ -22,7 +22,7 @@ class PadlockTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInstantiationRest() {
|
void testInstantiationRest() {
|
||||||
PadlockImpl padlock = new PadlockImpl(5, true);
|
PadlockImpl padlock = new PadlockImpl(5);
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
// ensure input buffer is uninitialized
|
// ensure input buffer is uninitialized
|
||||||
// should return null when first set
|
// should return null when first set
|
||||||
@ -34,7 +34,7 @@ class PadlockTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRejectInvalidInput() {
|
void testRejectInvalidInput() {
|
||||||
PadlockImpl padlock = new PadlockImpl(5, true);
|
PadlockImpl padlock = new PadlockImpl(5);
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
padlock.writeInputBuffer(i, i);
|
padlock.writeInputBuffer(i, i);
|
||||||
}
|
}
|
||||||
@ -54,7 +54,7 @@ class PadlockTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRejectInvalidInputBufferAddressAndValue() {
|
void testRejectInvalidInputBufferAddressAndValue() {
|
||||||
PadlockImpl padlock = new PadlockImpl(5, true);
|
PadlockImpl padlock = new PadlockImpl(5);
|
||||||
// test address
|
// test address
|
||||||
assertThrows(ArrayIndexOutOfBoundsException.class, () -> padlock.writeInputBuffer(-1, 1));
|
assertThrows(ArrayIndexOutOfBoundsException.class, () -> padlock.writeInputBuffer(-1, 1));
|
||||||
assertThrows(ArrayIndexOutOfBoundsException.class, () -> padlock.writeInputBuffer(-10, 1));
|
assertThrows(ArrayIndexOutOfBoundsException.class, () -> padlock.writeInputBuffer(-10, 1));
|
||||||
|
106
src/main/java/com/cleverthis/interview/DumbBruteSolver.java
Normal file
106
src/main/java/com/cleverthis/interview/DumbBruteSolver.java
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package com.cleverthis.interview;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Brute-forces padlock using lexicographically ordered permutation generation
|
||||||
|
*
|
||||||
|
* Algorithm documented at: https://en.wikipedia.org/wiki/Permutation#Generation_in_lexicographic_order
|
||||||
|
*/
|
||||||
|
public class DumbBruteSolver implements SolverInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solves the padlock passed in to the method. The padlock's internal state should be correct after this method
|
||||||
|
* runs.
|
||||||
|
* @param padlockAdapter A padlock conforming to the PadlockAdapter contract
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void solve(PadlockAdapter padlockAdapter) {
|
||||||
|
int numpadSize = padlockAdapter.getNumpadSize();
|
||||||
|
|
||||||
|
Integer[] currentPermutation = new Integer[numpadSize];
|
||||||
|
for(int i = 0; i < numpadSize; i++) {
|
||||||
|
currentPermutation[i] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
boolean isCorrect = checkPermutation(currentPermutation, padlockAdapter);
|
||||||
|
|
||||||
|
if (!isCorrect) {
|
||||||
|
boolean nextPermutationExists = calculateNextPermutation(currentPermutation, numpadSize);
|
||||||
|
if(!nextPermutationExists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the permutation to the padlock's memory and checks whether this permutation is the correct passcode.
|
||||||
|
* This is a naive solution that makes no considerations for write-cost.
|
||||||
|
* @param permutation The permutation to write to the padlock
|
||||||
|
* @param padlockAdapter The padlock to write to
|
||||||
|
* @return True if the correct padlock passcode has been found, false otherwise
|
||||||
|
*/
|
||||||
|
protected boolean checkPermutation(Integer[] permutation, PadlockAdapter padlockAdapter) {
|
||||||
|
for(int i = 0; i < padlockAdapter.getNumpadSize(); i++) {
|
||||||
|
padlockAdapter.writeInputBuffer(i, permutation[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return padlockAdapter.isPasscodeCorrect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the next permutation in lexicographic order, based on the algorithm linked on wikipedia
|
||||||
|
* @param currentPermutation The current permutation to run the algorithm on
|
||||||
|
* @param numpadSize The number of items in the set to be permuted
|
||||||
|
* @return true if next permutation successfully generated, false if permutations have been exhausted
|
||||||
|
*/
|
||||||
|
protected boolean calculateNextPermutation(Integer[] currentPermutation, int numpadSize) {
|
||||||
|
if(numpadSize < 2) { return false; }
|
||||||
|
|
||||||
|
//Integer k, l;
|
||||||
|
|
||||||
|
// Find the k and l indices, such that they meet the criteria for the permutation algorithm.
|
||||||
|
// If such indice values are found, swap them, then reverse the array subset from k+1 to the end of the array
|
||||||
|
for(int k = (numpadSize - 2); k >= 0; k--) {
|
||||||
|
if(currentPermutation[k] < currentPermutation[k + 1]) {
|
||||||
|
|
||||||
|
for(int l = (numpadSize - 1); l > k; l--) {
|
||||||
|
if(currentPermutation[k] < currentPermutation[l]) {
|
||||||
|
// Swap index k value and index l value in permutations array
|
||||||
|
// TODO: Could be a better swap algorithm
|
||||||
|
int tempInt = currentPermutation[k];
|
||||||
|
currentPermutation[k] = currentPermutation[l];
|
||||||
|
currentPermutation[l] = tempInt;
|
||||||
|
|
||||||
|
// Split the currentPermutation array into two slices. The slice happens at index k, with index k
|
||||||
|
// inclusive to the first slice
|
||||||
|
Integer[] firstSlice = Arrays.stream(currentPermutation, 0, k + 1).toArray(Integer[]::new);
|
||||||
|
Integer[] secondSlice = Arrays.stream(currentPermutation, k + 1, numpadSize).toArray(Integer[]::new);
|
||||||
|
|
||||||
|
// Reverse the subset of the permutation array from index k+1 to the end of the array
|
||||||
|
Collections.reverse(Arrays.asList(secondSlice));
|
||||||
|
|
||||||
|
// Concat the non-reversed and reversed subarrays into a new permutation
|
||||||
|
Integer[] newPermutation = Stream.concat(Arrays.stream(firstSlice), Arrays.stream(secondSlice)).toArray(Integer[]::new);
|
||||||
|
|
||||||
|
// Copy the new permutation into currentPermutation to return it
|
||||||
|
System.arraycopy(newPermutation, 0, currentPermutation, 0, numpadSize);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
package com.cleverthis.interview;
|
||||||
|
|
||||||
|
import org.apache.commons.text.similarity.LevenshteinDistance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper class that holds possible padlock passcode permutations in an integer array. This class
|
||||||
|
* overloads the toString() method and is comparable, using its string value to calculate the levenshtein distance and
|
||||||
|
* use those distance values for sorting into a data structure.
|
||||||
|
*/
|
||||||
|
public class IntegerLevenshtein implements Comparable<IntegerLevenshtein> {
|
||||||
|
private Integer[] integerData;
|
||||||
|
private Integer size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new IntegerLevenshtein
|
||||||
|
* @param size The number of integers (keypad size) of the passcode
|
||||||
|
*/
|
||||||
|
public IntegerLevenshtein(int size) {
|
||||||
|
this.integerData = new Integer[size];
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the integer data array
|
||||||
|
* @param integerData The new integer data
|
||||||
|
*/
|
||||||
|
public void setIntegerData(Integer[] integerData) {
|
||||||
|
this.integerData = integerData.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the integer data array
|
||||||
|
* @return the integer data
|
||||||
|
*/
|
||||||
|
public Integer[] getIntegerData() {
|
||||||
|
return this.integerData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Casts each integer to a string and concatenates them together into a single string.
|
||||||
|
* For example, an internal integer array of [1,2,3,4] will be returned as the string "1234".
|
||||||
|
* @return A string representation of the integer array.
|
||||||
|
*/
|
||||||
|
public String toString() {
|
||||||
|
String temp = new String();
|
||||||
|
for(int i = 0; i < size; i++) {
|
||||||
|
temp = temp.concat(integerData[i].toString());
|
||||||
|
}
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overriden compareTo method that compares the calculated levenshtein distance between two IntegerLevenshtein objects
|
||||||
|
* @param otherInteger the object to be compared.
|
||||||
|
* @return The levenshtein distance between the two objects.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int compareTo(IntegerLevenshtein otherInteger) {
|
||||||
|
LevenshteinDistance levenshtein = LevenshteinDistance.getDefaultInstance();
|
||||||
|
return levenshtein.apply(this.toString(), otherInteger.toString());
|
||||||
|
}
|
||||||
|
}
|
47
src/main/java/com/cleverthis/interview/PadlockAdapter.java
Normal file
47
src/main/java/com/cleverthis/interview/PadlockAdapter.java
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package com.cleverthis.interview;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This defines the interface that padlocks must conform to.
|
||||||
|
* Concrete implementations will be adapted to this interface contract through concrete adapter classes.
|
||||||
|
*/
|
||||||
|
public interface PadlockAdapter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of the padlock's physical number pad
|
||||||
|
*
|
||||||
|
* @return A count of the physical buttons on the padlock
|
||||||
|
*/
|
||||||
|
int getNumpadSize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write key presses to the input buffer of the padlock
|
||||||
|
* @param address The position / index of the button that is pressed. For example, address 0 is the first button pressed.
|
||||||
|
* @param keyIndex The value of the button that is pressed. Cannot be greater than the numpad size, as the buttons increment
|
||||||
|
* sequentially
|
||||||
|
* @return The old value of keyIndex at the inputted address
|
||||||
|
*/
|
||||||
|
Integer writeInputBuffer(int address, int keyIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the inputted password is correct
|
||||||
|
* @return True if password is correct, false otherwise
|
||||||
|
*/
|
||||||
|
boolean isPasscodeCorrect();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the write counter
|
||||||
|
* @return The number of times a write operation has occurred
|
||||||
|
*/
|
||||||
|
long getWriteCounter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the check counter
|
||||||
|
* @return The number of times the password has been checked for correctness
|
||||||
|
*/
|
||||||
|
long getCheckCounter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets both the check and write counters
|
||||||
|
*/
|
||||||
|
void resetCounter();
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package com.cleverthis.interview;
|
||||||
|
|
||||||
|
import com.cleverthis.interview.padlock.PadlockImpl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The concrete implementation of PadlockAdapter that communicates with the padlock directly through a Java
|
||||||
|
* class. This implementation also contains a cache that it keeps in sync with the underlying API, as a front-line
|
||||||
|
* optimization against unnecessary write operations.
|
||||||
|
*/
|
||||||
|
public class PadlockJavaAdapter extends PadlockImpl implements PadlockAdapter {
|
||||||
|
/**
|
||||||
|
* Intermediate cache between the underlying padlock API and any code that interfaces with the PadlockAdapter
|
||||||
|
* API.
|
||||||
|
*/
|
||||||
|
private final Integer[] inputBufferState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a padlock instance.
|
||||||
|
*
|
||||||
|
* @param numpadSize The number of buttons on the numpad of this lock.
|
||||||
|
*/
|
||||||
|
public PadlockJavaAdapter(int numpadSize) {
|
||||||
|
super(numpadSize);
|
||||||
|
|
||||||
|
inputBufferState = new Integer[numpadSize];
|
||||||
|
|
||||||
|
for(int i = 0; i < this.getNumpadSize(); i++) {
|
||||||
|
inputBufferState[i] = i;
|
||||||
|
super.writeInputBuffer(i, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes to the underlying input buffer, but only if the cache shows that the write is necessary
|
||||||
|
* @param address The position / index of the button that is pressed. For example, address 0 is the first button pressed.
|
||||||
|
* @param keyIndex The value of the button that is pressed. Cannot be greater than the numpad size, as the buttons increment
|
||||||
|
* sequentially
|
||||||
|
* @return The old value that was replaced, which is the same as keyIndex if a write operation doesn't actually occur.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Integer writeInputBuffer(int address, int keyIndex) {
|
||||||
|
if(inputBufferState[address] != keyIndex) {
|
||||||
|
inputBufferState[address] = keyIndex;
|
||||||
|
return super.writeInputBuffer(address, keyIndex);
|
||||||
|
} else {
|
||||||
|
return keyIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +0,0 @@
|
|||||||
package com.cleverthis.interview;
|
|
||||||
|
|
||||||
import com.cleverthis.interview.padlock.PadlockImpl;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a placeholder class showing a simple boilerplate.
|
|
||||||
* This class is not required, so you can replace with your own architecture.
|
|
||||||
*/
|
|
||||||
public class Solution {
|
|
||||||
public void solve(PadlockImpl padlock) {
|
|
||||||
throw new RuntimeException("TODO");
|
|
||||||
}
|
|
||||||
}
|
|
13
src/main/java/com/cleverthis/interview/SolverInterface.java
Normal file
13
src/main/java/com/cleverthis/interview/SolverInterface.java
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package com.cleverthis.interview;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that defines the class signature for solver implementations
|
||||||
|
*/
|
||||||
|
public interface SolverInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solves the padlock passed in that conforms to the PadlockAdapter interface
|
||||||
|
* @param padlockAdapter the padlock object to solve
|
||||||
|
*/
|
||||||
|
void solve(PadlockAdapter padlockAdapter);
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package com.cleverthis.interview;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import org.apache.commons.text.similarity.LevenshteinDistance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a write-aware brute solver implementation that uses the levenshtein distance to sort passcode permutations
|
||||||
|
* into a tree. Walking the tree in-order (Likely a depth-first traversal internally) results in a naive nearest-neighbor
|
||||||
|
* optimization that generally performs well, but may perform poorly in some cases. Heuristic solutions exist that have
|
||||||
|
* acceptable performance, and this is a more naive heuristic implementation.
|
||||||
|
*
|
||||||
|
* This permutation optimization problem is NP-hard and can be represented as the traveling salesperson problem.
|
||||||
|
* Representing passcode permutations as vertices and the levenshtein distance as edges in an undirected, weighted graph,
|
||||||
|
* an optimal solution is the shortest path satisfying a tour of the entire graph.
|
||||||
|
*
|
||||||
|
* A more advanced solution (based on Christofides, or another TSP heuristic) may result in more performance gains, but
|
||||||
|
* the performance of this nearest-neighbor solution is adequate for the keypad sizes under test. A keypad size of 9
|
||||||
|
* results in a graph of vertex-count 9!. It's unlikely performance gains from a more intelligent TSP solver would
|
||||||
|
* offset the performance cost of building a graph and optimizing traversal.
|
||||||
|
*/
|
||||||
|
public class WriteAwareBruteSolver extends DumbBruteSolver {
|
||||||
|
|
||||||
|
private final TreeSet<IntegerLevenshtein> orderedTree;
|
||||||
|
private Integer numpadSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an ordered tree on instantiation to be used as a cache for subsequent brute-force solves.
|
||||||
|
* @param numpadSize The size of the padlock's numpad, used in generation of the internal ordered tree
|
||||||
|
*/
|
||||||
|
public WriteAwareBruteSolver(int numpadSize) {
|
||||||
|
orderedTree = new TreeSet<IntegerLevenshtein>();
|
||||||
|
this.numpadSize = numpadSize;
|
||||||
|
|
||||||
|
this.createOrderedTree(numpadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solves the padlock passed in to the method. The padlock's internal state should be correct after this method
|
||||||
|
* runs.
|
||||||
|
* @param padlockAdapter A padlock conforming to the PadlockAdapter contract
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void solve(PadlockAdapter padlockAdapter) {
|
||||||
|
int padlockNumpadSize = padlockAdapter.getNumpadSize();
|
||||||
|
|
||||||
|
if (this.numpadSize != padlockNumpadSize) {
|
||||||
|
this.createOrderedTree(padlockNumpadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (IntegerLevenshtein integerLevenshtein : orderedTree) {
|
||||||
|
if (this.checkPermutation(integerLevenshtein.getIntegerData(), padlockAdapter)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the tree (the number of possible permutations)
|
||||||
|
* @return the size of the ordered tree
|
||||||
|
*/
|
||||||
|
public Integer getTreeSize() {
|
||||||
|
return this.orderedTree.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an ordered tree of possible passcode permutations
|
||||||
|
* @param size The number of keys on the numpad
|
||||||
|
*/
|
||||||
|
protected void createOrderedTree(int size) {
|
||||||
|
if(this.numpadSize != size) {
|
||||||
|
this.numpadSize = size;
|
||||||
|
orderedTree.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer[] currentPermutation = new Integer[numpadSize];
|
||||||
|
for(int i = 0; i < numpadSize; i++) {
|
||||||
|
currentPermutation[i] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean morePermutationsExist = true;
|
||||||
|
|
||||||
|
do {
|
||||||
|
IntegerLevenshtein levenshteinPermutation = new IntegerLevenshtein(numpadSize);
|
||||||
|
levenshteinPermutation.setIntegerData(currentPermutation);
|
||||||
|
orderedTree.add(levenshteinPermutation);
|
||||||
|
morePermutationsExist = this.calculateNextPermutation(currentPermutation, numpadSize);
|
||||||
|
} while(morePermutationsExist);
|
||||||
|
}
|
||||||
|
}
|
102
src/test/java/com/cleverthis/interview/DumbBruteSolverTest.java
Normal file
102
src/test/java/com/cleverthis/interview/DumbBruteSolverTest.java
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package com.cleverthis.interview;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for DumbBruteSolver class
|
||||||
|
*
|
||||||
|
* Tests that lexicographically ordered permutations are properly calculated, and that the solver works
|
||||||
|
*/
|
||||||
|
public class DumbBruteSolverTest extends SolutionTestBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overridden method that tests in SolutionTestBase.class expect defined and calls.
|
||||||
|
* Creates a DumbBruteSolver and calls the solve() method.
|
||||||
|
* @param padlock The padlock to solve
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void solve(PadlockAdapter padlock) {
|
||||||
|
new DumbBruteSolver().solve(padlock);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check whether a 7-button padlock can be solved
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
protected void testSolver() {
|
||||||
|
DumbBruteSolver dumbSolver = new DumbBruteSolver();
|
||||||
|
PadlockJavaAdapter padlock = new PadlockJavaAdapter(7);
|
||||||
|
|
||||||
|
dumbSolver.solve(padlock);
|
||||||
|
|
||||||
|
assertTrue(padlock.isPasscodeCorrect());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the next permutation is calculated correctly
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
protected void testOnePermutation() {
|
||||||
|
DumbBruteSolver dumbSolver = new DumbBruteSolver();
|
||||||
|
|
||||||
|
Integer[] permutation = new Integer[] {1, 2, 3, 4};
|
||||||
|
Integer[] correctPermutation = new Integer[] {1, 2, 4, 3};
|
||||||
|
|
||||||
|
dumbSolver.calculateNextPermutation(permutation, 4);
|
||||||
|
|
||||||
|
assertArrayEquals(permutation, correctPermutation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether two consecutive permutations are correct
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
protected void testTwoPermutations() {
|
||||||
|
DumbBruteSolver dumbSolver = new DumbBruteSolver();
|
||||||
|
|
||||||
|
Integer[] permutation = new Integer[] {1, 2, 3, 4};
|
||||||
|
Integer[] correctPermutation = new Integer[] {1, 3, 2, 4};
|
||||||
|
|
||||||
|
for(int i = 0; i < 2; i++) {
|
||||||
|
dumbSolver.calculateNextPermutation(permutation, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertArrayEquals(permutation, correctPermutation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether 23 consecutive permutations are correct
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
protected void test23Permutations() {
|
||||||
|
DumbBruteSolver dumbSolver = new DumbBruteSolver();
|
||||||
|
|
||||||
|
Integer[] permutation = new Integer[] {1, 2, 3, 4};
|
||||||
|
Integer[] correctPermutation = new Integer[] {4, 3, 2, 1};
|
||||||
|
|
||||||
|
for(int i = 0; i < 23; i++) {
|
||||||
|
dumbSolver.calculateNextPermutation(permutation, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertArrayEquals(permutation, correctPermutation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the 24th permutation returns false, signifying we've exhausted all possible permutations
|
||||||
|
* for a list of size 4
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
protected void test24Permutations() {
|
||||||
|
DumbBruteSolver dumbSolver = new DumbBruteSolver();
|
||||||
|
|
||||||
|
Integer[] permutation = new Integer[] {1, 2, 3, 4};
|
||||||
|
|
||||||
|
for(int i = 0; i < 23; i++) {
|
||||||
|
assertTrue(dumbSolver.calculateNextPermutation(permutation, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(dumbSolver.calculateNextPermutation(permutation, 4));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package com.cleverthis.interview;
|
||||||
|
|
||||||
|
import com.cleverthis.interview.padlock.PadlockImpl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performance test but not mean to run in unit test.
|
||||||
|
*/
|
||||||
|
public class PerformanceAnalyze {
|
||||||
|
|
||||||
|
private static final int TOTAL_RUN = 500;
|
||||||
|
private static final int NUMPAD_SIZE = 9;
|
||||||
|
/**
|
||||||
|
* The solver object reference is held between tests, because the data structure used for optimization does not
|
||||||
|
* change between runs and keeping it in memory greatly increases performance.
|
||||||
|
*/
|
||||||
|
static WriteAwareBruteSolver solver = new WriteAwareBruteSolver(NUMPAD_SIZE);
|
||||||
|
|
||||||
|
private static void solve(PadlockAdapter padlock) {
|
||||||
|
solver.solve(padlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
System.out.println("Total run: " + TOTAL_RUN);
|
||||||
|
System.out.println("Numpad size: " + NUMPAD_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
long timeSum = 0;
|
||||||
|
long writeSum = 0;
|
||||||
|
for (int i = 0; i < TOTAL_RUN; i++) {
|
||||||
|
PadlockAdapter padlock = new PadlockJavaAdapter(NUMPAD_SIZE);
|
||||||
|
padlock.resetCounter();
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
solve((PadlockAdapter) padlock);
|
||||||
|
long end = System.currentTimeMillis();
|
||||||
|
if (!padlock.isPasscodeCorrect()) throw new IllegalStateException(
|
||||||
|
"Invalid solution: passcode not correct after return");
|
||||||
|
long dT = end - start;
|
||||||
|
timeSum += dT;
|
||||||
|
writeSum += padlock.getWriteCounter();
|
||||||
|
System.out.println("Run #" + (i + 1) + ": time: " + dT + "ms; write: " + padlock.getWriteCounter());
|
||||||
|
}
|
||||||
|
System.out.println("Run time sum: " + timeSum + "ms");
|
||||||
|
System.out.println("Write sum: " + writeSum);
|
||||||
|
double avgTime = timeSum / (double) TOTAL_RUN;
|
||||||
|
double avgWrite = writeSum / (double) TOTAL_RUN;
|
||||||
|
System.out.println("Avg run time: " + avgTime + "ms");
|
||||||
|
System.out.println("Avg write: " + avgWrite);
|
||||||
|
System.out.println("Calculated estimate avg run time: " + (avgTime / 1000 + avgTime) + "s");
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +0,0 @@
|
|||||||
package com.cleverthis.interview;
|
|
||||||
|
|
||||||
import com.cleverthis.interview.padlock.PadlockImpl;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a simple placeholder to show how unit test works.
|
|
||||||
* You can replace it with your own test.
|
|
||||||
*/
|
|
||||||
class SolutionTest {
|
|
||||||
private void solve(PadlockImpl padlock) {
|
|
||||||
new Solution().solve(padlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void verify(){
|
|
||||||
Random random = new Random();
|
|
||||||
PadlockImpl padlock = new PadlockImpl(random.nextInt(1, 8));
|
|
||||||
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
solve(padlock);
|
|
||||||
long endTime = System.currentTimeMillis();
|
|
||||||
assertTrue(padlock.isPasscodeCorrect());
|
|
||||||
System.out.println("Time usage: " + (endTime - startTime) + "ms");
|
|
||||||
}
|
|
||||||
}
|
|
34
src/test/java/com/cleverthis/interview/SolutionTestBase.java
Normal file
34
src/test/java/com/cleverthis/interview/SolutionTestBase.java
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package com.cleverthis.interview;
|
||||||
|
|
||||||
|
import com.cleverthis.interview.padlock.PadlockImpl;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a base class for verifying the correctness of the solution.
|
||||||
|
*/
|
||||||
|
public abstract class SolutionTestBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement your solution in this function.
|
||||||
|
* */
|
||||||
|
protected abstract void solve(PadlockAdapter padlock);
|
||||||
|
|
||||||
|
protected void verify(int numpadSize) {
|
||||||
|
PadlockAdapter padlock = new PadlockJavaAdapter(numpadSize);
|
||||||
|
solve(padlock);
|
||||||
|
assertTrue(padlock.isPasscodeCorrect());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests padlocks with numpad sizes of 1 to 7. This test runs for every class that extends this SolutionTestBase
|
||||||
|
* abstract class.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void verify1to7() {
|
||||||
|
for (int i = 1; i <= 7; i++) {
|
||||||
|
verify(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package com.cleverthis.interview;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for WriteAwareBruteSolver class
|
||||||
|
*
|
||||||
|
* These tests do not keep a long-lived reference to WriteAwareBruteSolver, instead opting
|
||||||
|
* to reconstruct the permutation tree for every test. This is a trade-off between performance
|
||||||
|
* and avoiding any inadvertent behavior changes from state persisting between tests.
|
||||||
|
*/
|
||||||
|
public class WriteAwareBruteSolverTest extends SolutionTestBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overridden method that tests in SolutionTestBase.class expect defined and calls.
|
||||||
|
* Creates a WriteAwareBruteSolver and calls the solve() method.
|
||||||
|
* @param padlock The padlock to solve
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void solve(PadlockAdapter padlock) {
|
||||||
|
new WriteAwareBruteSolver(padlock.getNumpadSize()).solve(padlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether the solver can brute-force a 7-numpad padlock.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
protected void testSolver() {
|
||||||
|
Integer numpadSize = 7;
|
||||||
|
WriteAwareBruteSolver writeAwareSolver = new WriteAwareBruteSolver(numpadSize);
|
||||||
|
PadlockJavaAdapter padlock = new PadlockJavaAdapter(numpadSize);
|
||||||
|
|
||||||
|
writeAwareSolver.solve(padlock);
|
||||||
|
|
||||||
|
assertTrue(padlock.isPasscodeCorrect());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether a numpad-size of 4 is correctly expanded to 24 possible permutations in the underlying
|
||||||
|
* tree
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
protected void testTreeSize() {
|
||||||
|
Integer numpadSize = 4;
|
||||||
|
WriteAwareBruteSolver writeAwareSolver = new WriteAwareBruteSolver(numpadSize);
|
||||||
|
PadlockJavaAdapter padlock = new PadlockJavaAdapter(4);
|
||||||
|
|
||||||
|
writeAwareSolver.solve(padlock);
|
||||||
|
|
||||||
|
assertEquals(writeAwareSolver.getTreeSize(), 24);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user