]> source.dussan.org Git - jackcess.git/commitdiff
implement some date/time functions
authorJames Ahlborn <jtahlborn@yahoo.com>
Fri, 22 Sep 2017 06:48:06 +0000 (06:48 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Fri, 22 Sep 2017 06:48:06 +0000 (06:48 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/exprs@1119 f203690c-595d-4dc9-a70b-905162fa7fd2

src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java
src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java [new file with mode: 0644]
src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java
src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java
src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java

index a87ad344c2491970474b1d426e3e8730480d5d4e..0cf71c4ad4cedd83ab9d8624808bfdd4fd455d05 100644 (file)
@@ -88,7 +88,6 @@ public class BuiltinOperators
     Value.Type mathType = param1.getType();
 
     switch(mathType) {
-    // case STRING: break; unsupported
     case DATE:
     case TIME:
     case DATE_TIME:
@@ -99,6 +98,7 @@ public class BuiltinOperators
       return toValue(-param1.getAsLong());
     case DOUBLE:
       return toValue(-param1.getAsDouble());
+    case STRING:
     case BIG_DEC:
       return toValue(param1.getAsBigDecimal().negate());
     default:
@@ -599,6 +599,10 @@ public class BuiltinOperators
     return new LongValue((long)i);
   }
 
+  public static Value toValue(long s) {
+    return new LongValue(s);
+  }
+
   public static Value toValue(Long s) {
     return new LongValue(s);
   }
@@ -607,6 +611,10 @@ public class BuiltinOperators
     return new DoubleValue((double)f);
   }
 
+  public static Value toValue(double s) {
+    return new DoubleValue(s);
+  }
+
   public static Value toValue(Double s) {
     return new DoubleValue(s);
   }
@@ -619,8 +627,21 @@ public class BuiltinOperators
     return new BigDecimalValue(s);
   }
 
