Skip to content

Allow min max to work with dates#152

Closed
jva wants to merge 9 commits into
masterfrom
feature/allow_min_max_to_work_with_dates
Closed

Allow min max to work with dates#152
jva wants to merge 9 commits into
masterfrom
feature/allow_min_max_to_work_with_dates

Conversation

@jva
Copy link
Copy Markdown
Collaborator

@jva jva commented Mar 19, 2026

Min/Max now work with dates.

Tried to create a comprehensive coverage of tests, as well as for failing edge cases that were discovered during development. The pom.xml changes are for compilation for a person that does not have a cache of the needed Maven artifacts. Also for running the Mondrian's test suite with mvn verify -DrunITs. That helped identify a few initial regressions.

The added FormatAwareFunDef is also a step in the direction of UDFs being able to opt in and tell the caller their type instead of relying on existing check which scans the params depth-first and finds the first argument with known type. This will be used later, e.g. - DateDiffDays has two date parameters, but the default formatting of the return value will be numeric instead of currently detected date.

diff --git a/mondrian/pom.xml b/mondrian/pom.xml
index 46fdd10df..43e2139bb 100644
--- a/mondrian/pom.xml
+++ b/mondrian/pom.xml
@@ -119,6 +119,12 @@
       <groupId>eigenbase</groupId>
       <artifactId>eigenbase-resgen</artifactId>
       <version>${eigenbase-resgen.version}</version>
+      <exclusions>
+        <exclusion>
+          <groupId>eigenbase</groupId>
+          <artifactId>eigenbase-xom</artifactId>
+        </exclusion>
+      </exclusions>
     </dependency>
     <dependency>
       <groupId>org.olap4j</groupId>
@@ -249,6 +255,32 @@
               </sources>
             </configuration>
           </execution>
+          <execution>
+            <id>add-test-source</id>
+            <phase>generate-test-sources</phase>
+            <goals>
+              <goal>add-test-source</goal>
+            </goals>
+            <configuration>
+              <sources>
+                <source>src/it/java</source>
+              </sources>
+            </configuration>
+          </execution>
+          <execution>
+            <id>add-test-resource</id>
+            <phase>generate-test-resources</phase>
+            <goals>
+              <goal>add-test-resource</goal>
+            </goals>
+            <configuration>
+              <resources>
+                <resource>
+                  <directory>src/it/resources</directory>
+                </resource>
+              </resources>
+            </configuration>
+          </execution>
           <execution>
             <id>add-resource</id>
             <phase>generate-resources</phase>
@@ -306,6 +338,12 @@
           </transformationSets>
         </configuration>
       </plugin>
+      <plugin>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <skipTests>true</skipTests>
+        </configuration>
+      </plugin>
       <plugin>
         <artifactId>maven-failsafe-plugin</artifactId>
         <executions>
@@ -376,7 +414,7 @@
           <plugin>
             <groupId>io.fabric8</groupId>
             <artifactId>docker-maven-plugin</artifactId>
-            <version>0.26.0</version>
+            <version>0.45.1</version>
             <executions>
               <execution>
                 <id>start</id>
@@ -398,7 +436,7 @@
             <configuration>
               <images>
                 <image>
-                  <name>${pentaho.docker.pull.host}mysql:8.0.27</name>
+                  <name>mysql:8.0</name>
                   <alias>mondrian-mysql8027-db</alias>
                   <run>
                     <!-- prevents spam of mbind: Operation not permitted log messages -->
@@ -508,9 +546,9 @@
         <mondrian.foodmart.jdbcUser>mondrian</mondrian.foodmart.jdbcUser>
         <mondrian.jdbcDrivers>${mysql.driver}</mondrian.jdbcDrivers>
         <mysql.driver>com.mysql.cj.jdbc.Driver</mysql.driver>
-        <mondrian.foodmart.jdbcURL>jdbc:mysql://${docker.container.mondrian-mysql8027-db.ip}:3306/foodmart</mondrian.foodmart.jdbcURL>
+        <mondrian.foodmart.jdbcURL>jdbc:mysql://localhost:3306/foodmart</mondrian.foodmart.jdbcURL>
         <dependency.sql-maven-plugin.version>1.5</dependency.sql-maven-plugin.version>
