FREEMARKER-86: Added new built-ins: sequence?min and sequence?max (FREEMARKER-86), which return the smallest and greatest item from a list of numbers or date/time/date-times.
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/0c77097a Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/0c77097a Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/0c77097a Branch: refs/heads/2.3 Commit: 0c77097aac210ba6b89e957ccaa6d4ca160dee21 Parents: 15e8ac3 Author: ddekany <ddek...@apache.org> Authored: Mon Mar 12 07:02:51 2018 +0100 Committer: ddekany <ddek...@apache.org> Committed: Mon Mar 12 07:02:51 2018 +0100 ---------------------------------------------------------------------- src/main/java/freemarker/core/BuiltIn.java | 4 +- .../freemarker/core/BuiltInsForSequences.java | 68 +++++++++++++++ src/manual/en_US/book.xml | 52 ++++++++++++ src/test/java/freemarker/core/MinMaxBITest.java | 87 ++++++++++++++++++++ 4 files changed, 210 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/0c77097a/src/main/java/freemarker/core/BuiltIn.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java index 78b9e45..6a6ad37 100644 --- a/src/main/java/freemarker/core/BuiltIn.java +++ b/src/main/java/freemarker/core/BuiltIn.java @@ -84,7 +84,7 @@ abstract class BuiltIn extends Expression implements Cloneable { static final Set<String> CAMEL_CASE_NAMES = new TreeSet<String>(); static final Set<String> SNAKE_CASE_NAMES = new TreeSet<String>(); - static final int NUMBER_OF_BIS = 266; + static final int NUMBER_OF_BIS = 268; static final HashMap<String, BuiltIn> BUILT_INS_BY_NAME = new HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f); static { @@ -245,6 +245,8 @@ abstract class BuiltIn extends Expression implements Cloneable { putBI("node_namespace", "nodeNamespace", new node_namespaceBI()); putBI("node_type", "nodeType", new node_typeBI()); putBI("no_esc", "noEsc", new no_escBI()); + putBI("max", new BuiltInsForSequences.maxBI()); + putBI("min", new BuiltInsForSequences.minBI()); putBI("number", new BuiltInsForStringsMisc.numberBI()); putBI("number_to_date", "numberToDate", new number_to_dateBI(TemplateDateModel.DATE)); putBI("number_to_time", "numberToTime", new number_to_dateBI(TemplateDateModel.TIME)); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/0c77097a/src/main/java/freemarker/core/BuiltInsForSequences.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/BuiltInsForSequences.java b/src/main/java/freemarker/core/BuiltInsForSequences.java index a58c7f6..9bc8d95 100644 --- a/src/main/java/freemarker/core/BuiltInsForSequences.java +++ b/src/main/java/freemarker/core/BuiltInsForSequences.java @@ -884,6 +884,74 @@ class BuiltInsForSequences { } } + private static abstract class MinOrMaxBI extends BuiltIn { + + private final int comparatorOperator; + + protected MinOrMaxBI(int comparatorOperator) { + this.comparatorOperator = comparatorOperator; + } + + @Override + TemplateModel _eval(Environment env) + throws TemplateException { + TemplateModel model = target.eval(env); + if (model instanceof TemplateCollectionModel) { + return calculateResultForColletion((TemplateCollectionModel) model, env); + } else if (model instanceof TemplateSequenceModel) { + return calculateResultForSequence((TemplateSequenceModel) model, env); + } else { + throw new NonSequenceOrCollectionException(target, model, env); + } + } + + private TemplateModel calculateResultForColletion(TemplateCollectionModel coll, Environment env) + throws TemplateException { + TemplateModel best = null; + TemplateModelIterator iter = coll.iterator(); + while (iter.hasNext()) { + TemplateModel cur = iter.next(); + if (cur != null + && (best == null || EvalUtil.compare(cur, null, comparatorOperator, null, best, + null, this, true, false, false, false, env))) { + best = cur; + } + } + return best; + } + + private TemplateModel calculateResultForSequence(TemplateSequenceModel seq, Environment env) + throws TemplateException { + TemplateModel best = null; + for (int i = 0; i < seq.size(); i++) { + TemplateModel cur = seq.get(i); + if (cur != null + && (best == null || EvalUtil.compare(cur, null, comparatorOperator, null, best, + null, this, true, false, false, false, env))) { + best = cur; + } + } + return best; + } + + } + + static class maxBI extends MinOrMaxBI { + + public maxBI() { + super(EvalUtil.CMP_OP_GREATER_THAN); + } + + } + + static class minBI extends MinOrMaxBI { + + public minBI() { + super(EvalUtil.CMP_OP_LESS_THAN); + } + + } + // Can't be instantiated private BuiltInsForSequences() { } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/0c77097a/src/manual/en_US/book.xml ---------------------------------------------------------------------- diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index db1d1f3..4b89a2f 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -12740,6 +12740,14 @@ grant codeBase "file:/path/to/freemarker.jar" </listitem> <listitem> + <para><link linkend="ref_builtin_min_max">max</link></para> + </listitem> + + <listitem> + <para><link linkend="ref_builtin_min_max">min</link></para> + </listitem> + + <listitem> <para><link linkend="ref_builtin_namespace">namespace</link></para> </listitem> @@ -16664,6 +16672,41 @@ red, green, blue. die with error if the sequence is empty.</para> </section> + <section xml:id="ref_builtin_min_max"> + <title>min, max</title> + + <indexterm> + <primary>min built-in</primary> + </indexterm> + + <indexterm> + <primary>max built-in</primary> + </indexterm> + + <para>Returns the smaller (<literal>min</literal>) or greatest + (<literal>max</literal>) item of the sequence (or collection). The + items must be either all numbers, or all date/time values of the + same kind (date-only, time-only, date-time), or else a comparison + error will occur. These are the same restrictions as for the <link + linkend="dgui_template_exp_comparison"><literal><</literal> and + <literal>></literal> operators</link>.</para> + + <para>Missing items (i.e., Java <literal>null</literal>-s) will be + silently ignored. If the sequence is empty or it only contains + missing (Java <literal>null</literal>) items, the result itself will + be missing.</para> + + <para>Example:</para> + + <programlisting role="template">${[1, 2, 3]?min} +${[1, 2, 3]?max} +${[]?min!'-'}</programlisting> + + <programlisting role="output">1 +3 +-</programlisting> + </section> + <section xml:id="ref_builtin_reverse"> <title>reverse</title> @@ -27375,6 +27418,15 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> </listitem> <listitem> + <para>Added new built-ins: <literal>sequence?min</literal> and + <literal>sequence?max</literal> (<link + xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-86">FREEMARKER-86</link>), + which return the smallest and greatest item from a list of + numbers or date/time/date-times. <link + linkend="ref_builtin_min_max">See more here...</link></para> + </listitem> + + <listitem> <para>Bug fixed (<link xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-83">FREEMARKER-83</link>); this fix is only active when <link http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/0c77097a/src/test/java/freemarker/core/MinMaxBITest.java ---------------------------------------------------------------------- diff --git a/src/test/java/freemarker/core/MinMaxBITest.java b/src/test/java/freemarker/core/MinMaxBITest.java new file mode 100644 index 0000000..3bff7ba --- /dev/null +++ b/src/test/java/freemarker/core/MinMaxBITest.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package freemarker.core; + +import java.sql.Time; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import com.google.common.collect.ImmutableList; + +import freemarker.template.DefaultIterableAdapter; +import freemarker.template.utility.DateUtil; +import freemarker.template.utility.ObjectWrapperWithAPISupport; +import freemarker.test.TemplateTest; + +public class MinMaxBITest extends TemplateTest { + + @Test + public void basicsTest() throws Exception { + getConfiguration().setTimeZone(DateUtil.UTC); + getConfiguration().setTimeFormat("HH:mm:ss"); + + ObjectWrapperWithAPISupport ow = (ObjectWrapperWithAPISupport) getConfiguration().getObjectWrapper(); + for (boolean exposeAsSeq : new boolean[] { true, false }) { // Expose xs as SequenceTM or as CollectionTM + for (InputMinMax testParams : ImmutableList.of( + // Test parameters: List (xs), Expected result for `?min`, For `?max` + new InputMinMax(ImmutableList.of(1, 2, 3), "1", "3"), + new InputMinMax(ImmutableList.of(3, 2, 1), "1", "3"), + new InputMinMax(ImmutableList.of(1, 3, 2), "1", "3"), + new InputMinMax(ImmutableList.of(2, 1, 3), "1", "3"), + new InputMinMax(ImmutableList.of(2), "2", "2"), + new InputMinMax(Collections.emptyList(), "-", "-"), + new InputMinMax(ImmutableList.of(1.5, -0.5, 1L, 2.25), "-0.5", "2.25"), + new InputMinMax(ImmutableList.of(Double.NEGATIVE_INFINITY, 1, Double.POSITIVE_INFINITY), + "-\u221E", "\u221E"), // \u221E = â + new InputMinMax(Arrays.asList(new Object[] { null, 1, null, 2, null }), "1", "2"), + new InputMinMax(Arrays.asList(new Object[] { null, null, null }), "-", "-"), + new InputMinMax(ImmutableList.of(new Time(2000), new Time(3000), new Time(1000)), + "00:00:01", "00:00:03") + )) { + addToDataModel("xs", + exposeAsSeq ? testParams.input : DefaultIterableAdapter.adapt(testParams.input, ow)); + assertOutput("${xs?min!'-'}", testParams.minExpected); + assertOutput("${xs?max!'-'}", testParams.maxExpected); + } + } + } + + private class InputMinMax { + private final List<?> input; + private final String minExpected; + private final String maxExpected; + + public InputMinMax(List<?> input, String minExpected, String maxExpected) { + this.input = input; + this.minExpected = minExpected; + this.maxExpected = maxExpected; + } + } + + @Test + public void comparisonErrorTest() { + assertErrorContains("${['a', 'x']?min}", "less-than", "string"); + assertErrorContains("${[0, true]?min}", "number", "boolean"); + } + +}