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>&lt;</literal> and
+          <literal>&gt;</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");
+    }
+    
+}

Reply via email to