diff --git a/pom.xml b/pom.xml index bad3271..c10843b 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,11 @@ + + org.springframework.boot + spring-boot-starter-web + + org.junit.jupiter junit-jupiter diff --git a/src/main/java/assessment/Application.java b/src/main/java/assessment/Application.java new file mode 100644 index 0000000..e6fe20e --- /dev/null +++ b/src/main/java/assessment/Application.java @@ -0,0 +1,12 @@ +package assessment; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/src/main/java/assessment/Grid.java b/src/main/java/assessment/algorithm/Grid.java similarity index 55% rename from src/main/java/assessment/Grid.java rename to src/main/java/assessment/algorithm/Grid.java index 59629c1..236994c 100644 --- a/src/main/java/assessment/Grid.java +++ b/src/main/java/assessment/algorithm/Grid.java @@ -1,4 +1,4 @@ -package assessment; +package assessment.algorithm; import java.io.BufferedReader; import java.io.IOException; @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; public record Grid(List> grid) { + private static final double timeValueFactor = .2; public static Grid fromFile(String resource) { try { @@ -27,12 +28,30 @@ public record Grid(List> grid) { } } - public int get(int r, int c){ - return grid.get(r).get(c); + public int getInitialValue(int x, int y){ + return grid.get(y).get(x); } //assumes square public int getWidth(){ return grid.size(); } + + /** + * de waarde van een punt x,y wordt bepaald door de beginwaarde, tenzij we er al geweest zijn + * dan telt de tijd sinds we er geweest zijn = afstand in sindsdien afgelegd pad + * de waarde is rechtevenredig met de afstand + */ + public double getCurrentValue(Path path, int x, int y) { + int gridValue = getInitialValue(x, y); + if (path.hasPoint(this, x, y)) { + // been there + int distanceInPath = path.getDistanceInPath(x, y) - 1; + double increment = gridValue * timeValueFactor; + + return Math.min((distanceInPath - 1) * increment, gridValue); + } else { + return gridValue; + } + } } diff --git a/src/main/java/assessment/BestPathFinder.java b/src/main/java/assessment/algorithm/OptimalPathFinder.java similarity index 63% rename from src/main/java/assessment/BestPathFinder.java rename to src/main/java/assessment/algorithm/OptimalPathFinder.java index 39bc187..4b9e822 100644 --- a/src/main/java/assessment/BestPathFinder.java +++ b/src/main/java/assessment/algorithm/OptimalPathFinder.java @@ -1,10 +1,16 @@ -package assessment; +package assessment.algorithm; import java.util.*; -//TODO make non static (CH) -public class BestPathFinder { - private static final double timeValueFactor = .2; +/** + * Finds most valuable path given distance/time covered (t) and elapsed compute time (T) + * for a drone moving through a square grid of size N that has cells that have varying value + *

+ * outstanding questions: + * * is a cell of (initial) value 0 equally valuable as a cell just occupied? + * -> probably, what is the chance that you find a 'treasure' behind it, that you would have not found via an alternative path? + */ +public class OptimalPathFinder { // paths to be considered private final PriorityQueue paths = new PriorityQueue<>(); @@ -20,8 +26,8 @@ public class BestPathFinder { * @param y startpositie Y * @return het meest waardevolle pad */ - public Path findMaxValPath(Grid g, int N, int t, int T, int x, int y) { - Path path = Path.newPath(g, x, y); + public Path findOptimalPath(Grid g, int N, int t, long T, int x, int y) { + Path path = Path.newPath(g, Point.create(g, x, y)); paths.add(path); // overall best path Path max = path; @@ -56,27 +62,27 @@ public class BestPathFinder { // find best new directions List newDirections = new ArrayList<>(); if (y > 0) { - newDirections.add(new Point(x, y - 1, getValueFromGrid(g, path, x, y - 1))); + newDirections.add(Point.create(g, path, x, y - 1)); if (x < N - 1) { - newDirections.add(new Point(x + 1, y - 1, getValueFromGrid(g, path, x + 1, y - 1))); + newDirections.add(Point.create(g, path, x + 1, y - 1)); } } if (x > 0) { - newDirections.add(new Point(x - 1, y, getValueFromGrid(g, path, x - 1, y))); + newDirections.add(Point.create(g, path, x - 1, y)); if (y > 0) { - newDirections.add(new Point(x - 1, y - 1, getValueFromGrid(g, path, x - 1, y - 1))); + newDirections.add(Point.create(g, path, x - 1, y - 1)); } } if (x < N - 1) { - newDirections.add(new Point(x + 1, y, getValueFromGrid(g, path, x + 1, y))); + newDirections.add(Point.create(g, path, x + 1, y)); if (y < N - 1) { - newDirections.add(new Point(x + 1, y + 1, getValueFromGrid(g, path, x + 1, y + 1))); + newDirections.add(Point.create(g, path, x + 1, y + 1)); } } if (y < N - 1) { - newDirections.add(new Point(x, y + 1, getValueFromGrid(g, path, x, y + 1))); + newDirections.add(Point.create(g, path, x, y + 1)); if (x > 0) - newDirections.add(new Point(x - 1, y + 1, getValueFromGrid(g, path, x - 1, y + 1))); + newDirections.add(Point.create(g, path, x - 1, y + 1)); } if (!newDirections.isEmpty()) { @@ -98,7 +104,7 @@ public class BestPathFinder { } } if (!pointsAdded) { - //evict + // dead end, evict Path ended = paths.poll(); if (ended != null && ended.value() > max.value()) { max = ended; @@ -108,22 +114,4 @@ public class BestPathFinder { } return max; } - - /** - * de waarde van een punt x,y wordt bepaald door de beginwaarde, tenzij we er al geweest zijn - * dan telt de tijd sinds we er geweest zijn = afstand in sindsdien afgelegd pad - * de waarde is rechtevenredig met de afstand - */ - private double getValueFromGrid(Grid grid, Path path, int x, int y) { - int gridValue = grid.get(x, y); - if (path.hasPoint(grid, x, y)) { - // been there - int distanceInPath = path.getDistanceInPath(x, y); - double increment = gridValue * timeValueFactor; - - return Math.min((distanceInPath - 1) * increment, gridValue); - } else { - return gridValue; - } - } } diff --git a/src/main/java/assessment/Path.java b/src/main/java/assessment/algorithm/Path.java similarity index 88% rename from src/main/java/assessment/Path.java rename to src/main/java/assessment/algorithm/Path.java index e12557b..dedbeae 100644 --- a/src/main/java/assessment/Path.java +++ b/src/main/java/assessment/algorithm/Path.java @@ -1,4 +1,4 @@ -package assessment; +package assessment.algorithm; import java.util.ArrayList; import java.util.HashSet; @@ -8,7 +8,7 @@ public class Path implements Comparable { // beide bevatten de al afgelegde punten // points is bedoeld om door de punten te lopen - private final ArrayList points = new ArrayList<>(); + public final ArrayList points = new ArrayList<>(); // trodden is bedoeld om zo snel mogelijk vast te stellen of we al op het punt geweest zijn private final HashSet trodden = new HashSet<>(); @@ -17,9 +17,9 @@ public class Path implements Comparable { } // meh row/col vs x/y - static Path newPath(Grid g, int x, int y) { + public static Path newPath(Grid g, Point start) { Path p = new Path(); - p.add(g, new Point(x, y, g.get(y, x))); + p.add(g, start); return p; } @@ -28,14 +28,14 @@ public class Path implements Comparable { trodden.add(point.y * g.getWidth() + point.x); } - public Double value() { + public double value() { return points.stream().mapToDouble(point -> point.value).sum(); } // compare descending, highest value first @Override public int compareTo(Path o) { - return -this.value().compareTo(o.value()); + return -Double.compare(this.value(), o.value()); } public int length() { diff --git a/src/main/java/assessment/Point.java b/src/main/java/assessment/algorithm/Point.java similarity index 58% rename from src/main/java/assessment/Point.java rename to src/main/java/assessment/algorithm/Point.java index c474730..3163053 100644 --- a/src/main/java/assessment/Point.java +++ b/src/main/java/assessment/algorithm/Point.java @@ -1,17 +1,28 @@ -package assessment; +package assessment.algorithm; + +import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Objects; + public class Point implements Comparable { - final int x; - final int y; - final double value; + public final int x; + public final int y; + public double value; - public Point(int x, int y, double value) { + private Point(int x, int y, double initialValue) { this.x = x; this.y = y; - this.value = value; + this.value = initialValue; + } + + public static Point create(Grid g, int x, int y) { + return new Point(x, y, g.getInitialValue(x, y)); + } + + public static Point create(Grid g, Path p, int x, int y) { + return new Point(x, y, g.getCurrentValue(p, x, y)); } @Override @@ -37,4 +48,5 @@ public class Point implements Comparable { public int hashCode() { return Objects.hash(x, y); } + } diff --git a/src/main/java/assessment/restapi/FlightPathApi.java b/src/main/java/assessment/restapi/FlightPathApi.java new file mode 100644 index 0000000..01b41b5 --- /dev/null +++ b/src/main/java/assessment/restapi/FlightPathApi.java @@ -0,0 +1,29 @@ +package assessment.restapi; + +import assessment.algorithm.OptimalPathFinder; +import assessment.algorithm.Grid; +import assessment.algorithm.Path; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class FlightPathApi { + + @GetMapping(path = "/api/path/{gridname}/{pathlength}/{maxMillis}/{x}/{y}") + public PathDto getOptimalPath(@PathVariable String gridname, + @PathVariable int pathlength, + @PathVariable long maxMillis, + @PathVariable int x, + @PathVariable int y) { + Grid grid = Grid.fromFile("grids/" + gridname + ".txt"); + Path p = new OptimalPathFinder().findOptimalPath(grid, grid.getWidth(), pathlength, maxMillis, x, y); + return new PathDto(p); + } + + @GetMapping(path = "/api/grid/{gridname}") + public GridDto getGrid(@PathVariable String gridname) { + Grid grid = Grid.fromFile("grids/" + gridname + ".txt"); + return new GridDto(grid); + } +} diff --git a/src/main/java/assessment/restapi/GridDto.java b/src/main/java/assessment/restapi/GridDto.java new file mode 100644 index 0000000..5aab82d --- /dev/null +++ b/src/main/java/assessment/restapi/GridDto.java @@ -0,0 +1,23 @@ +package assessment.restapi; + +import assessment.algorithm.Grid; + +import java.util.List; + +public class GridDto { + private List> grid; + private int size; + + public GridDto(Grid grid) { + this.grid = grid.grid(); + this.size = grid.getWidth(); + } + + public List> getGrid() { + return grid; + } + + public int getSize() { + return size; + } +} diff --git a/src/main/java/assessment/restapi/PathDto.java b/src/main/java/assessment/restapi/PathDto.java new file mode 100644 index 0000000..0a84e39 --- /dev/null +++ b/src/main/java/assessment/restapi/PathDto.java @@ -0,0 +1,25 @@ +package assessment.restapi; + +import assessment.algorithm.Path; + +import java.util.List; +import java.util.stream.Collectors; + +public class PathDto { + private final List points; + private final double value; + + public PathDto(Path p){ + this.points = p.points.stream().map(PointDto::new).collect(Collectors.toList()); + this.value = p.value(); + } + + public List getPoints() { + return points; + } + + public double getValue() { + return value; + } + +} diff --git a/src/main/java/assessment/restapi/PointDto.java b/src/main/java/assessment/restapi/PointDto.java new file mode 100644 index 0000000..94af040 --- /dev/null +++ b/src/main/java/assessment/restapi/PointDto.java @@ -0,0 +1,22 @@ +package assessment.restapi; + +import assessment.algorithm.Point; + +public class PointDto { + private final int x; + private final int y; + + public PointDto(Point p) { + this.x = p.x; + this.y = p.y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + +} diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css new file mode 100644 index 0000000..afa713f --- /dev/null +++ b/src/main/resources/static/css/style.css @@ -0,0 +1,62 @@ +body{ + background-color: green; +} + +canvas { + width: 100vw; + height: 100vh; +} + +#console { + font: 13px Arial, sans-serif; + padding: 5px; + color: greenyellow; + background: black; + position: absolute; + right: 10px; + bottom: 0; + width: 20%; + height: 10em; + border: 2px solid darkgray; + border-radius: 10px; + z-index: 10; +} + +#prompt { + position: absolute; + bottom: 0; + width: 90%; +} + +#command_input { + background: black; + color: greenyellow; + outline: none; + width: 90%; + height: 1em; + resize: none; +} + +#command_history { + font-size: 12px; + color: greenyellow; + position: absolute; + bottom: 1.5em; + max-height: 20em; + width: 100%; + overflow-y: visible; + overflow-x: hidden; +} + +.multiline { + border-top: 1px slategray solid; + border-left: 1px slategray solid; + border-right: 1px slategray solid; + border-bottom: none transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} + +.single_line { + border: none transparent; +} \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..0167365 --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,24 @@ + + + + OptimalPathFinder + + + + + +

+
+
+
+ +
+ + + + + + + \ No newline at end of file diff --git a/src/main/resources/static/js/console.js b/src/main/resources/static/js/console.js new file mode 100644 index 0000000..304fa16 --- /dev/null +++ b/src/main/resources/static/js/console.js @@ -0,0 +1,596 @@ +const command_input_element = document.getElementById('command_input'); +const command_history_element = document.getElementById('command_history'); +let command_history = ['']; +let command_history_index = 0; +let token_index = 0; +const bindings = {}; +const state = {}; +const keywords = { + 'true': true, + 'false': false, + 'pi': Math.PI, + 'PI': Math.PI, + 'e': Math.E +} + +let tokens; + +const adjust_input_element_height = function () { + let num_lines = command_input_element.value.split(/\n/).length; + command_input_element.setAttribute('style', 'height: ' + num_lines + 'em'); + if (num_lines > 1) { + command_input_element.setAttribute('class', 'multiline'); + } else { + command_input_element.setAttribute('class', 'single_line'); + } +} + +// command_input_element.onkeypress = function handle_key_input(event) { +// if (event.key === 'Enter') { +// event.preventDefault(); +// } +// } + +command_input_element.onkeyup = function handle_key_input(event) { + adjust_input_element_height(); + if (event.key === 'c' && event.ctrlKey) { + command_input_element.value = ''; + } + if (event.key === 'ArrowUp' && !event.shiftKey) { + if (command_history_index > -1) { + command_input_element.value = command_history[command_history_index]; + if (command_history_index > 0) { + command_history_index -= 1; + } + } + } + if (event.key === 'ArrowDown' && !event.shiftKey) { + if (command_history_index < command_history.length - 1) { + command_history_index += 1; + command_input_element.value = command_history[command_history_index]; + } else { + command_input_element.value = ''; + } + } + if (event.key === 'Enter') { + handle_enter(); + } +}; + +const handle_enter = function () { + let command = command_input_element.value.trim(); + command_input_element.value = ''; + adjust_input_element_height(); + + if (command.length > 0) { + command_history_element.innerText += command + "\n"; + command_input_element.value = ''; + command_history_index = command_history.length; + + scan(command); + let statement = parse(); + let value = evaluate(statement); + + if (value !== undefined) { + let binding; + if (value.is_binding) { // if it's declaration work with the initializer + binding = value.name; // but we also need the name of the bound variable + value = state[value.name]; // lookup the value for the binding + } + + // if (value.is_visual) { + // + // } else { + // if (binding && bindings[binding].previous && bindings[binding].previous.is_visual) { + // label(bindings[binding].previous, '@' + bindings[binding].previous.id); + // } + // } + // if (value.description) { + // value = value.description; + // } + command_history_element.innerText += value.toString() + "\n"; + command_history.push(command); + command_history_element.scrollTo(0, command_history_element.scrollHeight); + } + + } +} + +const evaluate = function (expr) { + switch (expr.type) { + case 'declaration': { + let value = evaluate(expr.initializer); + let binding_name = expr.var_name.value; + bindings[binding_name] = { + is_binding: true, + name: binding_name, + }; + state[binding_name] = value; // assign new value to binding + + return bindings[binding_name]; // don't return the value itself, but the binding_object + } // with which you can lookup the value + + case 'group': // expression within parentheses + return evaluate(expr.expression); + case 'unary': { + let right_operand = evaluate(expr.right); + if (expr.operator === token_types.MINUS) { + return -right_operand; //TODO create negate function (because now it only works for numbers) + } else if (expr.operator === token_types.NOT) { + return !right_operand; + } else { + throw {message: 'illegal unary operator'}; + } + } + case 'binary': { + switch (expr.operator) { + case token_types.MINUS: + return subtraction(evaluate(expr.left), evaluate(expr.right)); + case token_types.PLUS: + return add(evaluate(expr.left), evaluate(expr.right)); + case token_types.STAR: + return multiply(evaluate(expr.left), evaluate(expr.right)); + case token_types.SLASH: + return division(evaluate(expr.left), evaluate(expr.right)); + case token_types.EQUALS_EQUALS: + return test_equal(evaluate(expr.left), evaluate(expr.right)); + case token_types.AND: + return logical_and(evaluate(expr.left), evaluate(expr.right)); + case token_types.OR: + return logical_or(evaluate(expr.left), evaluate(expr.right)); + } + throw {message: 'illegal binary operator'}; + } + case 'identifier': { + if (expr.name in keywords) { + return keywords[expr.name]; + } else { + if (state[expr.name]) { + return state[expr.name]; + } else { + return undefined; + } + } + } + case 'literal': { + return expr.value; + } + } +} + +function parse() { + token_index = 0; + if (check(token_types.IDENTIFIER, token_index) && check(token_types.EQUALS, token_index + 1)) { + let var_name = current_token(); + advance(); + advance(); + return {type: 'declaration', var_name: var_name, initializer: expression()}; + } else { + return expression(); + } +} + +function expression() { + return equality(); +} + +function equality() { + let expr = comparison() + + while (match([token_types.EQUALS_EQUALS, token_types.NOT_EQUALS])) { + let operator = previous_token(); + let right = unary(); + expr = {type: 'binary', left: expr, operator: operator, right: right}; + } + + return expr; +} + +function comparison() { + let expr = add_sub(); + + while (match([token_types.LESS, token_types.LESS_OR_EQUAL, token_types.GREATER, token_types.GREATER_OR_EQUAL])) { + let operator = previous_token(); + let right = add_sub(); + expr = {type: 'binary', left: expr, operator: operator, right: right}; + } + + return expr; +} + +function add_sub() { + let expr = mult_div(); + + while (match([token_types.OR, token_types.MINUS, token_types.PLUS])) { + let operator = previous_token(); + let right = mult_div(); + expr = {type: 'binary', left: expr, operator: operator, right: right}; + } + + return expr; +} + +function mult_div() { + let expr = unary(); + + while (match([token_types.AND, token_types.SLASH, token_types.STAR, token_types.DOT])) { + let operator = previous_token(); + let right = unary(); + expr = {type: 'binary', left: expr, operator: operator, right: right}; + } + + return expr; +} + +function unary() { + if (match([token_types.NOT, token_types.MINUS])) { + let operator = previous_token(); + let right = unary(); + return {type: 'unary', operator: operator, right: right}; + } else { + return primary(); + } +} + +function primary() { + if (match([token_types.NUMERIC, token_types.STRING])) { + return {type: 'literal', value: previous_token().value, value_type: previous_token().type}; + } else if (match([token_types.LEFT_PAREN])) { + let expr = expression(); + if (expr && match([token_types.RIGHT_PAREN])) { + return { + type: 'group', + expression: expr + }; + } else { + throw {message: 'expected expression or )'}; + } + } else if (check(token_types.IDENTIFIER, token_index)) { + let identifier = { + type: 'identifier', + name: current_token().value + }; + advance(); + return identifier; + } else if (match([token_types.LEFT_BRACKET])) { + let array = []; + if (!check(token_types.RIGHT_BRACKET, token_index)) { + let result; + do { + result = expression(); + if (result) { + array.push(result); + } else { + throw {message: "Expect ']' after array elements."}; + } + match([token_types.COMMA]); + } while (!match([token_types.RIGHT_BRACKET])); + } + + return {type: 'array', elements: array}; + + } +} + +/** + * matches token against array of tokens to check for equality (matching type) + * @param tokens_to_match array of tokens + * @returns {boolean} + */ +function match(tokens_to_match) { + for (let i = 0; i < tokens_to_match.length; i++) { + if (are_same(tokens_to_match[i], current_token())) { + advance(); + return true; + } + } + return false; +} + +/** + * Checks if token at position index matches the given + * @param token_to_check expected token type + * @param index of token to check + * @returns {boolean} + */ +function check(token_to_check, index) { + let token = tokens[index]; + if (!token) { + return false; + } + return are_same(token_to_check, token); + +} + +/** + * checks if 2 tokens have same type + * @param token_1 + * @param token_2 + * @returns {boolean} + */ +function are_same(token_1, token_2) { + if (is_at_end()) { + return false; + } else { + return token_1.type === token_2.type; + } + +} + +function is_at_end() { + return token_index >= tokens.length; +} + +function advance() { + token_index += 1; +} + +function previous_token() { + return tokens[token_index - 1]; +} + +function current_token() { + return tokens[token_index]; +} + + +/** + * Creates an array of tokens from a line of input. + * + * @returns {token_types[]} + * @param command + */ +const scan = function (command) { + tokens = []; + let current_index = 0, // current index of char to look at in the command string + word_start_index = 0; // marker for start of a literal or identifier + + while (!is_at_end()) { + word_start_index = current_index; + let token = scan_token(); + if (token) { // undefined mostly means whitespace + tokens.push(token); + } + } + + function scan_token() { + let next_char = advance(); + switch (next_char) { + case '(': + return token_types.LEFT_PAREN; + case ')': + return token_types.RIGHT_PAREN; + case '[': + return token_types.LEFT_BRACKET; + case ']': + return token_types.RIGHT_BRACKET; + case ',': + return token_types.COMMA; + case '.': + return token_types.DOT; + case '-': + return token_types.MINUS; + case '+': + return token_types.PLUS; + case '*': + return token_types.STAR; + case '/': + return token_types.SLASH; + case '>': + if (expect('=')) { + return token_types.GREATER_OR_EQUAL; + } else { + return token_types.GREATER; + } + case '<': + if (expect('=')) { + return token_types.LESS_OR_EQUAL; + } else { + return token_types.LESS; + } + case '!': + if (expect('=')) { + return token_types.NOT_EQUALS; + } else { + return token_types.NOT; + } + case '=': + if (expect('=')) { + return token_types.EQUALS_EQUALS; + } else { + return token_types.EQUALS; + } + case '&': + return token_types.AND; + case '|': + return token_types.OR; + case '\'': + return string(); + case ' ': + case '\t': + case '\r': + advance(); + break; + } + if (is_digit(next_char)) { + let token = Object.assign({}, token_types.NUMERIC); + token.value = parse_number(); + return token; + } else { + if (is_alpha_or_underscore(next_char)) { + let token = Object.assign({}, token_types.IDENTIFIER); + token.value = parse_identifier(); + return token; + } + } + } + + function expect(expected_char) { + if (is_at_end()) { + return false; + } + if (current_char() === expected_char) { + advance(); + return true; + } else { + return false; + } + } + + function advance() { + if (current_index < command.length) { + current_index += 1; + } + return command[current_index - 1]; + } + + function is_at_end() { + return current_index >= command.length; + } + + function current_char() { + return command[current_index]; + } + + function is_digit(char) { + return char >= '0' && char <= '9'; + } + + function is_part_of_number(char) { + return is_digit(char) || char === '.'; // no scientific notation for now + } + + function parse_reference() { + while (current_char() === '@' || is_digit(current_char())) { + advance(); + } + return command.substring(word_start_index, current_index); + } + + function parse_number() { + while (is_part_of_number(current_char())) { + advance(); + } + let number_string = command.substring(word_start_index, current_index); + return Number.parseFloat(number_string); + } + + function is_alpha_or_underscore(char) { + return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char === '_'; + } + + function is_alphanumeric_or_underscore(char) { + return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || is_digit(char) || char === '_'; + } + + function parse_identifier() { + while (is_alphanumeric_or_underscore(current_char())) { + advance(); + } + return command.substring(word_start_index, current_index); + } + + function string() { // as of yet strings may not unclude escaped quotes that are also the start/end quote + while (current_char() !== '\'' && !is_at_end()) { + advance(); + } + if (is_at_end() && current_char() !== '\'') { + throw {message: 'unterminated string'} + } else { + let string_token = Object.assign({}, token_types.STRING); + string_token.value = command.substring(word_start_index + 1, current_index); + advance(); + return string_token; + } + } +}; + +const token_types = { + LEFT_PAREN: {type: 'left_paren'}, + RIGHT_PAREN: {type: 'right_paren'}, + LEFT_BRACKET: {type: 'left_bracket'}, + RIGHT_BRACKET: {type: 'right_bracket'}, + COMMA: {type: 'comma'}, + DOT: {type: 'dot'}, + MINUS: {type: 'minus'}, + PLUS: {type: 'plus'}, + STAR: {type: 'star'}, + SLASH: {type: 'slash'}, + EQUALS: {type: 'equals'}, + EQUALS_EQUALS: {type: 'equals_equals'}, + NOT_EQUALS: {type: 'not_equals'}, + NOT: {type: 'not'}, + GREATER: {type: 'greater'}, + GREATER_OR_EQUAL: {type: 'greater_or_equal'}, + LESS: {type: 'less'}, + LESS_OR_EQUAL: {type: 'less_or_equal'}, + NUMERIC: {type: 'number', value: undefined}, + IDENTIFIER: {type: 'identifier', value: undefined}, + STRING: {type: 'string', value: undefined}, + AND: {type: 'logical_and'}, + OR: {type: 'logical_or'} +}; + +const multiply = function (left, right) { + return left * right; +} + +const division = function (left, right) { + return left / right; +} + +const add = function (left, right) { + return left + right; +} + +const subtraction = function (left, right) { + return left - right; +} + +const test_equal = function (left, right) { + if (left.is_vector && right.is_vector) { + return left.equals(right); + } else { + return left === right; + } +} + +const logical_and = function (left, right) { + return left && right; +} + +const logical_or = function (left, right) { + return left || right; +} + +const create_2d_id_matrix = function () { + return { + data: [[1, 0], [0, 1]], + id: index_sequence++, + is_visual: true, + is_vector: false, // for type comparison + is_matrix: true, + type: 'matrix', // for showing type to user + is_new: true, // to determine view action + visible: true, + toString: function () { + return `matrix@${this.id}`; + }, + hide: function () { + return hide(this); + }, + label: function (text) { + return label(this, text); + }, + show: function () { + return show(this); + }, + equals: function (other) { + return (this.id === other.id || (this.type === other.type && this.data === other.data)); // TODO + }, + row: function (index) { + return this.data[index]; + } + } + +} \ No newline at end of file diff --git a/src/main/resources/static/js/grid.js b/src/main/resources/static/js/grid.js new file mode 100644 index 0000000..1664a4c --- /dev/null +++ b/src/main/resources/static/js/grid.js @@ -0,0 +1,32 @@ +let canvas = document.getElementById('myCanvas'); +let ctx; +if (canvas.getContext) { + ctx = canvas.getContext('2d'); + + // draw here + ctx.fillStyle = 'green'; + ctx.fillRect(0, 0, 500, 500); +} else { + console.log("Canvas not supported"); +} + +fetch('/api/grid/100', { + method: 'GET' // or 'POST' +}).then((response) => { + if (response.ok) { + return response.json(); // Here we're using JSON but you can use other formats such as blob, text etc. + } else { + throw new Error('Server response wasn\'t OK'); + } +}).then((grid) => { + const cell_factor = 500 / grid.size; + ctx.font = `5px Arial`; + ctx.fillStyle = "grey" + for (let r = 0; r < grid.size; r++) { + for (let c = 0; c < grid.size; c++) { + ctx.fillText("" + grid.grid[r][c], 5 + c * cell_factor, r * cell_factor); + } + } +}).catch((err) => { + console.log('Fetching failed', err); +}); \ No newline at end of file diff --git a/src/main/resources/static/js/parser.js b/src/main/resources/static/js/parser.js new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/assessment/algorithm/CircleTest.java b/src/test/java/assessment/algorithm/CircleTest.java new file mode 100644 index 0000000..76b127f --- /dev/null +++ b/src/test/java/assessment/algorithm/CircleTest.java @@ -0,0 +1,34 @@ +package assessment.algorithm; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CircleTest { + + @Test + void test_return_to_previous_is_not_worth_it() { + Grid grid = Grid.fromFile("grids/20.txt"); + Path path = Path.newPath(grid, Point.create(grid, 0, 0)); // 0.0 + path.add(grid, Point.create(grid, path, 0, 1)); // 1.0 + path.add(grid, Point.create(grid, path, 0, 2)); // 3.0 + path.add(grid, Point.create(grid, path, 0, 1)); // 3.0 + Assertions.assertEquals(3.0, path.value()); + } + + @Test + void test_return_to_previous_is_worth_something() { + Grid grid = Grid.fromFile("grids/20.txt"); + Path path = Path.newPath(grid, Point.create(grid, 0, 0)); // 0.0 + path.add(grid, Point.create(grid, path, 0, 1)); // 1.0 + path.add(grid, Point.create(grid, path, 0, 2)); // 3.0 + path.add(grid, Point.create(grid, path, 1, 1)); // 3.0 + path.add(grid, Point.create(grid, path, 0, 1)); // 3.2 distance 1, factor .2 + Assertions.assertEquals(3.2, path.value()); + } +} + diff --git a/src/test/java/GridReadTest.java b/src/test/java/assessment/algorithm/GridReadTest.java similarity index 87% rename from src/test/java/GridReadTest.java rename to src/test/java/assessment/algorithm/GridReadTest.java index 8e0d696..27ddb6f 100644 --- a/src/test/java/GridReadTest.java +++ b/src/test/java/assessment/algorithm/GridReadTest.java @@ -1,4 +1,6 @@ -import assessment.Grid; +package assessment.algorithm; + +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.util.List; diff --git a/src/test/java/PathFinderTest.java b/src/test/java/assessment/algorithm/PathFinderTest.java similarity index 58% rename from src/test/java/PathFinderTest.java rename to src/test/java/assessment/algorithm/PathFinderTest.java index ab4c860..81c7216 100644 --- a/src/test/java/PathFinderTest.java +++ b/src/test/java/assessment/algorithm/PathFinderTest.java @@ -1,6 +1,5 @@ -import assessment.Grid; -import assessment.Path; -import assessment.BestPathFinder; +package assessment.algorithm; + import org.junit.jupiter.api.Test; public class PathFinderTest { @@ -8,21 +7,21 @@ public class PathFinderTest { @Test public void testBestPath20() { Grid grid = Grid.fromFile("grids/20.txt"); - Path path = new BestPathFinder().findMaxValPath(grid, 20, 8, 1000, 9, 9); + Path path = new OptimalPathFinder().findOptimalPath(grid, 20, 8, 1000, 9, 9); System.out.println(path); } @Test public void testBestPath100() { Grid grid = Grid.fromFile("grids/100.txt"); - Path path = new BestPathFinder().findMaxValPath(grid, 100, 8, 10000, 50, 50); + Path path = new OptimalPathFinder().findOptimalPath(grid, 100, 8, 10000, 50, 50); System.out.println(path); } @Test public void testBestPath1000() { Grid grid = Grid.fromFile("grids/1000.txt"); - Path path = new BestPathFinder().findMaxValPath(grid, 1000, 18, 100000, 500, 500); + Path path = new OptimalPathFinder().findOptimalPath(grid, 1000, 18, 10000, 500, 500); System.out.println(path); } }