-        <mysql.url.default>jdbc:mysql://${docker.container.mondrian-mysql8027-db.ip}:3306/mysql</mysql.url.default>
+        <mysql.url.default>jdbc:mysql://localhost:3306/mysql</mysql.url.default>
       </properties>
     </profile>
     <profile>
diff --git a/mondrian/src/it/java/mondrian/rolap/cache/SegmentCacheIndexImplTest.java b/mondrian/src/it/java/mondrian/rolap/cache/SegmentCacheIndexImplTest.java
index 138f80cd8..c45edb3db 100755
--- a/mondrian/src/it/java/mondrian/rolap/cache/SegmentCacheIndexImplTest.java
+++ b/mondrian/src/it/java/mondrian/rolap/cache/SegmentCacheIndexImplTest.java
@@ -18,7 +18,7 @@ import mondrian.test.FoodMartTestCase;
 public class SegmentCacheIndexImplTest extends FoodMartTestCase {
     public void testNoHeaderOnLoad() {
         final SegmentCacheIndexImpl index =
-            new SegmentCacheIndexImpl(Thread.currentThread());
+            new SegmentCacheIndexImpl(new Thread[]{Thread.currentThread()});
 
         final SegmentHeader header = mock(SegmentHeader.class);
         final SegmentBody body = mock(SegmentBody.class);
diff --git a/mondrian/src/main/java/mondrian/mdx/ResolvedFunCall.java b/mondrian/src/main/java/mondrian/mdx/ResolvedFunCall.java
index 7989f20b0..d2adb29e7 100644
--- a/mondrian/src/main/java/mondrian/mdx/ResolvedFunCall.java
+++ b/mondrian/src/main/java/mondrian/mdx/ResolvedFunCall.java
@@ -125,6 +125,20 @@ public final class ResolvedFunCall extends ExpBase implements FunCall {
         return funDef;
     }
 
+    /**
+     * Returns a {@link FormatAwareFunDef} if the underlying function
+     * implements it, or null otherwise. The caller should check the
+     * return value of {@link FormatAwareFunDef#getFormatExpIndex} for
+     * {@link FormatAwareFunDef#NOT_PARTICIPATING} to determine if the
+     * function actually opts in.
+     */
+    public FormatAwareFunDef getFormatAwareFunDef() {
+        if (funDef instanceof FormatAwareFunDef) {
+            return (FormatAwareFunDef) funDef;
+        }
+        return null;
+    }
+
     public final int getCategory() {
         return funDef.getReturnCategory();
     }
diff --git a/mondrian/src/main/java/mondrian/olap/FormatAwareFunDef.java b/mondrian/src/main/java/mondrian/olap/FormatAwareFunDef.java
new file mode 100644
index 000000000..078044c35
--- /dev/null
+++ b/mondrian/src/main/java/mondrian/olap/FormatAwareFunDef.java
@@ -0,0 +1,57 @@
+/*
+// This software is subject to the terms of the Eclipse Public License v1.0
+// Agreement, available at the following URL:
+// http://www.eclipse.org/legal/epl-v10.html.
+// You must accept the terms of that agreement to use this software.
+//
+// Copyright (C) 2026 eazyBI
+// All Rights Reserved.
+*/
+
+package mondrian.olap;
+
+/**
+ * Interface for functions that want to control which format string is
+ * inferred for calculated members using this function.
+ *
+ * <p>When a calculated member has no explicit FORMAT_STRING and its
+ * defining expression is a call to a function implementing this interface,
+ * {@link Formula} will call {@link #getFormatExpIndex(Exp[])} to determine
+ * which argument's format to use, instead of doing a depth-first walk
+ * that picks the first measure it encounters.
+ *
+ * <p>Can be implemented by {@link FunDef} implementations directly
+ * (e.g., Min/Max), or by {@link mondrian.spi.UserDefinedFunction}
+ * implementations — the UDF adapter in UdfResolver delegates automatically.
+ *
+ * @since Mar 2026
+ */
+public interface FormatAwareFunDef {
+    /**
+     * Sentinel value indicating the function does not participate in
+     * format-aware resolution (e.g., a UDF wrapper where the actual UDF
+     * does not implement this interface). Formula will fall through to
+     * the default format-finding behavior.
+     */
+    int NOT_PARTICIPATING = Integer.MIN_VALUE;
+
+    /**
+     * Returns the index of the argument whose format string should be
+     * used for the result of this function call.
+     *
+     * <p>Return values:
+     * <ul>
+     * <li>{@code >= 0}: use the format from the argument at this index</li>
+     * <li>{@code -1}: skip format inference from arguments entirely
+     *     (useful when the function's result type differs from all
+     *     argument types, e.g., DateDiffDays returns a number from
+     *     two date arguments)</li>
+     * <li>{@link #NOT_PARTICIPATING}: this function does not participate;
+     *     fall through to the default format-finding behavior</li>
+     * </ul>
+     *
+     * @param args the arguments to the function call
+     * @return argument index, -1 to skip, or NOT_PARTICIPATING
+     */
+    int getFormatExpIndex(Exp[] args);
+}
diff --git a/mondrian/src/main/java/mondrian/olap/Formula.java b/mondrian/src/main/java/mondrian/olap/Formula.java
index 43490aa31..6957fc559 100644
--- a/mondrian/src/main/java/mondrian/olap/Formula.java
+++ b/mondrian/src/main/java/mondrian/olap/Formula.java
@@ -498,6 +498,32 @@ public class Formula extends QueryPart {
             return null;
         }
 
+        // If the expression is a function that implements FormatAwareFunDef
+        // (directly on the FunDef, or via a wrapped UDF), let it control
+        // which argument's format to use.
+        if (exp instanceof ResolvedFunCall) {
+            ResolvedFunCall call = (ResolvedFunCall) exp;
+            FormatAwareFunDef formatAware = call.getFormatAwareFunDef();
+            if (formatAware != null) {
+                int index = formatAware.getFormatExpIndex(call.getArgs());
+                if (index == -1) {
+                    // Function explicitly opts out of format inheritance
+                    return null;
+                }
+                if (index != FormatAwareFunDef.NOT_PARTICIPATING
+                    && index >= 0 && index < call.getArgCount())
+                {
+                    try {
+                        call.getArg(index).accept(
+                            new FormatFinder(validator));
+                    } catch (FoundOne foundOne) {
+                        return foundOne.exp;
+                    }
+                }
+                // NOT_PARTICIPATING: fall through to default behavior
+            }
+        }
+
         // Burrow into the expression. If we find a member, use its format
         // string.
         try {
diff --git a/mondrian/src/main/java/mondrian/olap/fun/MinMaxFunDef.java b/mondrian/src/main/java/mondrian/olap/fun/MinMaxFunDef.java
index 83961d6b7..29b16ec6c 100644
--- a/mondrian/src/main/java/mondrian/olap/fun/MinMaxFunDef.java
+++ b/mondrian/src/main/java/mondrian/olap/fun/MinMaxFunDef.java
@@ -5,64 +5,259 @@
 * You must accept the terms of that agreement to use this software.
 *
 * Copyright (c) 2002-2021 Hitachi Vantara..  All rights reserved.
+* Copyright (c) 2026 eazyBI.  All rights reserved.
 */
 
 package mondrian.olap.fun;
 
 import mondrian.calc.*;
+import mondrian.calc.impl.AbstractCalc;
 import mondrian.calc.impl.AbstractDoubleCalc;
 import mondrian.calc.impl.ValueCalc;
 import mondrian.mdx.ResolvedFunCall;
 import mondrian.olap.*;
+import mondrian.olap.FormatAwareFunDef;
+
+import java.util.Date;
+import java.util.List;
 
 /**
  * Definition of the <code>Min</code> and <code>Max</code> MDX functions.
+ * 
+ * <p>Supports both numeric and date expressions. Mondrian calculated members
+ * are always statically typed as Numeric regardless of their formula's actual
+ * return type, so date support relies on runtime type detection in
+ * {@link #extremeValue}. Format string inference is handled by
+ * {@link FormatAwareFunDef} to ensure the value expression's format is used
+ * rather than a measure from a Filter condition.
+ * PATCH - added support for date.
  *
  * @author jhyde
  * @since Mar 23, 2006
  */
-class MinMaxFunDef extends AbstractAggregateFunDef {
-  static final ReflectiveMultiResolver MinResolver =
-      new ReflectiveMultiResolver( "Min", "Min(<Set>[, <Numeric Expression>])",
-          "Returns the minimum value of a numeric expression evaluated over a set.", new String[] { "fnx", "fnxn" },
-          MinMaxFunDef.class );
-
-  static final MultiResolver MaxResolver =
-      new ReflectiveMultiResolver( "Max", "Max(<Set>[, <Numeric Expression>])",
-          "Returns the maximum value of a numeric expression evaluated over a set.", new String[] { "fnx", "fnxn" },
-          MinMaxFunDef.class );
-  private static final String TIMING_NAME = MinMaxFunDef.class.getSimpleName();
-
-  private final boolean max;
-
-  public MinMaxFunDef( FunDef dummyFunDef ) {
-    super( dummyFunDef );
-    this.max = dummyFunDef.getName().equals( "Max" );
-  }
-
-  public Calc compileCall( ResolvedFunCall call, ExpCompiler compiler ) {
-    final ListCalc listCalc = compiler.compileList( call.getArg( 0 ) );
-    final Calc calc =
-        call.getArgCount() > 1 ? compiler.compileScalar( call.getArg( 1 ), true ) : new ValueCalc( call );
-    return new AbstractDoubleCalc( call, new Calc[] { listCalc, calc } ) {
-      public double evaluateDouble( Evaluator evaluator ) {
-        evaluator.getTiming().markStart( TIMING_NAME );
-        final int savepoint = evaluator.savepoint();
-        try {
-          TupleList memberList = evaluateCurrentList( listCalc, evaluator );
-          evaluator.setNonEmpty( false );
-          return (Double) ( max ? max( evaluator, memberList, calc ) : min( evaluator, memberList, calc ) );
-        } finally {
-          evaluator.restore( savepoint );
-          evaluator.getTiming().markEnd( TIMING_NAME );
+class MinMaxFunDef extends AbstractAggregateFunDef
+    implements FormatAwareFunDef
+{
+    // Extends ReflectiveMultiResolver to maintain binary compatibility with
+    // BuiltinFunTable which expects these exact field types.
+    static final ReflectiveMultiResolver MinResolver =
+        new MinMaxResolverImpl(
+            "Min",
+            "Min(<Set>[, <Numeric Expression>])",
+            "Returns the minimum value of a numeric or date expression "
+                + "evaluated over a set.");
+
+    static final MultiResolver MaxResolver =
+        new MinMaxResolverImpl(
+            "Max",
+            "Max(<Set>[, <Numeric Expression>])",
+            "Returns the maximum value of a numeric or date expression "
+                + "evaluated over a set.");
+
+    private static final String TIMING_NAME = MinMaxFunDef.class.getSimpleName();
+    private final boolean max;
+
+    public MinMaxFunDef(FunDef dummyFunDef) {
+        super(dummyFunDef);
+        this.max = dummyFunDef.getName().equals("Max");
+    }
+
+    /**
+     * For the 2-arg form, use the value expression (last arg) for format
+     * inference instead of the default depth-first walk which may find a
+     * numeric measure in a Filter condition first. For the 1-arg form,
+     * fall through to the default format-finding behavior.
+     */
+    public int getFormatExpIndex(Exp[] args) {
+        return args.length >= 2 ? args.length - 1 : NOT_PARTICIPATING;
+    }
+
+    public Calc compileCall(ResolvedFunCall call, ExpCompiler compiler) {
+        final ListCalc listCalc =
+            compiler.compileList(call.getArg(0));
+        // compileScalar(arg, false) avoids numeric type coercion that would
+        // convert Date values to numbers when Filter is involved. The numeric
+        // path is unaffected because evaluateSet coerces Numbers via
+        // doubleValue() at runtime.
+        final Calc calc =
+            call.getArgCount() > 1
+            ? compiler.compileScalar(call.getArg(1), false)
+            : new ValueCalc(call);
+        if (getReturnCategory() == Category.Numeric) {
+            // Numeric return type (1-arg form and 2-arg with numeric expr).
+            // Uses AbstractDoubleCalc so callers like IIf can compile the
+            // result as a double. The evaluate() override passes through
+            // Date values directly for calculated measures that are typed
+            // as Numeric but return dates at runtime.
+            return new AbstractDoubleCalc(
+                call, new Calc[] {listCalc, calc})
+            {
+                public Object evaluate(Evaluator evaluator) {
+                    evaluator.getTiming().markStart(TIMING_NAME);
+                    final int savepoint = evaluator.savepoint();
+                    try {
+                        TupleList memberList =
+                            evaluateCurrentList(listCalc, evaluator);
+                        evaluator.setNonEmpty(false);
+                        Object result = max
+                            ? maxValue(evaluator, memberList, calc)
+                            : minValue(evaluator, memberList, calc);
+                        if (result instanceof Number) {
+                            double d = ((Number) result).doubleValue();
+                            return d == FunUtil.DoubleNull ? null : d;
+                        }
+                        return result;
+                    } finally {
+                        evaluator.restore(savepoint);
+                        evaluator.getTiming().markEnd(TIMING_NAME);
+                    }
+                }
+
+                public double evaluateDouble(Evaluator evaluator) {
+                    Object result = evaluate(evaluator);
+                    if (result instanceof Number) {
+                        return ((Number) result).doubleValue();
+                    }
+                    return FunUtil.DoubleNull;
+                }
+
+                public boolean dependsOn(Hierarchy hierarchy) {
+                    return anyDependsButFirst(getCalcs(), hierarchy);
+                }
+            };
         }
-      }
+        // Non-numeric return type (2-arg with DateTime expr).
+        return new AbstractCalc(call, new Calc[] {listCalc, calc}) {
+            public Object evaluate(Evaluator evaluator) {
+                evaluator.getTiming().markStart(TIMING_NAME);
+                final int savepoint = evaluator.savepoint();
+                try {
+                    TupleList memberList =
+                        evaluateCurrentList(listCalc, evaluator);
+                    evaluator.setNonEmpty(false);
+                    return max
+                        ? maxValue(evaluator, memberList, calc)
+                        : minValue(evaluator, memberList, calc);
+                } finally {
+                    evaluator.restore(savepoint);
+                    evaluator.getTiming().markEnd(TIMING_NAME);
+                }
+            }
+
+            public boolean dependsOn(Hierarchy hierarchy) {
+                return anyDependsButFirst(getCalcs(), hierarchy);
+            }
+        };
+    }
+
+    private static Object minValue(
+        Evaluator evaluator, TupleList members, Calc calc)
+    {
+        return extremeValue(evaluator, members, calc, false);
+    }
+
+    private static Object maxValue(
+        Evaluator evaluator, TupleList members, Calc calc)
+    {
+        return extremeValue(evaluator, members, calc, true);
+    }
 
-      public boolean dependsOn( Hierarchy hierarchy ) {
-        return anyDependsButFirst( getCalcs(), hierarchy );
-      }
-    };
-  }
+    /**
+     * Evaluates the expression for each tuple in the set and returns the
+     * minimum or maximum value. Handles both Number and Date values at
+     * runtime based on the actual type of the first non-null value.
+     */
+    private static Object extremeValue(
+        Evaluator evaluator, TupleList members, Calc calc, boolean max)
+    {
+        SetWrapper sw = evaluateSet(evaluator, members, calc);
+        if (sw.errorCount > 0) {
+            return Double.NaN;
+        }
+        final List v = sw.v;
+        final int size = v.size();
+        if (size == 0) {
+            return Util.nullValue;
+        }
+        Object extreme = v.get(0);
+        if (extreme instanceof Date) {
+            for (int i = 1; i < size; i++) {
+                Date d = (Date) v.get(i);
+                if (max
+                    ? d.getTime() > ((Date) extreme).getTime()
+                    : d.getTime() < ((Date) extreme).getTime())
+                {
+                    extreme = d;
+                }
+            }
+            return extreme;
+        }
+        double extremeDouble = ((Number) extreme).doubleValue();
+        for (int i = 1; i < size; i++) {
+            double iValue = ((Number) v.get(i)).doubleValue();
+            if (max ? iValue > extremeDouble : iValue < extremeDouble) {
+                extremeDouble = iValue;
+            }
+        }
+        return extremeDouble;
+    }
+
+    /**
+     * Custom resolver that tries DateTime first, then Numeric for the
+     * 2-arg form. The 1-arg form delegates to the standard "fnx" signature.
+     *
+     * <p>Extends ReflectiveMultiResolver for binary compatibility with
+     * BuiltinFunTable which references these fields with specific types.
+     */
+    private static class MinMaxResolverImpl extends ReflectiveMultiResolver {
+        MinMaxResolverImpl(String name, String signature, String description) {
+            super(name, signature, description,
+                new String[]{"fnx"},
+                MinMaxFunDef.class);
+        }
+
+        public FunDef resolve(
+            Exp[] args,
+            Validator validator,
+            List<Conversion> conversions)
+        {
+            if (args.length == 1) {
+                return super.resolve(args, validator, conversions);
+            }
+            if (args.length == 2) {
+                FunDef dateFun = resolveSetAndExpression(
+                    args, validator, conversions, Category.DateTime);
+                if (dateFun != null) {
+                    return dateFun;
+                }
+                return resolveSetAndExpression(
+                    args, validator, conversions, Category.Numeric);
+            }
+            return null;
+        }
+
+        private FunDef resolveSetAndExpression(
+            Exp[] args,
+            Validator validator,
+            List<Conversion> conversions,
+            int expressionCategory)
+        {
+            conversions.clear();
+            if (!validator.canConvert(
+                    0, args[0], Category.Set, conversions))
+            {
+                return null;
+            }
+            if (!validator.canConvert(
+                    1, args[1], expressionCategory, conversions))
+            {
+                return null;
+            }
+            FunDef dummy = createDummyFunDef(
+                this, expressionCategory, args);
+            return new MinMaxFunDef(dummy);
+        }
+    }
 }
 
 // End MinMaxFunDef.java
diff --git a/pom.xml b/pom.xml
index 4c9fa6c6b..5452d5579 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,7 +26,7 @@
 
     <olap4j.version>1.2.0</olap4j.version>
     <eigenbase-properties.version>1.1.2</eigenbase-properties.version>
-    <commons-io.version>1.4</commons-io.version>
+    <commons-io.version>2.18.0</commons-io.version>
     <mysql-connector-java.version>8.0.27</mysql-connector-java.version>
     <olap4j-tck.version>1.0.1.539</olap4j-tck.version>
     <eigenbase-resgen.version>1.3.1</eigenbase-resgen.version>
@@ -54,6 +54,7 @@
     <maven.compiler.target>1.8</maven.compiler.target>
     <source.jdk.version>1.8</source.jdk.version>
     <target.jdk.version>1.8</target.jdk.version>
+    <release.jdk.version>8</release.jdk.version>
     <project.build.debug>true</project.build.debug>
   </properties>
   <repositories>

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends Mondrian’s Min/Max behavior to support DateTime expressions (including correct format-string inference), and adds integration/spec coverage to validate date and numeric behavior across common query patterns (Filter, IIf, nesting, etc.).

Changes:

  • Add a FormatAwareFunDef opt-in mechanism so functions/UDF adapters can explicitly control which argument drives FORMAT_STRING inference.
  • Update Min/Max implementation and format inference logic to handle DateTime results correctly (including in the presence of Filter).
  • Add RSpec coverage for date-based Min/Max behavior and a few related edge cases/regressions.

Reviewed changes

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
mondrian/pom.xml Build/test lifecycle changes to support IT sources/resources and updated plugin/config behavior.
mondrian/src/it/java/mondrian/rolap/cache/SegmentCacheIndexImplTest.java Adjust test construction for updated SegmentCacheIndexImpl API (thread array).
mondrian/src/main/java/mondrian/mdx/ResolvedFunCall.java Exposes FormatAwareFunDef capability from resolved function calls.
mondrian/src/main/java/mondrian/olap/FormatAwareFunDef.java New interface allowing functions/UDFs to control format-string inference source.
mondrian/src/main/java/mondrian/olap/Formula.java Uses FormatAwareFunDef (when present) to select format inheritance argument.
mondrian/src/main/java/mondrian/olap/fun/MinMaxFunDef.java Extends Min/Max runtime evaluation to handle DateTime values and participates in format inference.
pom.xml Updates dependency versions / compiler properties used by the overall build.
spec/mondrian_spec.rb Adds specs covering DateTime and numeric Min/Max, including formatting and edge cases.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread spec/mondrian_spec.rb
Comment thread spec/mondrian_spec.rb Outdated
@jva jva assigned jva and unassigned jjustaments Mar 19, 2026
@rsim
Copy link
Copy Markdown
Owner

rsim commented Mar 30, 2026

Create pull request in the new https://github.com/rsim/mondrian-olap-java repo.

@rsim rsim closed this Mar 30, 2026
@rsim rsim deleted the feature/allow_min_max_to_work_with_dates branch March 30, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants