diff --git a/ruby/case_numeric_camparing_as_string.rb b/ruby/case_numeric_camparing_as_string.rb index 12c2222b..7362d8ef 100755 --- a/ruby/case_numeric_camparing_as_string.rb +++ b/ruby/case_numeric_camparing_as_string.rb @@ -2,13 +2,67 @@ require_relative '_helpers.rb' + + +pp render({}, "{%- assign value = 5.0 | round: 1 -%} {%- if value >= 4.0 and value <= 4.2 -%} Très bien {%- elsif value >= 4.3 and value <= 4.7 -%} Superbe {%- elsif value >= 4.8 and value <= 4.9 -%} Fabuleux {%- elsif value != 5.0 -%} Exceptionnel {% endif %}") + + +first = 98 +second = true +operator = '>' +pp "{% if #{first} #{operator} #{second} %}true{% else %}false{% endif %}" +pp render({}, "{% if #{first} #{operator} #{second} %}true{% else %}false{% endif %}") + # pp render({}, "{% assign comparingValue = 98.0 %}{{ 98 > comparingValue }}") # assertEqual("true", render({}, "{% assign comparingValue = 98.0 %}{{ '98' == comparingValue }}")) -assertRaise do - assertEqual("true", render({}, "{% if 98 > '98' %}true{% else %}false{% endif %}")) -end -assertEqual(" false ", render({}, "{% if null <= 0 %} true {% else %} false {% endif %}")) -assertEqual("no", render({}, "{% if 42.1 >= false %}yes{% else %}no{% endif %}")) -assertEqual("no", render({}, "{% if true > false %}yes{% else %}no{% endif %}")) -assertEqual("no", render({}, "{% if true < false %}yes{% else %}no{% endif %}")) + # product of two operators of types: + # number + # string + # true + # false + # null + # with operators: + # > + # >= + # < + # <= + # == + # != + + resArray = [] + arr = [98, '97', true, false, nil] + ops = ['>', '>=', '<', '<=', '==', '!='] + arr.product(arr).product(ops).each do |pair| + operator = pair[1] + first = pair[0][0] + second = pair[0][1] + begin + + if first.class == String then first = "'#{first}'" end + if first === nil then first = 'nil' end + if second.class == String then second = "'#{second}'" end + if second === nil then second = 'nil' end + res = render({}, "{% if #{first} #{operator} #{second} %}true{% else %}false{% endif %}") + if res === 'true' then resArray.push(1) else resArray.push(0) end + pp "#{first} #{operator} #{second} = #{res}" + rescue Exception => e + if first.class == String then first = "'#{first}'" end + if first === nil then first = 'nil' end + if second.class == String then second = "'#{second}'" end + if second === nil then second = 'nil' end + resArray.push('e') + pp "#{first} #{operator} #{second} = ERROR: #{e.message[/.*/]}" + end + + end + + pp resArray.join('') + +# assertRaise do +# assertEqual("true", render({}, "{% if 98 > '98' %}true{% else %}false{% endif %}")) +# end +# assertEqual(" false ", render({}, "{% if null <= 0 %} true {% else %} false {% endif %}")) +# assertEqual("no", render({}, "{% if 42.1 >= false %}yes{% else %}no{% endif %}")) +# assertEqual("no", render({}, "{% if true > false %}yes{% else %}no{% endif %}")) +# assertEqual("no", render({}, "{% if true < false %}yes{% else %}no{% endif %}")) diff --git a/src/main/java/liqp/LValue.java b/src/main/java/liqp/LValue.java index 7ce99ec7..0fead747 100644 --- a/src/main/java/liqp/LValue.java +++ b/src/main/java/liqp/LValue.java @@ -275,11 +275,14 @@ public Number asNumber(Object value) throws NumberFormatException { return str.matches("\\d+") ? Long.valueOf(str) : Double.valueOf(str); } - public BigDecimal asStrictNumber(Number number) { + public BigDecimal asStrictNumber(Object number) { if (number == null) { return null; } - return new BigDecimal(number.toString()); + if (number instanceof BigDecimal) { + return (BigDecimal) number; + } + return new BigDecimal(number.toString().trim()); } // mimic ruby's `BigDecimal.to_f` with standard java capabilities @@ -458,6 +461,13 @@ public boolean canBeDouble(Object value) { return String.valueOf(value).trim().matches("-?\\d+(\\.\\d*)?"); } + public boolean canBeNumber(Object value) { + if (value == null) { + return false; + } + return canBeInteger(value) || canBeDouble(value); + } + public boolean isMap(Object value) { return (value instanceof Map); } diff --git a/src/main/java/liqp/TemplateParser.java b/src/main/java/liqp/TemplateParser.java index e32206fe..d705fb3c 100644 --- a/src/main/java/liqp/TemplateParser.java +++ b/src/main/java/liqp/TemplateParser.java @@ -59,6 +59,7 @@ public enum ErrorMode { public final Insertions insertions; public final Filters filters; public final boolean evaluateInOutputTag; + public final boolean strictTypedExpressions; public final TemplateParser.ErrorMode errorMode; public final boolean liquidStyleInclude; public final boolean liquidStyleWhere; @@ -131,6 +132,7 @@ public static class Builder { private List insertions = new ArrayList<>(); private List filters = new ArrayList<>(); private Boolean evaluateInOutputTag; + private Boolean strictTypedExpressions; private TemplateParser.ErrorMode errorMode; private Boolean liquidStyleInclude; private Boolean liquidStyleWhere; @@ -174,6 +176,7 @@ public Builder(TemplateParser parser) { this.limitMaxRenderTimeMillis = parser.limitMaxRenderTimeMillis; this.limitMaxTemplateSizeBytes = parser.limitMaxTemplateSizeBytes; this.evaluateInOutputTag = parser.evaluateInOutputTag; + this.strictTypedExpressions = parser.strictTypedExpressions; this.liquidStyleInclude = parser.liquidStyleInclude; this.liquidStyleWhere = parser.liquidStyleWhere; @@ -232,6 +235,11 @@ public Builder withEvaluateInOutputTag(boolean evaluateInOutputTag) { return this; } + public Builder withStrictTypedExpressions(boolean strictTypedExpressions) { + this.strictTypedExpressions = strictTypedExpressions; + return this; + } + @SuppressWarnings("hiding") public Builder withLiquidStyleInclude(boolean liquidStyleInclude) { this.liquidStyleInclude = liquidStyleInclude; @@ -347,6 +355,11 @@ public TemplateParser build() { evaluateInOutputTag = fl.isEvaluateInOutputTag(); } + Boolean strictTypedExpressions = this.strictTypedExpressions; + if (strictTypedExpressions == null) { + strictTypedExpressions = fl.isStrictTypedExpressions(); + } + Boolean liquidStyleInclude = this.liquidStyleInclude; if (liquidStyleInclude == null) { liquidStyleInclude = fl.isLiquidStyleInclude(); @@ -382,14 +395,16 @@ public TemplateParser build() { } return new TemplateParser(strictVariables, showExceptionsFromInclude, evaluateMode, renderTransformer, locale, defaultTimeZone, environmentMapConfigurator, errorMode, fl, stripSpacesAroundTags, stripSingleLine, mapper, - allInsertions, finalFilters, snippetsFolderName, evaluateInOutputTag, liquidStyleInclude, liquidStyleWhere, limitMaxIterations, limitMaxSizeRenderedString, limitMaxRenderTimeMillis, limitMaxTemplateSizeBytes); + allInsertions, finalFilters, snippetsFolderName, evaluateInOutputTag, strictTypedExpressions, liquidStyleInclude, liquidStyleWhere, limitMaxIterations, limitMaxSizeRenderedString, limitMaxRenderTimeMillis, limitMaxTemplateSizeBytes); } } TemplateParser(boolean strictVariables, boolean showExceptionsFromInclude, EvaluateMode evaluateMode, RenderTransformer renderTransformer, Locale locale, ZoneId defaultTimeZone, Consumer> environmentMapConfigurator, ErrorMode errorMode, Flavor flavor, boolean stripSpacesAroundTags, boolean stripSingleLine, - ObjectMapper mapper, Insertions insertions, Filters filters, String snippetsFolderName, boolean evaluateInOutputTag, boolean liquidStyleInclude, Boolean liquidStyleWhere, int maxIterations, int maxSizeRenderedString, long maxRenderTimeMillis, long maxTemplateSizeBytes) { + ObjectMapper mapper, Insertions insertions, Filters filters, String snippetsFolderName, boolean evaluateInOutputTag, + boolean strictTypedExpressions, + boolean liquidStyleInclude, Boolean liquidStyleWhere, int maxIterations, int maxSizeRenderedString, long maxRenderTimeMillis, long maxTemplateSizeBytes) { this.flavor = flavor; this.stripSpacesAroundTags = stripSpacesAroundTags; this.stripSingleLine = stripSingleLine; @@ -397,6 +412,7 @@ public TemplateParser build() { this.insertions = insertions; this.filters = filters; this.evaluateInOutputTag = evaluateInOutputTag; + this.strictTypedExpressions = strictTypedExpressions; this.errorMode = errorMode; this.liquidStyleInclude = liquidStyleInclude; this.liquidStyleWhere = liquidStyleWhere; diff --git a/src/main/java/liqp/filters/Round.java b/src/main/java/liqp/filters/Round.java index ee37469f..03737c4c 100644 --- a/src/main/java/liqp/filters/Round.java +++ b/src/main/java/liqp/filters/Round.java @@ -1,5 +1,6 @@ package liqp.filters; +import java.math.BigDecimal; import java.math.RoundingMode; import java.text.DecimalFormat; @@ -31,6 +32,6 @@ public Object apply(Object value, Object... params) { DecimalFormat formatter = new DecimalFormat(formatBuilder.toString()); formatter.setRoundingMode(RoundingMode.HALF_UP); - return formatter.format(number); + return new BigDecimal(formatter.format(number)); } } diff --git a/src/main/java/liqp/nodes/ComparingExpressionNode.java b/src/main/java/liqp/nodes/ComparingExpressionNode.java index ddfe3a66..a420d957 100644 --- a/src/main/java/liqp/nodes/ComparingExpressionNode.java +++ b/src/main/java/liqp/nodes/ComparingExpressionNode.java @@ -3,13 +3,18 @@ import liqp.LValue; import liqp.TemplateContext; +import java.math.BigDecimal; +import java.util.Optional; + public abstract class ComparingExpressionNode extends LValue implements LNode { protected final LNode lhs; protected final LNode rhs; + private final boolean relative; - public ComparingExpressionNode(LNode lhs, LNode rhs) { + public ComparingExpressionNode(LNode lhs, LNode rhs, boolean realtive) { this.lhs = lhs; this.rhs = rhs; + this.relative = realtive; } @Override @@ -48,8 +53,63 @@ public Object render(TemplateContext context) { if (b instanceof Number) { b = asStrictNumber((Number)b); } - return doCompare(a, b); + boolean strictTypedExpressions; + if (context != null && context.getParser() != null) { + // this variable always set except for tests + strictTypedExpressions = context.getParser().strictTypedExpressions; + } else { + strictTypedExpressions = true; + } + if (relative) { + Optional common = relativeCompareCommonRules(a, b, strictTypedExpressions); + if (common.isPresent()) { + return common.get(); + } + if (!strictTypedExpressions) { + if (a instanceof Boolean) { + a = booleanToNumber((Boolean) a); + } + if (b instanceof Boolean) { + b = booleanToNumber((Boolean) b); + } + if (a == null) { + a = BigDecimal.ZERO; + } + if (b == null) { + b = BigDecimal.ZERO; + } + } + } + if (!strictTypedExpressions) { + if ((a instanceof Number && canBeNumber(b)) || (b instanceof Number && canBeNumber(a))) { + a = asStrictNumber(a); + b = asStrictNumber(b); + } + } + return doCompare(a, b, strictTypedExpressions); + } + + private Object booleanToNumber(Boolean a) { + if (Boolean.TRUE.equals(a)) { + return BigDecimal.ONE; + } else { + return BigDecimal.ZERO; + } } - abstract Object doCompare(Object a, Object b); + abstract Object doCompare(Object a, Object b, boolean strictTypedExpressions); + + protected Optional relativeCompareCommonRules(Object a, Object b, boolean strictTypedExpressions) { + if (strictTypedExpressions) { + if (a instanceof Boolean || b instanceof Boolean) { + // relative comparing with boolean should be false all the time + return Optional.of(false); + } + if (a == null || b == null) { + // relative comparing with null should be false all the time + return Optional.of(false); + } + } + return Optional.empty(); + } } diff --git a/src/main/java/liqp/nodes/EqNode.java b/src/main/java/liqp/nodes/EqNode.java index 8fc39bbe..38b20cae 100644 --- a/src/main/java/liqp/nodes/EqNode.java +++ b/src/main/java/liqp/nodes/EqNode.java @@ -7,11 +7,11 @@ public class EqNode extends ComparingExpressionNode { public EqNode(LNode lhs, LNode rhs) { - super(lhs, rhs); + super(lhs, rhs, false); } @Override - Object doCompare(Object a, Object b) { + Object doCompare(Object a, Object b, boolean strictTypedExpressions) { if (a == null) { return b == null; diff --git a/src/main/java/liqp/nodes/GtEqNode.java b/src/main/java/liqp/nodes/GtEqNode.java index aadd452d..e4dfc10b 100644 --- a/src/main/java/liqp/nodes/GtEqNode.java +++ b/src/main/java/liqp/nodes/GtEqNode.java @@ -3,26 +3,12 @@ public class GtEqNode extends ComparingExpressionNode { public GtEqNode(LNode lhs, LNode rhs) { - super(lhs, rhs); + super(lhs, rhs, true); } @SuppressWarnings("unchecked") @Override - Object doCompare(Object a, Object b) { - if (a == null) { - return b == null; - } - if (b == null) { - return false; - } - - if (a instanceof Boolean) { - return false; - } - if (b instanceof Boolean) { - return false; - } - + Object doCompare(Object a, Object b, boolean strictTypedExpressions) { if (a instanceof Comparable && a.getClass().isInstance(b)) { return ((Comparable) a).compareTo(b) >= 0; } else if (b instanceof Comparable && b.getClass().isInstance(a)) { diff --git a/src/main/java/liqp/nodes/GtNode.java b/src/main/java/liqp/nodes/GtNode.java index eed3e47c..74123bda 100644 --- a/src/main/java/liqp/nodes/GtNode.java +++ b/src/main/java/liqp/nodes/GtNode.java @@ -1,28 +1,16 @@ package liqp.nodes; +import java.util.Optional; + public class GtNode extends ComparingExpressionNode { public GtNode(LNode lhs, LNode rhs) { - super(lhs, rhs); + super(lhs, rhs, true); } @SuppressWarnings("unchecked") @Override - Object doCompare(Object a, Object b) { - - if (a == null) { - return b == null; - } - if (b == null) { - return false; - } - - if (a instanceof Boolean) { - return false; - } - if (b instanceof Boolean) { - return false; - } + Object doCompare(Object a, Object b, boolean strictTypedExpressions) { if (a instanceof Comparable && a.getClass().isInstance(b)) { return ((Comparable) a).compareTo(b) > 0; diff --git a/src/main/java/liqp/nodes/LtEqNode.java b/src/main/java/liqp/nodes/LtEqNode.java index cab70777..1320e376 100644 --- a/src/main/java/liqp/nodes/LtEqNode.java +++ b/src/main/java/liqp/nodes/LtEqNode.java @@ -1,27 +1,15 @@ package liqp.nodes; +import java.util.Optional; + public class LtEqNode extends ComparingExpressionNode { public LtEqNode(LNode lhs, LNode rhs) { - super(lhs, rhs); + super(lhs, rhs, true); } @SuppressWarnings("unchecked") @Override - Object doCompare(Object a, Object b) { - - if (a == null) { - return b == null; - } - if (b == null) { - return false; - } - - if (a instanceof Boolean) { - return false; - } - if (b instanceof Boolean) { - return false; - } + Object doCompare(Object a, Object b, boolean strictTypedExpressions) { if (a instanceof Comparable && a.getClass().isInstance(b)) { return ((Comparable) a).compareTo(b) <= 0; diff --git a/src/main/java/liqp/nodes/LtNode.java b/src/main/java/liqp/nodes/LtNode.java index 32f7bbf7..e8e0d26a 100644 --- a/src/main/java/liqp/nodes/LtNode.java +++ b/src/main/java/liqp/nodes/LtNode.java @@ -1,28 +1,17 @@ package liqp.nodes; +import java.util.Optional; + public class LtNode extends ComparingExpressionNode { public LtNode(LNode lhs, LNode rhs) { - super(lhs, rhs); + super(lhs, rhs, true); } @SuppressWarnings("unchecked") @Override - Object doCompare(Object a, Object b) { - - if (a == null) { - return b == null; - } - if (b == null) { - return false; - } + Object doCompare(Object a, Object b, boolean strictTypedExpressions) { - if (a instanceof Boolean) { - return false; - } - if (b instanceof Boolean) { - return false; - } if (a instanceof Comparable && a.getClass().isInstance(b)) { return ((Comparable) a).compareTo(b) < 0; } else if (b instanceof Comparable && b.getClass().isInstance(a)) { diff --git a/src/main/java/liqp/nodes/NEqNode.java b/src/main/java/liqp/nodes/NEqNode.java index f6b7620b..4d4a316b 100644 --- a/src/main/java/liqp/nodes/NEqNode.java +++ b/src/main/java/liqp/nodes/NEqNode.java @@ -7,19 +7,19 @@ public class NEqNode extends ComparingExpressionNode { public NEqNode(LNode lhs, LNode rhs) { - super(lhs, rhs); + super(lhs, rhs, false); } @Override - Object doCompare(Object a, Object b) { + Object doCompare(Object a, Object b, boolean strictTypedExpressions) { if (a instanceof Boolean && b instanceof Boolean) { return !Objects.equals(a, b); } if (a instanceof Boolean) { - return false; + return true; } if (b instanceof Boolean) { - return false; + return true; } return !LValue.areEqual(a, b); } diff --git a/src/main/java/liqp/parser/Flavor.java b/src/main/java/liqp/parser/Flavor.java index a18963e7..4c8bcea3 100644 --- a/src/main/java/liqp/parser/Flavor.java +++ b/src/main/java/liqp/parser/Flavor.java @@ -11,7 +11,8 @@ public enum Flavor { TemplateParser.ErrorMode.STRICT, true, true, - false + false, + true ), // JEKYLL("_includes", @@ -20,7 +21,8 @@ public enum Flavor { TemplateParser.ErrorMode.WARN, false, false, - false + false, + true ), LIQP("snippets", @@ -29,7 +31,8 @@ public enum Flavor { TemplateParser.ErrorMode.STRICT, true, true, - true + true, + false ); public final String snippetsFolderName; @@ -39,6 +42,7 @@ public enum Flavor { private final boolean liquidStyleInclude; private final boolean liquidStyleWhere; private final boolean evaluateInOutputTag; + private final boolean strictTypedExpressions; private TemplateParser parser; Flavor(String snippetsFolderName, @@ -47,7 +51,9 @@ public enum Flavor { TemplateParser.ErrorMode errorMode, boolean isLiquidStyleInclude, boolean isLiquidStyleWhere, - boolean evaluateInOutputTag) { + boolean evaluateInOutputTag, + boolean strictTypedExpressions + ) { this.snippetsFolderName = snippetsFolderName; this.filters = filters; this.insertions = insertions; @@ -55,6 +61,7 @@ public enum Flavor { this.liquidStyleInclude = isLiquidStyleInclude; this.liquidStyleWhere = isLiquidStyleWhere; this.evaluateInOutputTag = evaluateInOutputTag; + this.strictTypedExpressions = strictTypedExpressions; } /** @@ -109,4 +116,14 @@ public boolean isLiquidStyleInclude() { public boolean isLiquidStyleWhere() { return liquidStyleWhere; } + + /** + * ruby is strictly typed, so comparing string like '98' and number like 97 will raise exception, + * and we must follow this behavior for compatibility in Liquid and Jekyll flavors + * but historically in this library and practice from other implementations show that people expect weak-typing in expressions. + * So this is a flag that actually control this. + */ + public boolean isStrictTypedExpressions() { + return strictTypedExpressions; + } } diff --git a/src/test/java/liqp/StatementsTest.java b/src/test/java/liqp/StatementsTest.java index d1377ee1..6d2086eb 100644 --- a/src/test/java/liqp/StatementsTest.java +++ b/src/test/java/liqp/StatementsTest.java @@ -100,9 +100,9 @@ public void zero_lq_or_equal_oneTest() throws Exception { @Test public void zero_lq_or_equal_one_involving_nilTest() throws Exception { - assertThat(TemplateParser.DEFAULT.parse(" {% if null <= 0 %} true {% else %} false {% endif %} ").render(), is(" false ")); + assertThat(TemplateParser.DEFAULT_JEKYLL.parse(" {% if null <= 0 %} true {% else %} false {% endif %} ").render(), is(" false ")); - assertThat(TemplateParser.DEFAULT.parse(" {% if 0 <= null %} true {% else %} false {% endif %} ").render(), is(" false ")); + assertThat(TemplateParser.DEFAULT_JEKYLL.parse(" {% if 0 <= null %} true {% else %} false {% endif %} ").render(), is(" false ")); } /* diff --git a/src/test/java/liqp/blocks/IfTest.java b/src/test/java/liqp/blocks/IfTest.java index 3f719c16..81ace433 100644 --- a/src/test/java/liqp/blocks/IfTest.java +++ b/src/test/java/liqp/blocks/IfTest.java @@ -308,15 +308,15 @@ public void nested_ifTest() throws RecognitionException { @Test public void comparisons_on_nullTest() throws RecognitionException { - assertThat(TemplateParser.DEFAULT.parse("{% if null < 10 %} NO {% endif %}").render(), is("")); - assertThat(TemplateParser.DEFAULT.parse("{% if null <= 10 %} NO {% endif %}").render(), is("")); - assertThat(TemplateParser.DEFAULT.parse("{% if null >= 10 %} NO {% endif %}").render(), is("")); - assertThat(TemplateParser.DEFAULT.parse("{% if null > 10 %} NO {% endif %}").render(), is("")); - - assertThat(TemplateParser.DEFAULT.parse("{% if 10 < null %} NO {% endif %}").render(), is("")); - assertThat(TemplateParser.DEFAULT.parse("{% if 10 <= null %} NO {% endif %}").render(), is("")); - assertThat(TemplateParser.DEFAULT.parse("{% if 10 >= null %} NO {% endif %}").render(), is("")); - assertThat(TemplateParser.DEFAULT.parse("{% if 10 > null %} NO {% endif %}").render(), is("")); + assertThat(TemplateParser.DEFAULT_JEKYLL.parse("{% if null < 10 %} NO {% endif %}").render(), is("")); + assertThat(TemplateParser.DEFAULT_JEKYLL.parse("{% if null <= 10 %} NO {% endif %}").render(), is("")); + assertThat(TemplateParser.DEFAULT_JEKYLL.parse("{% if null >= 10 %} NO {% endif %}").render(), is("")); + assertThat(TemplateParser.DEFAULT_JEKYLL.parse("{% if null > 10 %} NO {% endif %}").render(), is("")); + + assertThat(TemplateParser.DEFAULT_JEKYLL.parse("{% if 10 < null %} NO {% endif %}").render(), is("")); + assertThat(TemplateParser.DEFAULT_JEKYLL.parse("{% if 10 <= null %} NO {% endif %}").render(), is("")); + assertThat(TemplateParser.DEFAULT_JEKYLL.parse("{% if 10 >= null %} NO {% endif %}").render(), is("")); + assertThat(TemplateParser.DEFAULT_JEKYLL.parse("{% if 10 > null %} NO {% endif %}").render(), is("")); } /* diff --git a/src/test/java/liqp/nodes/ComparingExpressionNodeTest.java b/src/test/java/liqp/nodes/ComparingExpressionNodeTest.java new file mode 100644 index 00000000..8462bfc2 --- /dev/null +++ b/src/test/java/liqp/nodes/ComparingExpressionNodeTest.java @@ -0,0 +1,219 @@ +package liqp.nodes; + +import liqp.TemplateParser; +import liqp.parser.Flavor; +import org.junit.Test; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +public class ComparingExpressionNodeTest { + + /** + * this is ruby code that create product of all types and operators. the order is predictable + * so lets code result as single string + * where 1 is true, 0 is false and e is error + */ +// resArray = [] +// arr = [98, '98', true, false, nil] +// ops = ['>', '>=', '<', '<=', '==', '!='] +// arr.product(arr).product(ops).each do |pair| +// operator = pair[1] +// first = pair[0][0] +// second = pair[0][1] +// begin +// +// if first.class == String then first = "'#{first}'" end +// if first === nil then first = 'nil' end +// if second.class == String then second = "'#{second}'" end +// if second === nil then second = 'nil' end +// res = render({}, "{% if #{first} #{operator} #{second} %}true{% else %}false{% endif %}") +// if res === 'true' then resArray.push(1) else resArray.push(0) end +// # pp "#{first} #{operator} #{second} = #{res}" +// rescue Exception => e +// if first.class == String then first = "'#{first}'" end +// if first === nil then first = 'nil' end +// if second.class == String then second = "'#{second}'" end +// if second === nil then second = 'nil' end +// resArray.push('e') +// # pp "#{first} #{operator} #{second} = ERROR: #{e.message[/.*/]}" +// end +// +// end +// +// pp resArray.join('') + + // 010110eeee01000001000001000001eeee01010110000001000001000001000001000001000010000001000001000001000001000001000010000001000001000001000001000001000010 + + + @Test + public void testCombinationsStrict() { + + TemplateParser parser = new TemplateParser.Builder() + .withFlavor(Flavor.JEKYLL) + .withStrictTypedExpressions(true) + .build(); + + + String expected = "010110eeee01000001000001000001eeee01010110000001000001000001000001000001000010000001000001000001000001000001000010000001000001000001000001000001000010"; + testCombunationsStrctWithExpectation(expected, parser); + } + + // js code for the same: + // const f = (a, b) => [].concat(...a.map(d => b.map(e => [].concat(d, e)))); + //const cartesian = (a, b, ...c) => (b ? cartesian(f(a, b), ...c) : a); + //const isString = (el) => (typeof el === 'string' || el instanceof String); + //const wrapString = (str) => isString(str) ? '\'' + str + '\'' : str; + // + //resArray = []; + //arr = [98, '97', true, false, null]; + //ops = ['>', '>=', '<', '<=', '==', '!='] + // + //cartesian(arr, arr, ops).forEach(combo => { + // var operator = combo[2]; + // var first = combo[0]; + // var second = combo[1]; + // var res; + // try { + // switch (operator) { + // case '>': + // resArray.push(); + // res = first > second ? 1 : 0; + // break; + // case '>=': + // res = first >= second ? 1 : 0; + // break; + // case '<': + // res = first < second ? 1 : 0; + // break; + // case '<=': + // res = first <= second ? 1 : 0; + // break; + // case '==': + // res = first == second ? 1 : 0; + // break; + // case '!=': + // res = first != second ? 1 : 0; + // break; + // } + // } catch(e) { + // res = 'e'; + // } + // resArray.push(res); + // console.log(wrapString(first) + ' ' + operator + ' ' + wrapString(second) + ' = ' + res); + // + //}) + //console.log(resArray.join('')) + @Test + public void testCombinationsNonStrict() { + + TemplateParser parser = new TemplateParser.Builder() + .withFlavor(Flavor.JEKYLL) + .withStrictTypedExpressions(false) + .build(); + + // non-strict version does not have errors in it + String expected = "010110110001110001110001110001001101010110110001110001110001001101001101010110110001110001001101001101001101010110010101001101001101001101010101010110"; + testCombunationsStrctWithExpectation(expected, parser); + } + + private void testCombunationsStrctWithExpectation(String expected, TemplateParser parser) { + List> cases = cartesianProduct(Arrays.asList( + Arrays.asList(98, "97", true, false, null), + Arrays.asList(98, "97", true, false, null), + // ['>', '>=', '<', '<=', '==', '!='] + Arrays.asList(">", ">=", "<", "<=", "==", "!=") + )); + // iterate each char in expected string: + for (int i = 0; i < expected.length(); i++) { + List pair = cases.get(i); + String operator = (String) pair.get(2); + Serializable first = pair.get(0); + if ("97".equals(first)) first = "'97'"; + if (first == null) first = "nil"; + Serializable second = pair.get(1); + if ("97".equals(second)) second = "'97'"; + if (second == null) second = "nil"; + String expectedRes = expected.substring(i, i + 1); + switch (expectedRes) { + case "0": + expectedRes = "false"; + break; + case "1": + expectedRes = "true"; + break; + case "e": + expectedRes = "error"; + break; + } + String res = null; + try { + res = parser.parse("{% if " + first + " " + operator + " " + second + " %}true{% else %}false{% endif %}").render(); + } catch (Exception e) { + res = "error"; + } + String errorMessage = String.format("%s %s %s = %s, but was %s" , first, operator, second, expectedRes, res); + assertEquals(errorMessage, expectedRes, res); + } + } + + @Test + public void testStringComparisonAlphabetically() { + TemplateParser parser = new TemplateParser.Builder() + .withFlavor(Flavor.JEKYLL) + .withEvaluateInOutputTag(true) + .withStrictTypedExpressions(false) + .build(); + + String res = parser.parse("{{'98' > '197'}}").render(); + assertEquals("true", res); + } + + + @Test + public void testBugNo286Case1() { + TemplateParser parser = new TemplateParser.Builder() + .withFlavor(Flavor.JEKYLL) + .build(); + + String res = parser.parse("{%- assign value = 5.0 | round: 1 -%} {%- if value >= 4.0 and value <= 4.2 -%} Très bien {%- elsif value >= 4.3 and value <= 4.7 -%} Superbe {%- elsif value >= 4.8 and value <= 4.9 -%} Fabuleux {%- elsif value != 5.0 -%} Exceptionnel {% endif %}").render(); + + assertEquals("", res.trim()); + } + + @Test + public void testBugNo286Case2() { + TemplateParser parser = new TemplateParser.Builder() + .withFlavor(Flavor.JEKYLL) + .build(); + + String res = parser.parse("{%- assign value = 5.0 | round: 1 -%} {%- if value >= 4.0 and value <= 4.2 -%} Très bien {%- elsif value >= 4.3 and value <= 4.7 -%} Superbe {%- elsif value >= 4.8 and value <= 4.9 -%} Fabuleux {%- elsif value == 5.0 -%} Exceptionnel {% endif %}").render(); + + assertEquals("Exceptionnel", res.trim()); + } + + // from https://stackoverflow.com/questions/714108/cartesian-product-of-an-arbitrary-number-of-sets + protected List> cartesianProduct(List> lists) { + List> resultLists = new ArrayList>(); + if (lists.isEmpty()) { + resultLists.add(new ArrayList()); + return resultLists; + } else { + List firstList = lists.get(0); + List> remainingLists = cartesianProduct(lists.subList(1, lists.size())); + for (T condition : firstList) { + for (List remainingList : remainingLists) { + ArrayList resultList = new ArrayList(); + resultList.add(condition); + resultList.addAll(remainingList); + resultLists.add(resultList); + } + } + } + return resultLists; + } +} \ No newline at end of file diff --git a/src/test/java/liqp/nodes/GtEqNodeTest.java b/src/test/java/liqp/nodes/GtEqNodeTest.java index 910e34f5..a6d1df92 100644 --- a/src/test/java/liqp/nodes/GtEqNodeTest.java +++ b/src/test/java/liqp/nodes/GtEqNodeTest.java @@ -36,7 +36,7 @@ public void applyTest() throws RecognitionException { for (String[] test : tests) { - Template template = TemplateParser.DEFAULT.parse(test[0]); + Template template = TemplateParser.DEFAULT_JEKYLL.parse(test[0]); String rendered = String.valueOf(template.render()); assertThat("template: [" + test[0]+"], expected:["+test[1]+"], real: [" + rendered + "]",rendered, is(test[1])); diff --git a/src/test/java/liqp/nodes/GtNodeTest.java b/src/test/java/liqp/nodes/GtNodeTest.java index 457f262e..4cb2d34e 100644 --- a/src/test/java/liqp/nodes/GtNodeTest.java +++ b/src/test/java/liqp/nodes/GtNodeTest.java @@ -41,7 +41,7 @@ public void applyTest() throws RecognitionException { for (String[] test : tests) { - Template template = TemplateParser.DEFAULT.parse(test[0]); + Template template = TemplateParser.DEFAULT_JEKYLL.parse(test[0]); String rendered = String.valueOf(template.render()); assertThat(rendered, is(test[1])); diff --git a/src/test/java/liqp/nodes/LtEqNodeTest.java b/src/test/java/liqp/nodes/LtEqNodeTest.java index 97efd501..d84e54de 100644 --- a/src/test/java/liqp/nodes/LtEqNodeTest.java +++ b/src/test/java/liqp/nodes/LtEqNodeTest.java @@ -36,7 +36,7 @@ public void applyTest() throws RecognitionException { for (String[] test : tests) { - Template template = TemplateParser.DEFAULT.parse(test[0]); + Template template = TemplateParser.DEFAULT_JEKYLL.parse(test[0]); String rendered = String.valueOf(template.render()); assertThat(rendered, is(test[1])); diff --git a/src/test/java/liqp/nodes/LtNodeTest.java b/src/test/java/liqp/nodes/LtNodeTest.java index fc736262..657abcd2 100644 --- a/src/test/java/liqp/nodes/LtNodeTest.java +++ b/src/test/java/liqp/nodes/LtNodeTest.java @@ -36,7 +36,7 @@ public void applyTest() throws RecognitionException { for (String[] test : tests) { - Template template = TemplateParser.DEFAULT.parse(test[0]); + Template template = TemplateParser.DEFAULT_JEKYLL.parse(test[0]); String rendered = String.valueOf(template.render()); assertThat(rendered, is(test[1]));