From: James Ahlborn Date: Fri, 22 Sep 2017 06:48:06 +0000 (+0000) Subject: implement some date/time functions X-Git-Tag: jackcess-2.2.0~24^2~28 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=8eb5fe91d9f7ad8d5c72429f95979cc3c2061b8d;p=jackcess.git implement some date/time functions git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/exprs@1119 f203690c-595d-4dc9-a70b-905162fa7fd2 --- diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java index a87ad34..0cf71c4 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java @@ -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 index 0000000..2d3a777 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java @@ -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); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java index bcefa55..9584424 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java @@ -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; } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java index 15ef42f..2bd3651 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java @@ -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()); } }); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java index 463f147..563c4bf 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java @@ -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,