-  private static Value toDateValue(EvalContext ctx, Value.Type type, double v, 
-                                   Value param1, Value param2)
+  public static Value toValue(Value.Type type, Date d, DateFormat fmt) {
+    switch(type) {
+    case DATE:
+      return new DateValue(d, fmt);
+    case TIME:
+      return new TimeValue(d, fmt);
+    case DATE_TIME:
+      return new DateTimeValue(d, fmt);
+    default:
+      throw new RuntimeException("Unexpected date/time type " + type);
+    }
+  }
+  
+  static Value toDateValue(EvalContext ctx, Value.Type type, double v, 
+                           Value param1, Value param2)
   {
     DateFormat fmt = null;
     if((param1 instanceof BaseDateValue) && (param1.getType() == type)) {
@@ -628,6 +649,15 @@ public class BuiltinOperators
     } else if((param2 instanceof BaseDateValue) && (param2.getType() == type)) {
       fmt = ((BaseDateValue)param2).getFormat();
     } else {
+      fmt = getDateFormatForType(ctx, type);
+    }
+
+    Date d = new Date(ColumnImpl.fromDateDouble(v, fmt.getCalendar()));
+
+    return toValue(type, d, fmt);
+  }
+
+  static DateFormat getDateFormatForType(EvalContext ctx, Value.Type type) {
       String fmtStr = null;
       switch(type) {
       case DATE:
@@ -640,23 +670,9 @@ public class BuiltinOperators
         fmtStr = ctx.getTemporalConfig().getDefaultDateTimeFormat();
         break;
       default:
-        throw new RuntimeException("Unexpected type " + type);
+        throw new RuntimeException("Unexpected date/time type " + type);
       }
-      fmt = ctx.createDateFormat(fmtStr);
-    }
-
-    Date d = new Date(ColumnImpl.fromDateDouble(v, fmt.getCalendar()));
-
-    switch(type) {
-    case DATE:
-      return new DateValue(d, fmt);
-    case TIME:
-      return new TimeValue(d, fmt);
-    case DATE_TIME:
-      return new DateTimeValue(d, fmt);
-    default:
-      throw new RuntimeException("Unexpected type " + type);
-    }
+      return ctx.createDateFormat(fmtStr);
   }
 
   private static Value.Type getMathTypePrecedence(
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java
new file mode 100644 (file)
index 0000000..2d3a777
--- /dev/null
@@ -0,0 +1,218 @@
+/*
+Copyright (c) 2017 James Ahlborn
+
+Licensed 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 com.healthmarketscience.jackcess.impl.expr;
+
+
+import java.math.BigDecimal;
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+import com.healthmarketscience.jackcess.expr.EvalContext;
+import com.healthmarketscience.jackcess.expr.Function;
+import com.healthmarketscience.jackcess.expr.Value;
+import com.healthmarketscience.jackcess.impl.ColumnImpl;
+import static com.healthmarketscience.jackcess.impl.expr.DefaultFunctions.*;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class DefaultDateFunctions 
+{
+  // min, valid, recognizable date: January 1, 100 A.D. 00:00:00
+  private static final double MIN_DATE = -657434.0d;
+  // max, valid, recognizable date: December 31, 9999 A.D. 23:59:59
+  private static final double MAX_DATE = 2958465.999988426d;
+  
+  private DefaultDateFunctions() {}
+
+  static void init() {
+    // dummy method to ensure this class is loaded
+  }
+
+  public static final Function DATE = registerFunc(new Func0("Date") {
+    @Override
+    public boolean isPure() {
+      return false;
+    }
+    @Override
+    protected Value eval0(EvalContext ctx) {
+      DateFormat df = BuiltinOperators.getDateFormatForType(ctx, Value.Type.DATE);
+      double dd = ColumnImpl.toDateDouble(System.currentTimeMillis(), df.getCalendar());
+      // the integral part of the date/time double is the date value.  discard
+      // the fractional portion
+      dd = ((long)dd);
+      return BuiltinOperators.toValue(Value.Type.DATE, new Date(), df);
+    }
+  });
+
+  public static final Function NOW = registerFunc(new Func0("Now") {
+    @Override
+    public boolean isPure() {
+      return false;
+    }
+    @Override
+    protected Value eval0(EvalContext ctx) {
+      DateFormat df = BuiltinOperators.getDateFormatForType(ctx, Value.Type.DATE_TIME);
+      return BuiltinOperators.toValue(Value.Type.DATE_TIME, new Date(), df);
+    }
+  });
+
+  public static final Function TIME = registerFunc(new Func0("Time") {
+    @Override
+    public boolean isPure() {
+      return false;
+    }
+    @Override
+    protected Value eval0(EvalContext ctx) {
+      DateFormat df = BuiltinOperators.getDateFormatForType(ctx, Value.Type.TIME);
+      double dd = ColumnImpl.toDateDouble(System.currentTimeMillis(), df.getCalendar());
+      // the fractional part of the date/time double is the time value.  discard
+      // the integral portion
+      dd = Math.IEEEremainder(dd, 1.0d);
+      return BuiltinOperators.toValue(Value.Type.TIME, new Date(), df);
+    }
+  });
+
+  public static final Function HOUR = registerFunc(new Func1NullIsNull("Hour") {
+    @Override
+    protected Value eval1(EvalContext ctx, Value param1) {
+      return BuiltinOperators.toValue(
+          nonNullToCalendarField(ctx, param1, Calendar.HOUR_OF_DAY));
+    }
+  });
+
+  public static final Function MINUTE = registerFunc(new Func1NullIsNull("Minute") {
+    @Override
+    protected Value eval1(EvalContext ctx, Value param1) {
+      return BuiltinOperators.toValue(
+          nonNullToCalendarField(ctx, param1, Calendar.MINUTE));
+    }
+  });
+
+  public static final Function SECOND = registerFunc(new Func1NullIsNull("Second") {
+    @Override
+    protected Value eval1(EvalContext ctx, Value param1) {
+      return BuiltinOperators.toValue(
+          nonNullToCalendarField(ctx, param1, Calendar.SECOND));
+    }
+  });
+
+  public static final Function YEAR = registerFunc(new Func1NullIsNull("Year") {
+    @Override
+    protected Value eval1(EvalContext ctx, Value param1) {
+      // convert from 0 based to 1 based value
+      return BuiltinOperators.toValue(
+          nonNullToCalendarField(ctx, param1, Calendar.YEAR) + 1);
+    }
+  });
+
+  public static final Function MONTH = registerFunc(new Func1NullIsNull("Month") {
+    @Override
+    protected Value eval1(EvalContext ctx, Value param1) {
+      // convert from 0 based to 1 based value
+      return BuiltinOperators.toValue(
+          nonNullToCalendarField(ctx, param1, Calendar.MONTH) + 1);
+    }
+  });
+
+  public static final Function DAY = registerFunc(new Func1NullIsNull("Day") {
+    @Override
+    protected Value eval1(EvalContext ctx, Value param1) {
+      return BuiltinOperators.toValue(
+          nonNullToCalendarField(ctx, param1, Calendar.DAY_OF_MONTH));
+    }
+  });
+
+  public static final Function WEEKDAY = registerFunc(new FuncVar("Weekday", 1, 2) {
+    @Override
+    protected Value evalVar(EvalContext ctx, Value[] params) {
+      Value param1 = params[0];
+      if(param1 == null) {
+        return null;
+      }
+      int day = nonNullToCalendarField(ctx, param1, Calendar.DAY_OF_WEEK);
+      // FIXME handle first day of week
+      // if(params.length > 1) {
+      //   int firstDay = params[1].getAsLong();
+      // }
+      return BuiltinOperators.toValue(day);
+    }
+  });
+
+  
+  private static int nonNullToCalendarField(EvalContext ctx, Value param,
+                                            int field) {
+    return nonNullToCalendar(ctx, param).get(field);
+  }
+  
+  private static Calendar nonNullToCalendar(EvalContext ctx, Value param) {
+    param = nonNullToDateValue(ctx, param);
+    if(param == null) {
+      // not a date/time
+      throw new IllegalStateException("Invalid date/time expression '" + param + "'");
+    }
+
+    Calendar cal = 
+      ((param instanceof BaseDateValue) ?
+       ((BaseDateValue)param).getFormat().getCalendar() :
+       BuiltinOperators.getDateFormatForType(ctx, param.getType()).getCalendar());
+
+    cal.setTime(param.getAsDateTime(ctx));
+    return cal;
+  }
+  
+  static Value nonNullToDateValue(EvalContext ctx, Value param) {
+    Value.Type type = param.getType();
+    if(type.isTemporal()) {
+      return param;
+    }
+
+    if(type == Value.Type.STRING) {
+      // see if we can coerce to date/time
+
+      // FIXME use ExpressionatorTokenizer to detect explicit date/time format
+
+      try {
+        return numberToDateValue(ctx, param.getAsDouble());
+      } catch(NumberFormatException ignored) {
+        // not a number
+        return null;
+      }
+    }
+
+    // must be a number
+    return numberToDateValue(ctx, param.getAsDouble());
+  }
+
+  private static Value numberToDateValue(EvalContext ctx, double dd) {
+    if((dd < MIN_DATE) || (dd > MAX_DATE)) {
+      // outside valid date range
+      return null;
+    }
+
+    boolean hasDate = (((long)dd) != 0L);
+    boolean hasTime = (Math.IEEEremainder(dd, 1.0d) != 0.0d);
+
+    Value.Type type = (hasDate ? (hasTime ? Value.Type.DATE_TIME : Value.Type.DATE) :
+                       Value.Type.TIME);
+    DateFormat df = BuiltinOperators.getDateFormatForType(ctx, type);
+    Date d = new Date(ColumnImpl.fromDateDouble(dd, df.getCalendar()));
+    return BuiltinOperators.toValue(type, d, df);
+  }
+}
index bcefa5524e192e7b1f2a0a6477b5987b3b87e045..958442478562d4507e6811ed2aa62b57e069e316 100644 (file)
@@ -18,12 +18,14 @@ package com.healthmarketscience.jackcess.impl.expr;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 
 import com.healthmarketscience.jackcess.expr.EvalContext;
 import com.healthmarketscience.jackcess.expr.Function;
 import com.healthmarketscience.jackcess.expr.Value;
+import com.healthmarketscience.jackcess.impl.DatabaseImpl;
 
 /**
  *
@@ -41,12 +43,13 @@ public class DefaultFunctions
     // load all default functions
     DefaultTextFunctions.init();
     DefaultNumberFunctions.init();
+    DefaultDateFunctions.init();
   }
   
   private DefaultFunctions() {}
 
   public static Function getFunction(String name) {
-    return FUNCS.get(name.toLowerCase());
+    return FUNCS.get(DatabaseImpl.toLookupName(name));
   }
 
   public static abstract class BaseFunction implements Function
@@ -82,12 +85,19 @@ public class DefaultFunctions
         String range = ((_minParams == _maxParams) ? "" + _minParams :
                         _minParams + " to " + _maxParams);
         throw new IllegalArgumentException(
-            this + ": invalid number of parameters " +
+            "Invalid number of parameters " +
             num + " passed, expected " + range);
       }
     }
 
-    // FIXME, provide context for exceptions thrown
+    protected IllegalStateException invalidFunctionCall(
+        Throwable t, Value[] params)
+    {
+      String paramStr = Arrays.toString(params);
+      String msg = "Invalid function call {" + _name + "(" +
+        paramStr.substring(1, paramStr.length() - 1) + ")}";
+      return new IllegalStateException(msg, t);
+    }
     
     @Override
     public String toString() {
@@ -102,8 +112,12 @@ public class DefaultFunctions
     }
 
     public final Value eval(EvalContext ctx, Value... params) {
-      validateNumParams(params);
-      return eval0(ctx);
+      try {
+        validateNumParams(params);
+        return eval0(ctx);
+      } catch(Exception e) {
+        throw invalidFunctionCall(e, params);
+      }
     }
 
     protected abstract Value eval0(EvalContext ctx);
@@ -116,8 +130,12 @@ public class DefaultFunctions
     }
 
     public final Value eval(EvalContext ctx, Value... params) {
-      validateNumParams(params);
-      return eval1(ctx, params[0]);
+      try {
+        validateNumParams(params);
+        return eval1(ctx, params[0]);
+      } catch(Exception e) {
+        throw invalidFunctionCall(e, params);
+      }
     }
 
     protected abstract Value eval1(EvalContext ctx, Value param);
@@ -130,12 +148,16 @@ public class DefaultFunctions
     }
 
     public final Value eval(EvalContext ctx, Value... params) {
-      validateNumParams(params);
-      Value param1 = params[0];
-      if(param1.isNull()) {
-        return param1;
+      try {
+        validateNumParams(params);
+        Value param1 = params[0];
+        if(param1.isNull()) {
+          return param1;
+        }
+        return eval1(ctx, param1);
+      } catch(Exception e) {
+        throw invalidFunctionCall(e, params);
       }
-      return eval1(ctx, param1);
     }
 
     protected abstract Value eval1(EvalContext ctx, Value param);
@@ -148,8 +170,12 @@ public class DefaultFunctions
     }
 
     public final Value eval(EvalContext ctx, Value... params) {
-      validateNumParams(params);
-      return eval2(ctx, params[0], params[1]);
+      try {
+        validateNumParams(params);
+        return eval2(ctx, params[0], params[1]);
+      } catch(Exception e) {
+        throw invalidFunctionCall(e, params);
+      }
     }
 
     protected abstract Value eval2(EvalContext ctx, Value param1, Value param2);
@@ -162,8 +188,12 @@ public class DefaultFunctions
     }
 
     public final Value eval(EvalContext ctx, Value... params) {
-      validateNumParams(params);
-      return eval3(ctx, params[0], params[1], params[2]);
+      try {
+        validateNumParams(params);
+        return eval3(ctx, params[0], params[1], params[2]);
+      } catch(Exception e) {
+        throw invalidFunctionCall(e, params);
+      }
     }
 
     protected abstract Value eval3(EvalContext ctx, 
@@ -177,13 +207,18 @@ public class DefaultFunctions
     }
 
     public final Value eval(EvalContext ctx, Value... params) {
-      validateNumParams(params);
-      return evalVar(ctx, params);
+      try {
+        validateNumParams(params);
+        return evalVar(ctx, params);
+      } catch(Exception e) {
+        throw invalidFunctionCall(e, params);
+      }
     }
 
     protected abstract Value evalVar(EvalContext ctx, Value[] params);
   }
 
+  
   public static final Function IIF = registerFunc(new Func3("IIf") {
     @Override
     protected Value eval3(EvalContext ctx, 
@@ -262,15 +297,15 @@ public class DefaultFunctions
     }
   });
 
-  // public static final Function CDATE = registerFunc(new Func1("CDate") {
-  //   @Override
-  //   protected Value eval1(EvalContext ctx, Value param1) {
-  //   FIXME
-  //     BigDecimal bd = param1.getAsBigDecimal();
-  //     bd.setScale(4, DEFAULT_ROUND_MODE);
-  //     return BuiltinOperators.toValue(bd);
-  //   }
-  // });
+  public static final Function CDATE = registerFunc(new Func1("CDate") {
+    @Override
+    protected Value eval1(EvalContext ctx, Value param1) {
+      return DefaultDateFunctions.nonNullToDateValue(ctx, param1);
+    }
+  });
+  static {
+    registerFunc("CVDate", CDATE);
+  }
 
   public static final Function CDBL = registerFunc(new Func1("CDbl") {
     @Override
@@ -328,7 +363,28 @@ public class DefaultFunctions
     }
   });
 
-  // FIXME, CVAR
+  public static final Function CVAR = registerFunc(new Func1("CVar") {
+    @Override
+    protected Value eval1(EvalContext ctx, Value param1) {
+      return param1;
+    }
+  });
+
+  public static final Function ISNULL = registerFunc(new Func1("IsNull") {
+    @Override
+    protected Value eval1(EvalContext ctx, Value param1) {
+      return BuiltinOperators.toValue(param1.isNull());
+    }
+  });
+
+  public static final Function ISDATE = registerFunc(new Func1("IsDate") {
+    @Override
+    protected Value eval1(EvalContext ctx, Value param1) {
+      return BuiltinOperators.toValue(
+          !param1.isNull() &&
+          (DefaultDateFunctions.nonNullToDateValue(ctx, param1) != null));
+    }
+  });
 
 
   private static long roundToLong(Value param) {
@@ -343,25 +399,22 @@ public class DefaultFunctions
   // https://support.office.com/en-us/article/Access-Functions-by-category-b8b136c3-2716-4d39-94a2-658ce330ed83
 
   static Function registerFunc(Function func) {
-    return registerFunc(false, func);
+    registerFunc(func.getName(), func);
+    return func;
   }
 
   static Function registerStringFunc(Function func) {
-    return registerFunc(true, func);
+    // for our purposes the non-variant versions are the same function
+    // (e.g. "Foo" and "Foo$")
+    registerFunc(func.getName(), func);
+    registerFunc(func.getName() + NON_VAR_SUFFIX, func);
+    return func;
   }
 
-  private static Function registerFunc(boolean includeNonVar, Function func) {
-    String fname = func.getName().toLowerCase();
-    if(FUNCS.put(fname, func) != null) {
-      throw new IllegalStateException("Duplicate function " + func);
-    }
-    if(includeNonVar) {
-      // for our purposes the non-variant versions are the same function
-      fname += NON_VAR_SUFFIX;
-      if(FUNCS.put(fname, func) != null) {
-        throw new IllegalStateException("Duplicate function " + func);
-      }
+  private static void registerFunc(String fname, Function func) {
+    String lookupFname = DatabaseImpl.toLookupName(fname);
+    if(FUNCS.put(lookupFname, func) != null) {
+      throw new IllegalStateException("Duplicate function " + fname);
     }
-    return func;
   }
 }
index 15ef42f5dd18da8d40f10c79f0f7facb2e759b8f..2bd3651135f382d758035dd45d8346a7180a659e 100644 (file)
@@ -39,18 +39,25 @@ public class DefaultNumberFunctions
   public static final Function ABS = registerFunc(new Func1NullIsNull("Abs") {
     @Override
     protected Value eval1(EvalContext ctx, Value param1) {
-      Value.Type type = param1.getType();
-      if(!type.isNumeric()) {
-        // FIXME how to handle text/date?
-        // FIXME, cast to number, date as date?
-      }
-      if(type.isIntegral()) {
+      Value.Type mathType = param1.getType();
+
+      switch(mathType) {
+      case DATE:
+      case TIME:
+      case DATE_TIME:
+        // dates/times get converted to date doubles for arithmetic
+        double result = Math.abs(param1.getAsDouble());
+        return BuiltinOperators.toDateValue(ctx, mathType, result, param1, null);
+      case LONG:
         return BuiltinOperators.toValue(Math.abs(param1.getAsLong()));
-      }
-      if(type.getPreferredFPType() == Value.Type.DOUBLE) {
+      case DOUBLE:
         return BuiltinOperators.toValue(Math.abs(param1.getAsDouble()));
+      case STRING:
+      case BIG_DEC:
+        return BuiltinOperators.toValue(param1.getAsBigDecimal().abs());
+      default:
+        throw new RuntimeException("Unexpected type " + mathType);
       }
-      return BuiltinOperators.toValue(param1.getAsBigDecimal().abs());
     }
   });
 
index 463f1474f7a16c3b7f36ccad61d2f1e4eeafcf14..563c4bf7a5a5d1ac3aeb515ef1a9a59f1c8e8d96 100644 (file)
@@ -315,7 +315,7 @@ class ExpressionTokenizer
     
     if(hasTime) {
       int strLen = dateStr.length();
-      hasTime = ((strLen >= AMPM_SUFFIX_LEN) &&
+      hasAmPm = ((strLen >= AMPM_SUFFIX_LEN) &&
                  (dateStr.regionMatches(true, strLen - AMPM_SUFFIX_LEN, 
                                         AM_SUFFIX, 0, AMPM_SUFFIX_LEN) ||
                   dateStr.regionMatches(true, strLen - AMPM_SUFFIX_LEN,