From dba84c5957978ac6120a505f8acf5113621c6ba6 Mon Sep 17 00:00:00 2001 From: Alex Birkett Date: Sun, 1 Feb 2015 22:07:32 +0100 Subject: [PATCH] Added real world tests (not passing) --- src/main/java/no/birkett/kiwi/Expression.java | 4 + .../kiwi/NonlinearExpressionException.java | 7 + src/main/java/no/birkett/kiwi/Symbolics.java | 17 + .../no/birkett/kiwi/ConstraintParser.java | 173 +++++++++ .../java/no/birkett/kiwi/RealWorldTests.java | 359 ++++++++++++++++++ 5 files changed, 560 insertions(+) create mode 100644 src/main/java/no/birkett/kiwi/NonlinearExpressionException.java create mode 100644 src/test/java/no/birkett/kiwi/ConstraintParser.java create mode 100644 src/test/java/no/birkett/kiwi/RealWorldTests.java diff --git a/src/main/java/no/birkett/kiwi/Expression.java b/src/main/java/no/birkett/kiwi/Expression.java index 750af3e..120517a 100644 --- a/src/main/java/no/birkett/kiwi/Expression.java +++ b/src/main/java/no/birkett/kiwi/Expression.java @@ -64,5 +64,9 @@ public class Expression { } return result; } + + public final boolean isConstant() { + return terms.size() == 0; + } } diff --git a/src/main/java/no/birkett/kiwi/NonlinearExpressionException.java b/src/main/java/no/birkett/kiwi/NonlinearExpressionException.java new file mode 100644 index 0000000..48e1600 --- /dev/null +++ b/src/main/java/no/birkett/kiwi/NonlinearExpressionException.java @@ -0,0 +1,7 @@ +package no.birkett.kiwi; + +/** + * Created by alex on 01/02/15. + */ +public class NonlinearExpressionException extends RuntimeException { +} diff --git a/src/main/java/no/birkett/kiwi/Symbolics.java b/src/main/java/no/birkett/kiwi/Symbolics.java index 19319c9..ad61eb6 100644 --- a/src/main/java/no/birkett/kiwi/Symbolics.java +++ b/src/main/java/no/birkett/kiwi/Symbolics.java @@ -50,10 +50,27 @@ public class Symbolics { return new Expression(terms, expression.getConstant() * coefficient); } + public static Expression multiply(Expression expression1, Expression expression2) throws NonlinearExpressionException { + if (expression1.isConstant()) { + return multiply(expression1.getConstant(), expression2); + } else if (expression2.isConstant()) { + return multiply(expression2.getConstant(), expression1); + } else { + throw new NonlinearExpressionException(); + } + } + public static Expression divide(Expression expression, double denominator) { return multiply(expression, (1.0 / denominator)); } + public static Expression divide(Expression expression1, Expression expression2) throws NonlinearExpressionException { + if (expression2.isConstant()) { + return divide(expression1, expression2.getConstant()); + } else { + throw new NonlinearExpressionException(); + } + } public static Expression negate(Expression expression) { return multiply(expression, -1.0); diff --git a/src/test/java/no/birkett/kiwi/ConstraintParser.java b/src/test/java/no/birkett/kiwi/ConstraintParser.java new file mode 100644 index 0000000..5dd2efd --- /dev/null +++ b/src/test/java/no/birkett/kiwi/ConstraintParser.java @@ -0,0 +1,173 @@ +package no.birkett.kiwi; + +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by alex on 25/09/2014. + */ +public class ConstraintParser { + + private static final Pattern pattern = Pattern.compile("\\s*(.*?)\\s*(<=|==|>=|[GL]?EQ)\\s*(.*?)\\s*(!(required|strong|medium|weak))?"); + + final static String OPS = "-+/*^"; + + public interface CassowaryVariableResolver { + + Variable resolveVariable(String variableName); + Expression resolveConstant(String name); + } + + public static Constraint parseConstraint(String constraintString, CassowaryVariableResolver variableResolver) { + + Matcher matcher = pattern.matcher(constraintString); + matcher.find(); + if (matcher.matches()) { + Variable variable = variableResolver.resolveVariable(matcher.group(1)); + RelationalOperator operator = parseOperator(matcher.group(2)); + Expression expression = resolveExpression(matcher.group(3), variableResolver); + double strength = parseStrength(matcher.group(4)); + + new Constraint(Symbolics.subtract(variable, expression), operator); + + } else { + throw new RuntimeException("could not parse " + constraintString); + } + return null; + } + + private static RelationalOperator parseOperator(String operatorString) { + + RelationalOperator operator = null; + if ("EQ".equals(operatorString) || "==".equals(operatorString)) { + operator = RelationalOperator.OP_EQ; + } else if ("GEQ".equals(operatorString) || ">=".equals(operatorString)) { + operator = RelationalOperator.OP_GE; + } else if ("LEQ".equals(operatorString) || "<=".equals(operatorString)) { + operator = RelationalOperator.OP_LE; + } + return operator; + } + + private static double parseStrength(String strengthString) { + + double strength = Strength.REQUIRED; + if ("!required".equals(strengthString)) { + strength = Strength.REQUIRED; + } else if ("!strong".equals(strengthString)) { + strength = Strength.STRONG; + } else if ("!medium".equals(strengthString)) { + strength = Strength.MEDIUM; + } else if ("!weak".equals(strengthString)) { + strength = Strength.WEAK; + } + return strength; + } + + public static Expression resolveExpression(String expressionString, CassowaryVariableResolver variableResolver) { + + List postFixExpression = infixToPostfix(tokenizeExpression(expressionString)); + + Stack expressionStack = new Stack(); + + for (String expression : postFixExpression) { + if ("+".equals(expression)) { + expressionStack.push(Symbolics.add(expressionStack.pop(), (expressionStack.pop()))); + } else if ("-".equals(expression)) { + Expression a = expressionStack.pop(); + Expression b = expressionStack.pop(); + + expressionStack.push(Symbolics.subtract(b, a)); + } else if ("/".equals(expression)) { + Expression denominator = expressionStack.pop(); + Expression numerator = expressionStack.pop(); + expressionStack.push(Symbolics.divide(numerator, denominator)); + } else if ("*".equals(expression)) { + expressionStack.push(Symbolics.multiply(expressionStack.pop(), (expressionStack.pop()))); + } else { + Expression linearExpression = variableResolver.resolveConstant(expression); + if (linearExpression == null) { + linearExpression = new Expression(new Term(variableResolver.resolveVariable(expression))); + } + expressionStack.push(linearExpression); + } + } + + return expressionStack.pop(); + } + + public static List infixToPostfix(List tokenList) { + + Stack s = new Stack(); + + List postFix = new ArrayList(); + for (String token : tokenList) { + char c = token.charAt(0); + int idx = OPS.indexOf(c); + if (idx != -1 && token.length() == 1) { + if (s.isEmpty()) + s.push(idx); + else { + while (!s.isEmpty()) { + int prec2 = s.peek() / 2; + int prec1 = idx / 2; + if (prec2 > prec1 || (prec2 == prec1 && c != '^')) + postFix.add(Character.toString(OPS.charAt(s.pop()))); + else break; + } + s.push(idx); + } + } else if (c == '(') { + s.push(-2); + } else if (c == ')') { + while (s.peek() != -2) + postFix.add(Character.toString(OPS.charAt(s.pop()))); + s.pop(); + } else { + postFix.add(token); + } + } + while (!s.isEmpty()) + postFix.add(Character.toString(OPS.charAt(s.pop()))); + return postFix; + } + + public static List tokenizeExpression(String expressionString) { + ArrayList tokenList = new ArrayList(); + + StringBuilder stringBuilder = new StringBuilder(); + int i; + for (i = 0; i < expressionString.length(); i++) { + char c = expressionString.charAt(i); + switch (c) { + case '+': + case '-': + case '*': + case '/': + case '(': + case ')': + if (stringBuilder.length() > 0) { + tokenList.add(stringBuilder.toString()); + stringBuilder.setLength(0); + } + tokenList.add(Character.toString(c)); + break; + case ' ': + // ignore space + break; + default: + stringBuilder.append(c); + } + + } + if (stringBuilder.length() > 0) { + tokenList.add(stringBuilder.toString()); + } + + return tokenList; + } + +} diff --git a/src/test/java/no/birkett/kiwi/RealWorldTests.java b/src/test/java/no/birkett/kiwi/RealWorldTests.java new file mode 100644 index 0000000..82bb004 --- /dev/null +++ b/src/test/java/no/birkett/kiwi/RealWorldTests.java @@ -0,0 +1,359 @@ +package no.birkett.kiwi; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * Created by alex on 20/11/2014. + */ +public class RealWorldTests { + + private static double EPSILON = 1.0e-2; + + public static final String LEFT = "left"; + public static final String RIGHT = "right"; + public static final String TOP = "top"; + public static final String BOTTOM = "bottom"; + public static final String HEIGHT = "height"; + public static final String WIDTH = "width"; + public static final String CENTERX = "centerX"; + public static final String CENTERY = "centerY"; + + private static final String[] CONSTRAINTS = { + + "container.columnWidth == container.width * 0.4", + "container.thumbHeight == container.columnWidth / 2", + "container.padding == container.width * (0.2 / 3)", + "container.leftPadding == container.padding", + "container.rightPadding == container.width - container.padding", + "container.paddingUnderThumb == 5", + "container.rowPadding == 15", + "container.buttonPadding == 20", + + "thumb0.left == container.leftPadding", + "thumb0.top == container.padding", + "thumb0.height == container.thumbHeight", + "thumb0.width == container.columnWidth", + + "title0.left == container.leftPadding", + "title0.top == thumb0.bottom + container.paddingUnderThumb", + "title0.height == title0.intrinsicHeight", + "title0.width == container.columnWidth", + + "thumb1.right == container.rightPadding", + "thumb1.top == container.padding", + "thumb1.height == container.thumbHeight", + "thumb1.width == container.columnWidth", + + "title1.right == container.rightPadding", + "title1.top == thumb0.bottom + container.paddingUnderThumb", + "title1.height == title1.intrinsicHeight", + "title1.width == container.columnWidth", + + "thumb2.left == container.leftPadding", + "thumb2.top >= title0.bottom + container.rowPadding", + "thumb2.top == title0.bottom + container.rowPadding !weak", + "thumb2.top >= title1.bottom + container.rowPadding", + "thumb2.top == title1.bottom + container.rowPadding !weak", + "thumb2.height == container.thumbHeight", + "thumb2.width == container.columnWidth", + + "title2.left == container.leftPadding", + "title2.top == thumb2.bottom + container.paddingUnderThumb", + "title2.height == title2.intrinsicHeight", + "title2.width == container.columnWidth", + + "thumb3.right == container.rightPadding", + "thumb3.top == thumb2.top", + + "thumb3.height == container.thumbHeight", + "thumb3.width == container.columnWidth", + + "title3.right == container.rightPadding", + "title3.top == thumb3.bottom + container.paddingUnderThumb", + "title3.height == title3.intrinsicHeight", + "title3.width == container.columnWidth", + + "thumb4.left == container.leftPadding", + "thumb4.top >= title2.bottom + container.rowPadding", + "thumb4.top >= title3.bottom + container.rowPadding", + "thumb4.top == title2.bottom + container.rowPadding !weak", + "thumb4.top == title3.bottom + container.rowPadding !weak", + "thumb4.height == container.thumbHeight", + "thumb4.width == container.columnWidth", + + "title4.left == container.leftPadding", + "title4.top == thumb4.bottom + container.paddingUnderThumb", + "title4.height == title4.intrinsicHeight", + "title4.width == container.columnWidth", + + "thumb5.right == container.rightPadding", + "thumb5.top == thumb4.top", + "thumb5.height == container.thumbHeight", + "thumb5.width == container.columnWidth", + + "title5.right == container.rightPadding", + "title5.top == thumb5.bottom + container.paddingUnderThumb", + "title5.height == title5.intrinsicHeight", + "title5.width == container.columnWidth", + + "line.height == 1", + "line.width == container.width", + "line.top >= title4.bottom + container.rowPadding", + "line.top >= title5.bottom + container.rowPadding", + + "more.top == line.bottom + container.buttonPadding", + "more.height == more.intrinsicHeight", + "more.left == container.leftPadding", + "more.right == container.rightPadding", + + "container.height == more.bottom + container.buttonPadding"}; + + + public ConstraintParser.CassowaryVariableResolver createVariableResolver(final Solver solver, final HashMap> nodeHashMap) { + ConstraintParser.CassowaryVariableResolver variableResolver = new ConstraintParser.CassowaryVariableResolver() { + + private Variable getVariableFromNode(HashMap node, String variableName) { + + try { + if (node.containsKey(variableName)) { + return node.get(variableName); + } else { + Variable variable = new Variable(variableName); + node.put(variableName, variable); + if (RIGHT.equals(variableName)) { + solver.addConstraint(Symbolics.equals(variable, Symbolics.add(getVariableFromNode(node, LEFT), getVariableFromNode(node, WIDTH)))); + } else if (BOTTOM.equals(variableName)) { + solver.addConstraint(Symbolics.equals(variable, Symbolics.add(getVariableFromNode(node, TOP), getVariableFromNode(node, HEIGHT)))); + } else if (CENTERX.equals(variableName)) { + // solver.addConstraint(Symbolics.equals(variable, Symbolics.add(Symbolics.divide(getVariableFromNode(node, WIDTH), 2), getVariableFromNode(node, LEFT))); + } else if (CENTERY.equals(variableName)) { + // solver.addConstraint(Symbolics.equals(variable, Symbolics.add(new Expression(Symbolics.divide(getVariableFromNode(node, HEIGHT), 2)), getVariableFromNode(node, TOP)); + } + return variable; + } + } catch(DuplicateConstraintException e) { + + } catch (UnsatisfiableConstraintException e) { + + } + + return null; + + } + + private HashMap getNode(String nodeName) { + HashMap node; + if (nodeHashMap.containsKey(nodeName)) { + node = nodeHashMap.get(nodeName); + } else { + node = new HashMap(); + nodeHashMap.put(nodeName, node); + } + return node; + } + + @Override + public Variable resolveVariable(String variableName) { + + String[] stringArray = variableName.split("\\."); + if (stringArray.length == 2) { + String nodeName = stringArray[0]; + String propertyName = stringArray[1]; + + HashMap node = getNode(nodeName); + + return getVariableFromNode(node, propertyName); + + } else { + throw new RuntimeException("can't resolve variable"); + } + } + + @Override + public Expression resolveConstant(String name) { + try { + return new Expression(Double.parseDouble(name)); + } catch (NumberFormatException e) { + return null; + } + } + }; + return variableResolver; + } + + + @Test + public void testGridLayout() throws DuplicateConstraintException, UnsatisfiableConstraintException { + + final Solver solver = new Solver(); + + final HashMap> nodeHashMap = new HashMap>(); + + ConstraintParser.CassowaryVariableResolver variableResolver = createVariableResolver(solver, nodeHashMap); + + for (String constraint : CONSTRAINTS) { + solver.addConstraint(ConstraintParser.parseConstraint(constraint, variableResolver)); + } + + solver.addConstraint(ConstraintParser.parseConstraint("container.width == 300", variableResolver)); + solver.addConstraint(ConstraintParser.parseConstraint("title0.intrinsicHeight == 100", variableResolver)); + solver.addConstraint(ConstraintParser.parseConstraint("title1.intrinsicHeight == 110", variableResolver)); + solver.addConstraint(ConstraintParser.parseConstraint("title2.intrinsicHeight == 120", variableResolver)); + solver.addConstraint(ConstraintParser.parseConstraint("title3.intrinsicHeight == 130", variableResolver)); + solver.addConstraint(ConstraintParser.parseConstraint("title4.intrinsicHeight == 140", variableResolver)); + solver.addConstraint(ConstraintParser.parseConstraint("title5.intrinsicHeight == 150", variableResolver)); + solver.addConstraint(ConstraintParser.parseConstraint("more.intrinsicHeight == 160", variableResolver)); + + solver.updateVariables(); + + assertEquals(20, nodeHashMap.get("thumb0").get("top").getValue(), EPSILON); + assertEquals(20, nodeHashMap.get("thumb1").get("top").getValue(), EPSILON); + + assertEquals(85, nodeHashMap.get("title0").get("top").getValue(), EPSILON); + assertEquals(85, nodeHashMap.get("title1").get("top").getValue(), EPSILON); + + assertEquals(210, nodeHashMap.get("thumb2").get("top").getValue(), EPSILON); + assertEquals(210, nodeHashMap.get("thumb3").get("top").getValue(), EPSILON); + + assertEquals(275, nodeHashMap.get("title2").get("top").getValue(), EPSILON); + assertEquals(275, nodeHashMap.get("title3").get("top").getValue(), EPSILON); + + assertEquals(420, nodeHashMap.get("thumb4").get("top").getValue(), EPSILON); + assertEquals(420, nodeHashMap.get("thumb5").get("top").getValue(), EPSILON); + + assertEquals(485, nodeHashMap.get("title4").get("top").getValue(), EPSILON); + assertEquals(485, nodeHashMap.get("title5").get("top").getValue(), EPSILON); + + } + + /* @Test + public void testGridLayoutUsingEditVariables() throws CassowaryError { + + final SimplexSolver solver = new SimplexSolver(); + solver.setAutosolve(true); + + final HashMap> nodeHashMap = new HashMap>(); + + ConstraintParser.CassowaryVariableResolver variableResolver = createVariableResolver(solver, nodeHashMap); + + for (String constraint : CONSTRAINTS) { + solver.addConstraint(ConstraintParser.parseConstraint(constraint, variableResolver)); + } + + Variable containerWidth = nodeHashMap.get("container").get("width"); + Variable title0IntrinsicHeight = nodeHashMap.get("title0").get("intrinsicHeight"); + Variable title1IntrinsicHeight = nodeHashMap.get("title1").get("intrinsicHeight"); + Variable title2IntrinsicHeight = nodeHashMap.get("title2").get("intrinsicHeight"); + Variable title3IntrinsicHeight = nodeHashMap.get("title3").get("intrinsicHeight"); + Variable title4IntrinsicHeight = nodeHashMap.get("title4").get("intrinsicHeight"); + Variable title5IntrinsicHeight = nodeHashMap.get("title5").get("intrinsicHeight"); + Variable moreIntrinsicHeight = nodeHashMap.get("more").get("intrinsicHeight"); + + solver.addStay(containerWidth); + solver.addStay(title0IntrinsicHeight); + solver.addStay(title1IntrinsicHeight); + solver.addStay(title2IntrinsicHeight); + solver.addStay(title3IntrinsicHeight); + solver.addStay(title4IntrinsicHeight); + solver.addStay(title5IntrinsicHeight); + solver.addStay(moreIntrinsicHeight); + + solver.addEditVar(containerWidth); + solver.addEditVar(title0IntrinsicHeight); + solver.addEditVar(title1IntrinsicHeight); + solver.addEditVar(title2IntrinsicHeight); + solver.addEditVar(title3IntrinsicHeight); + solver.addEditVar(title4IntrinsicHeight); + solver.addEditVar(title5IntrinsicHeight); + solver.addEditVar(moreIntrinsicHeight); + solver.beginEdit(); + + solver.suggestValue(containerWidth, 300); + solver.suggestValue(title0IntrinsicHeight, 100); + solver.suggestValue(title1IntrinsicHeight, 110); + solver.suggestValue(title2IntrinsicHeight, 120); + solver.suggestValue(title3IntrinsicHeight, 130); + solver.suggestValue(title4IntrinsicHeight, 140); + solver.suggestValue(title5IntrinsicHeight, 150); + solver.suggestValue(moreIntrinsicHeight, 160); + + solver.resolve(); + + solver.solve(); + + assertEquals(20, nodeHashMap.get("thumb0").get("top").value(), EPSILON); + assertEquals(20, nodeHashMap.get("thumb1").get("top").value(), EPSILON); + + assertEquals(85, nodeHashMap.get("title0").get("top").value(), EPSILON); + assertEquals(85, nodeHashMap.get("title1").get("top").value(), EPSILON); + + assertEquals(210, nodeHashMap.get("thumb2").get("top").value(), EPSILON); + assertEquals(210, nodeHashMap.get("thumb3").get("top").value(), EPSILON); + + assertEquals(275, nodeHashMap.get("title2").get("top").value(), EPSILON); + assertEquals(275, nodeHashMap.get("title3").get("top").value(), EPSILON); + + assertEquals(420, nodeHashMap.get("thumb4").get("top").value(), EPSILON); + assertEquals(420, nodeHashMap.get("thumb5").get("top").value(), EPSILON); + + assertEquals(485, nodeHashMap.get("title4").get("top").value(), EPSILON); + assertEquals(485, nodeHashMap.get("title5").get("top").value(), EPSILON); + + } + + + @Test + public void testGridX1000() throws ConstraintNotFound { + + long nanoTime = System.nanoTime(); + for (int i = 0; i < 1000; i++) { + testGridLayout(); + } + System.out.println("testGridX1000 took " + (System.nanoTime() - nanoTime) / 1000000); + } + + @Test + public void testGridWithEditsX1000() throws CassowaryError { + + long nanoTime = System.nanoTime(); + + for (int i = 0; i < 1000; i++) { + testGridLayoutUsingEditVariables(); + } + System.out.println("testGridWithEditsX1000 took " + (System.nanoTime() - nanoTime) / 1000000 + " ms"); + + }*/ + + private static void printNodes(HashMap> variableHashMap) { + Iterator>> it = variableHashMap.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> pairs = it.next(); + System.out.println("node " + pairs.getKey()); + printVariables(pairs.getValue()); + } + } + + private static void printVariables(HashMap variableHashMap) { + Iterator> it = variableHashMap.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry pairs = it.next(); + System.out.println(" " + pairs.getKey() + " = " + pairs.getValue().getValue() + " (address:" + pairs.getValue().hashCode() + ")"); + } + } + +}