/* Copyright (c) 2016 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.math.BigInteger; import java.text.DecimalFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import com.healthmarketscience.jackcess.expr.EvalContext; import com.healthmarketscience.jackcess.expr.EvalException; import com.healthmarketscience.jackcess.expr.Function; import com.healthmarketscience.jackcess.expr.FunctionLookup; import com.healthmarketscience.jackcess.expr.LocaleContext; import com.healthmarketscience.jackcess.expr.NumericConfig; import com.healthmarketscience.jackcess.expr.TemporalConfig; import com.healthmarketscience.jackcess.expr.Value; import com.healthmarketscience.jackcess.impl.DatabaseImpl; import static com.healthmarketscience.jackcess.impl.expr.FunctionSupport.*; /** * * @author James Ahlborn */ public class DefaultFunctions { private static final Map FUNCS = new HashMap(); static { // load all default functions DefaultTextFunctions.init(); DefaultNumberFunctions.init(); DefaultDateFunctions.init(); DefaultFinancialFunctions.init(); } public static final FunctionLookup LOOKUP = new FunctionLookup() { @Override public Function getFunction(String name) { return FUNCS.get(DatabaseImpl.toLookupName(name)); } }; private DefaultFunctions() {} public static final Function IIF = registerFunc(new Func3("IIf") { @Override protected Value eval3(EvalContext ctx, Value param1, Value param2, Value param3) { // null is false return ((!param1.isNull() && param1.getAsBoolean(ctx)) ? param2 : param3); } }); public static final Function HEX = registerStringFunc(new Func1NullIsNull("Hex") { @Override protected Value eval1(EvalContext ctx, Value param1) { if(param1.getType().isString() && (param1.getAsString(ctx).length() == 0)) { return ValueSupport.ZERO_VAL; } int lv = param1.getAsLongInt(ctx); return ValueSupport.toValue(Integer.toHexString(lv).toUpperCase()); } }); public static final Function NZ = registerFunc(new FuncVar("Nz", 1, 2) { @Override protected Value evalVar(EvalContext ctx, Value[] params) { Value param1 = params[0]; if(!param1.isNull()) { return param1; } if(params.length > 1) { return params[1]; } Value.Type resultType = ctx.getResultType(); return (((resultType == null) || resultType.isString()) ? ValueSupport.EMPTY_STR_VAL : ValueSupport.ZERO_VAL); } }); public static final Function CHOOSE = registerFunc(new FuncVar("Choose", 1, Integer.MAX_VALUE) { @Override protected Value evalVar(EvalContext ctx, Value[] params) { Value param1 = params[0]; int idx = param1.getAsLongInt(ctx); if((idx < 1) || (idx >= params.length)) { return ValueSupport.NULL_VAL; } return params[idx]; } }); public static final Function SWITCH = registerFunc(new FuncVar("Switch") { @Override protected Value evalVar(EvalContext ctx, Value[] params) { if((params.length % 2) != 0) { throw new EvalException("Odd number of parameters"); } for(int i = 0; i < params.length; i+=2) { if(params[i].getAsBoolean(ctx)) { return params[i + 1]; } } return ValueSupport.NULL_VAL; } }); public static final Function OCT = registerStringFunc(new Func1NullIsNull("Oct") { @Override protected Value eval1(EvalContext ctx, Value param1) { if(param1.getType().isString() && (param1.getAsString(ctx).length() == 0)) { return ValueSupport.ZERO_VAL; } int lv = param1.getAsLongInt(ctx); return ValueSupport.toValue(Integer.toOctalString(lv)); } }); public static final Function CBOOL = registerFunc(new Func1("CBool") { @Override protected Value eval1(EvalContext ctx, Value param1) { boolean b = param1.getAsBoolean(ctx); return ValueSupport.toValue(b); } }); public static final Function CBYTE = registerFunc(new Func1("CByte") { @Override protected Value eval1(EvalContext ctx, Value param1) { int lv = param1.getAsLongInt(ctx); if((lv < 0) || (lv > 255)) { throw new EvalException("Byte code '" + lv + "' out of range "); } return ValueSupport.toValue(lv); } }); public static final Function CCUR = registerFunc(new Func1("CCur") { @Override protected Value eval1(EvalContext ctx, Value param1) { BigDecimal bd = param1.getAsBigDecimal(ctx); bd = bd.setScale(4, NumberFormatter.ROUND_MODE); return ValueSupport.toValue(bd); } }); public static final Function CDATE = registerFunc(new Func1("CDate") { @Override protected Value eval1(EvalContext ctx, Value param1) { return param1.getAsDateTimeValue(ctx); } }); static { registerFunc("CVDate", CDATE); } public static final Function CDBL = registerFunc(new Func1("CDbl") { @Override protected Value eval1(EvalContext ctx, Value param1) { Double dv = param1.getAsDouble(ctx); return ValueSupport.toValue(dv); } }); public static final Function CDEC = registerFunc(new Func1("CDec") { @Override protected Value eval1(EvalContext ctx, Value param1) { BigDecimal bd = param1.getAsBigDecimal(ctx); return ValueSupport.toValue(bd); } }); public static final Function CINT = registerFunc(new Func1("CInt") { @Override protected Value eval1(EvalContext ctx, Value param1) { int lv = param1.getAsLongInt(ctx); if((lv < Short.MIN_VALUE) || (lv > Short.MAX_VALUE)) { throw new EvalException("Int value '" + lv + "' out of range "); } return ValueSupport.toValue(lv); } }); public static final Function CLNG = registerFunc(new Func1("CLng") { @Override protected Value eval1(EvalContext ctx, Value param1) { int lv = param1.getAsLongInt(ctx); return ValueSupport.toValue(lv); } }); public static final Function CSNG = registerFunc(new Func1("CSng") { @Override protected Value eval1(EvalContext ctx, Value param1) { Double dv = param1.getAsDouble(ctx); if((dv < Float.MIN_VALUE) || (dv > Float.MAX_VALUE)) { throw new EvalException("Single value '" + dv + "' out of range "); } return ValueSupport.toValue(dv.floatValue()); } }); public static final Function CSTR = registerFunc(new Func1("CStr") { @Override protected Value eval1(EvalContext ctx, Value param1) { return ValueSupport.toValue(param1.getAsString(ctx)); } }); 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 ValueSupport.toValue(param1.isNull()); } }); public static final Function ISDATE = registerFunc(new Func1("IsDate") { @Override protected Value eval1(EvalContext ctx, Value param1) { if(param1.getType().isTemporal()) { return ValueSupport.TRUE_VAL; } // for the purposes of this method, a string literal should only // return true if it is explicitly a date/time, not if it is just a // number (even though casting a number string to a date/time works in // general) if(param1.getType().isString() && !stringIsNumeric(ctx, param1) && stringIsTemporal(ctx, param1)) { return ValueSupport.TRUE_VAL; } return ValueSupport.FALSE_VAL; } }); public static final Function ISNUMERIC = registerFunc(new Func1("IsNumeric") { @Override protected Value eval1(EvalContext ctx, Value param1) { if(param1.getType().isNumeric()) { return ValueSupport.TRUE_VAL; } // note, only a string can be considered numberic for this function, // even though a date/time can be cast to a number in general if(param1.getType().isString() && stringIsNumeric(ctx, param1)) { return ValueSupport.TRUE_VAL; } return ValueSupport.FALSE_VAL; } }); public static final Function FORMATNUMBER = registerFunc(new FuncVar("FormatNumber", 1, 6) { @Override protected Value evalVar(EvalContext ctx, Value[] params) { return formatNumber(ctx, params, FormatUtil.NumPatternType.GENERAL); } }); public static final Function FORMATPERCENT = registerFunc(new FuncVar("FormatPercent", 1, 6) { @Override protected Value evalVar(EvalContext ctx, Value[] params) { return formatNumber(ctx, params, FormatUtil.NumPatternType.PERCENT); } }); public static final Function FORMATCURRENCY = registerFunc(new FuncVar("FormatCurrency", 1, 6) { @Override protected Value evalVar(EvalContext ctx, Value[] params) { return formatNumber(ctx, params, FormatUtil.NumPatternType.CURRENCY); } }); public static final Function FORMATDATETIME = registerFunc(new FuncVar("FormatDateTime", 1, 2) { @Override protected Value evalVar(EvalContext ctx, Value[] params) { Value param1 = params[0]; if(param1.isNull()) { return ValueSupport.NULL_VAL; } LocalDateTime ldt = param1.getAsLocalDateTime(ctx); int fmtType = getOptionalIntParam(ctx, params, 1, 0); TemporalConfig.Type tempType = null; switch(fmtType) { case 0: // vbGeneralDate Value.Type valType = ValueSupport.getDateTimeType(ldt); switch(valType) { case DATE: tempType = TemporalConfig.Type.SHORT_DATE; break; case TIME: tempType = TemporalConfig.Type.LONG_TIME; break; default: tempType = TemporalConfig.Type.GENERAL_DATE; } break; case 1: // vbLongDate tempType = TemporalConfig.Type.LONG_DATE; break; case 2: // vbShortDate tempType = TemporalConfig.Type.SHORT_DATE; break; case 3: // vbLongTime tempType = TemporalConfig.Type.LONG_TIME; break; case 4: // vbShortTime tempType = TemporalConfig.Type.SHORT_TIME; break; default: throw new EvalException("Unknown format " + fmtType); } DateTimeFormatter dtf = ctx.createDateFormatter( ctx.getTemporalConfig().getDateTimeFormat(tempType)); return ValueSupport.toValue(dtf.format(ldt)); } }); public static final Function VARTYPE = registerFunc(new Func1("VarType") { @Override protected Value eval1(EvalContext ctx, Value param1) { Value.Type type = param1.getType(); int vType = 0; switch(type) { case NULL: // vbNull vType = 1; break; case STRING: // vbString vType = 8; break; case DATE: case TIME: case DATE_TIME: // vbDate vType = 7; break; case LONG: // vbLong vType = 3; break; case DOUBLE: // vbDouble vType = 5; break; case BIG_DEC: // vbDecimal vType = 14; break; default: throw new EvalException("Unknown type " + type); } return ValueSupport.toValue(vType); } }); public static final Function TYPENAME = registerFunc(new Func1("TypeName") { @Override protected Value eval1(EvalContext ctx, Value param1) { Value.Type type = param1.getType(); String tName = null; switch(type) { case NULL: tName = "Null"; break; case STRING: tName = "String"; break; case DATE: case TIME: case DATE_TIME: tName = "Date"; break; case LONG: tName = "Long"; break; case DOUBLE: tName = "Double"; break; case BIG_DEC: tName = "Decimal"; break; default: throw new EvalException("Unknown type " + type); } return ValueSupport.toValue(tName); } }); public static final Function VAL = registerStringFunc(new Func1NullIsNull("Val") { @Override protected Value eval1(EvalContext ctx, Value param1) { // strip all whitespace from string String str = ValueSupport.WHITESPACE_PAT.matcher(param1.getAsString(ctx)) .replaceAll(""); if(str.length() == 0) { return ValueSupport.ZERO_D_VAL; } Matcher m = null; if(str.charAt(0) == ValueSupport.NUMBER_BASE_PREFIX) { // see if we can parse as a radix format BigInteger bi = null; if((m = ValueSupport.HEX_PAT.matcher(str)).find()) { bi = ValueSupport.parseIntegerString(m.group(), 16); } else if((m = ValueSupport.OCTAL_PAT.matcher(str)).find()) { bi = ValueSupport.parseIntegerString(m.group(), 8); } if(bi != null) { // this function works differently than normal string to number // conversion. it seems to coerce these values to a short/long int // depending on the size of the number (which creates // positive/negative values dependent on the value length) int iVal = ((bi.bitLength() <= 16) ? bi.shortValue() : bi.intValue()); return ValueSupport.toValue((double)iVal); } } else { // parse as normal "decimal" number. if((m = ValueSupport.NUMBER_PAT.matcher(str)).find()) { BigDecimal bd = new BigDecimal(m.group()); return ValueSupport.toValue(bd.doubleValue()); } } return ValueSupport.ZERO_D_VAL; } }); private static boolean stringIsNumeric(LocaleContext ctx, Value param) { return (maybeGetAsBigDecimal(ctx, param) != null); } static BigDecimal maybeGetAsBigDecimal(LocaleContext ctx, Value param) { try { return param.getAsBigDecimal(ctx); } catch(EvalException ignored) { // not a number } return null; } private static boolean stringIsTemporal(EvalContext ctx, Value param) { return (maybeGetAsDateTimeValue(ctx, param) != null); } static Value maybeGetAsDateTimeValue(LocaleContext ctx, Value param) { try { // see if we can coerce to date/time return param.getAsDateTimeValue(ctx); } catch(EvalException ignored) { // not a date/time } return null; } private static boolean getOptionalTriStateBoolean( EvalContext ctx, Value[] params, int idx, boolean defValue) { boolean bv = defValue; if(params.length > idx) { int val = params[idx].getAsLongInt(ctx); switch(val) { case 0: // vbFalse bv = false; break; case -1: // vbTrue bv = true; break; case -2: // vbUseDefault bv = defValue; break; default: throw new EvalException("Unsupported tri-state boolean value " + val); } } return bv; } private static Value formatNumber( EvalContext ctx, Value[] params, FormatUtil.NumPatternType numPatType) { Value param1 = params[0]; if(param1.isNull()) { return ValueSupport.NULL_VAL; } NumericConfig cfg = ctx.getNumericConfig(); int numDecDigits = getOptionalIntParam( ctx, params, 1, cfg.getNumDecimalDigits(), -1); boolean incLeadDigit = getOptionalTriStateBoolean( ctx, params, 2, cfg.includeLeadingDigit()); boolean defNegParens = numPatType.useParensForNegatives(cfg); boolean negParens = getOptionalTriStateBoolean( ctx, params, 3, defNegParens); int defNumGroupDigits = cfg.getNumGroupingDigits(); boolean groupDigits = getOptionalTriStateBoolean( ctx, params, 4, (defNumGroupDigits > 0)); int numGroupDigits = (groupDigits ? defNumGroupDigits : 0); String fmtStr = FormatUtil.createNumberFormatPattern( numPatType, numDecDigits, incLeadDigit, negParens, numGroupDigits); DecimalFormat df = ctx.createDecimalFormat(fmtStr); return ValueSupport.toValue(df.format(param1.getAsBigDecimal(ctx))); } // https://www.techonthenet.com/access/functions/ // https://support.office.com/en-us/article/Access-Functions-by-category-b8b136c3-2716-4d39-94a2-658ce330ed83 static Function registerFunc(Function func) { registerFunc(func.getName(), func); return func; } static Function registerStringFunc(Function func) { registerFunc(func.getName(), func); registerFunc(new StringFuncWrapper(func)); return 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); } } }