View Javadoc
1   /*
2   Copyright (c) 2016 James Ahlborn
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl.expr;
18  
19  import java.math.BigDecimal;
20  import java.math.BigInteger;
21  import java.text.DecimalFormat;
22  import java.time.LocalDateTime;
23  import java.time.format.DateTimeFormatter;
24  import java.util.HashMap;
25  import java.util.Map;
26  import java.util.UUID;
27  import java.util.concurrent.ThreadLocalRandom;
28  import java.util.regex.Matcher;
29  
30  import com.healthmarketscience.jackcess.expr.EvalContext;
31  import com.healthmarketscience.jackcess.expr.EvalException;
32  import com.healthmarketscience.jackcess.expr.Function;
33  import com.healthmarketscience.jackcess.expr.FunctionLookup;
34  import com.healthmarketscience.jackcess.expr.LocaleContext;
35  import com.healthmarketscience.jackcess.expr.NumericConfig;
36  import com.healthmarketscience.jackcess.expr.TemporalConfig;
37  import com.healthmarketscience.jackcess.expr.Value;
38  import com.healthmarketscience.jackcess.impl.ColumnImpl;
39  import com.healthmarketscience.jackcess.impl.DatabaseImpl;
40  import com.healthmarketscience.jackcess.impl.StringUtil;
41  import static com.healthmarketscience.jackcess.impl.expr.FunctionSupport.*;
42  
43  /**
44   *
45   * @author James Ahlborn
46   */
47  public class DefaultFunctions
48  {
49    private static final Map<String,Function> FUNCS =
50      new HashMap<>();
51  
52    static {
53      // load all default functions
54      DefaultTextFunctions.init();
55      DefaultNumberFunctions.init();
56      DefaultDateFunctions.init();
57      DefaultFinancialFunctions.init();
58    }
59  
60    public static final FunctionLookupexpr/FunctionLookup.html#FunctionLookup">FunctionLookup LOOKUP = new FunctionLookup() {
61      @Override
62      public Function getFunction(String name) {
63        return FUNCS.get(DatabaseImpl.toLookupName(name));
64      }
65    };
66  
67    private DefaultFunctions() {}
68  
69  
70    public static final Function IIF = registerFunc(new Func3("IIf") {
71      @Override
72      protected Value eval3(EvalContext ctx,
73                            Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param2, Value param3) {
74        // null is false
75        return ((!param1.isNull() && param1.getAsBoolean(ctx)) ? param2 : param3);
76      }
77    });
78  
79    public static final Function HEX = registerStringFunc(new Func1NullIsNull("Hex") {
80      @Override
81      protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
82        if(param1.getType().isString() &&
83           (param1.getAsString(ctx).length() == 0)) {
84          return ValueSupport.ZERO_VAL;
85        }
86        int lv = param1.getAsLongInt(ctx);
87        return ValueSupport.toValue(
88            StringUtil.toUpperCase(Integer.toHexString(lv)));
89      }
90    });
91  
92    public static final Function NZ = registerFunc(new FuncVar("Nz", 1, 2) {
93      @Override
94      protected Value/com/healthmarketscience/jackcess/expr/Value.html#Value">Value evalVar(EvalContext ctx, Value[] params) {
95        Value param1 = params[0];
96        if(!param1.isNull()) {
97          return param1;
98        }
99        if(params.length > 1) {
100         return params[1];
101       }
102       Value.Type resultType = ctx.getResultType();
103       return (((resultType == null) || resultType.isString()) ?
104               ValueSupport.EMPTY_STR_VAL : ValueSupport.ZERO_VAL);
105     }
106   });
107 
108   public static final Function CHOOSE = registerFunc(new FuncVar("Choose", 1, Integer.MAX_VALUE) {
109     @Override
110     protected Value/com/healthmarketscience/jackcess/expr/Value.html#Value">Value evalVar(EvalContext ctx, Value[] params) {
111       Value param1 = params[0];
112       int idx = param1.getAsLongInt(ctx);
113       if((idx < 1) || (idx >= params.length)) {
114         return ValueSupport.NULL_VAL;
115       }
116       return params[idx];
117     }
118   });
119 
120   public static final Function SWITCH = registerFunc(new FuncVar("Switch") {
121     @Override
122     protected Value/com/healthmarketscience/jackcess/expr/Value.html#Value">Value evalVar(EvalContext ctx, Value[] params) {
123       if((params.length % 2) != 0) {
124         throw new EvalException("Odd number of parameters");
125       }
126       for(int i = 0; i < params.length; i+=2) {
127         if(params[i].getAsBoolean(ctx)) {
128           return params[i + 1];
129         }
130       }
131       return ValueSupport.NULL_VAL;
132     }
133   });
134 
135   public static final Function OCT = registerStringFunc(new Func1NullIsNull("Oct") {
136     @Override
137     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
138       if(param1.getType().isString() &&
139          (param1.getAsString(ctx).length() == 0)) {
140         return ValueSupport.ZERO_VAL;
141       }
142       int lv = param1.getAsLongInt(ctx);
143       return ValueSupport.toValue(Integer.toOctalString(lv));
144     }
145   });
146 
147   public static final Function CBOOL = registerFunc(new Func1("CBool") {
148     @Override
149     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
150       boolean b = param1.getAsBoolean(ctx);
151       return ValueSupport.toValue(b);
152     }
153   });
154 
155   public static final Function CBYTE = registerFunc(new Func1("CByte") {
156     @Override
157     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
158       int lv = param1.getAsLongInt(ctx);
159       if((lv < 0) || (lv > 255)) {
160         throw new EvalException("Byte code '" + lv + "' out of range ");
161       }
162       return ValueSupport.toValue(lv);
163     }
164   });
165 
166   public static final Function CCUR = registerFunc(new Func1("CCur") {
167     @Override
168     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
169       BigDecimal bd = param1.getAsBigDecimal(ctx);
170       bd = bd.setScale(4, NumberFormatter.ROUND_MODE);
171       return ValueSupport.toValue(bd);
172     }
173   });
174 
175   public static final Function CDATE = registerFunc(new Func1("CDate") {
176     @Override
177     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
178       return param1.getAsDateTimeValue(ctx);
179     }
180   });
181   static {
182     registerFunc("CVDate", CDATE);
183   }
184 
185   public static final Function CDBL = registerFunc(new Func1("CDbl") {
186     @Override
187     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
188       Double dv = param1.getAsDouble(ctx);
189       return ValueSupport.toValue(dv);
190     }
191   });
192 
193   public static final Function CDEC = registerFunc(new Func1("CDec") {
194     @Override
195     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
196       BigDecimal bd = param1.getAsBigDecimal(ctx);
197       return ValueSupport.toValue(bd);
198     }
199   });
200 
201   public static final Function CINT = registerFunc(new Func1("CInt") {
202     @Override
203     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
204       int lv = param1.getAsLongInt(ctx);
205       if((lv < Short.MIN_VALUE) || (lv > Short.MAX_VALUE)) {
206         throw new EvalException("Int value '" + lv + "' out of range ");
207       }
208       return ValueSupport.toValue(lv);
209     }
210   });
211 
212   public static final Function CLNG = registerFunc(new Func1("CLng") {
213     @Override
214     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
215       int lv = param1.getAsLongInt(ctx);
216       return ValueSupport.toValue(lv);
217     }
218   });
219 
220   public static final Function CSNG = registerFunc(new Func1("CSng") {
221     @Override
222     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
223       Double dv = param1.getAsDouble(ctx);
224       if((dv < Float.MIN_VALUE) || (dv > Float.MAX_VALUE)) {
225         throw new EvalException("Single value '" + dv + "' out of range ");
226       }
227       return ValueSupport.toValue(dv.floatValue());
228     }
229   });
230 
231   public static final Function CSTR = registerFunc(new Func1("CStr") {
232     @Override
233     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
234       return ValueSupport.toValue(param1.getAsString(ctx));
235     }
236   });
237 
238   public static final Function CVAR = registerFunc(new Func1("CVar") {
239     @Override
240     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
241       return param1;
242     }
243   });
244 
245   public static final Function ISNULL = registerFunc(new Func1("IsNull") {
246     @Override
247     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
248       return ValueSupport.toValue(param1.isNull());
249     }
250   });
251 
252   public static final Function ISDATE = registerFunc(new Func1("IsDate") {
253     @Override
254     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
255       if(param1.getType().isTemporal()) {
256         return ValueSupport.TRUE_VAL;
257       }
258 
259       // a string is a date/time only if the date/time parse reads it.  A
260       // string which is only a number, such as "200", converts to a
261       // date/time, but it is not one.  "3.9" is the time 03:09.
262       if(param1.getType().isString() &&
263          (DefaultDateFunctions.stringToDateValue(
264              ctx, param1.getAsString(ctx)) != null)) {
265         return ValueSupport.TRUE_VAL;
266       }
267 
268       return ValueSupport.FALSE_VAL;
269     }
270   });
271 
272   public static final Function ISNUMERIC = registerFunc(new Func1("IsNumeric") {
273     @Override
274     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
275       if(param1.getType().isNumeric()) {
276         return ValueSupport.TRUE_VAL;
277       }
278 
279       // note, only a string can be considered numberic for this function,
280       // even though a date/time can be cast to a number in general
281       if(param1.getType().isString() && stringIsNumeric(ctx, param1)) {
282         return ValueSupport.TRUE_VAL;
283       }
284 
285       return ValueSupport.FALSE_VAL;
286     }
287   });
288 
289   public static final Function FORMATNUMBER = registerFunc(new FuncVar("FormatNumber", 1, 6) {
290     @Override
291     protected Value/com/healthmarketscience/jackcess/expr/Value.html#Value">Value evalVar(EvalContext ctx, Value[] params) {
292       return formatNumber(ctx, params, FormatUtil.NumPatternType.GENERAL);
293     }
294   });
295 
296   public static final Function FORMATPERCENT = registerFunc(new FuncVar("FormatPercent", 1, 6) {
297     @Override
298     protected Value/com/healthmarketscience/jackcess/expr/Value.html#Value">Value evalVar(EvalContext ctx, Value[] params) {
299       return formatNumber(ctx, params, FormatUtil.NumPatternType.PERCENT);
300     }
301   });
302 
303   public static final Function FORMATCURRENCY = registerFunc(new FuncVar("FormatCurrency", 1, 6) {
304     @Override
305     protected Value/com/healthmarketscience/jackcess/expr/Value.html#Value">Value evalVar(EvalContext ctx, Value[] params) {
306       return formatNumber(ctx, params, FormatUtil.NumPatternType.CURRENCY);
307     }
308   });
309 
310   public static final Function FORMATDATETIME = registerFunc(new FuncVar("FormatDateTime", 1, 2) {
311     @Override
312     protected Value/com/healthmarketscience/jackcess/expr/Value.html#Value">Value evalVar(EvalContext ctx, Value[] params) {
313       Value param1 = params[0];
314       if(param1.isNull()) {
315         return ValueSupport.NULL_VAL;
316       }
317 
318       LocalDateTime ldt = param1.getAsLocalDateTime(ctx);
319 
320       int fmtType = getOptionalIntParam(ctx, params, 1, 0);
321       TemporalConfig.Type tempType = null;
322       switch(fmtType) {
323       case 0:
324         // vbGeneralDate
325         Value.Type valType = ValueSupport.getDateTimeType(ldt);
326         switch(valType) {
327         case DATE:
328           tempType = TemporalConfig.Type.SHORT_DATE;
329           break;
330         case TIME:
331           tempType = TemporalConfig.Type.LONG_TIME;
332           break;
333         default:
334           tempType = TemporalConfig.Type.GENERAL_DATE;
335         }
336         break;
337       case 1:
338         // vbLongDate
339         tempType = TemporalConfig.Type.LONG_DATE;
340         break;
341       case 2:
342         // vbShortDate
343         tempType = TemporalConfig.Type.SHORT_DATE;
344         break;
345       case 3:
346         // vbLongTime
347         tempType = TemporalConfig.Type.LONG_TIME;
348         break;
349       case 4:
350         // vbShortTime
351         tempType = TemporalConfig.Type.SHORT_TIME;
352         break;
353       default:
354         throw new EvalException("Unknown format " + fmtType);
355       }
356 
357       DateTimeFormatter dtf = ctx.createDateFormatter(
358           ctx.getTemporalConfig().getDateTimeFormat(tempType));
359       return ValueSupport.toValue(dtf.format(ldt));
360     }
361   });
362 
363   public static final Function GENGUID = registerFunc(new Func0("GenGUID") {
364     @Override
365     protected Value eval0(EvalContext ctx) {
366       // the jet engine asks the operating system for the guid, so what it
367       // writes is a version 1 guid on a very old windows and a random
368       // version 4 one on any current windows
369       return ValueSupport.toValue(
370           ColumnImpl.toGUIDString(UUID.randomUUID()));
371     }
372   });
373 
374   public static final Function GENUNIQUEID = registerFunc(new Func0("GenUniqueID") {
375     @Override
376     protected Value eval0(EvalContext ctx) {
377       // a random value over the whole range of a LONG, negative included.
378       // this does not use EvalContext.getRandom, because that gives a float,
379       // which cannot cover the 32 bits evenly
380       return ValueSupport.toValue(ThreadLocalRandom.current().nextInt());
381     }
382   });
383 
384   public static final Function VARTYPE = registerFunc(new Func1("VarType") {
385     @Override
386     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
387       Value.Type type = param1.getType();
388       int vType = 0;
389       switch(type) {
390       case NULL:
391         // vbNull
392         vType = 1;
393         break;
394       case STRING:
395         // vbString
396         vType = 8;
397         break;
398       case DATE:
399       case TIME:
400       case DATE_TIME:
401         // vbDate
402         vType = 7;
403         break;
404       case LONG:
405         // vbLong
406         vType = 3;
407         break;
408       case DOUBLE:
409         // vbDouble
410         vType = 5;
411         break;
412       case BIG_DEC:
413         // vbDecimal
414         vType = 14;
415         break;
416       default:
417         throw new EvalException("Unknown type " + type);
418       }
419       return ValueSupport.toValue(vType);
420     }
421   });
422 
423   public static final Function TYPENAME = registerFunc(new Func1("TypeName") {
424     @Override
425     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
426       Value.Type type = param1.getType();
427       String tName = null;
428       switch(type) {
429       case NULL:
430         tName = "Null";
431         break;
432       case STRING:
433         tName = "String";
434         break;
435       case DATE:
436       case TIME:
437       case DATE_TIME:
438         tName = "Date";
439         break;
440       case LONG:
441         tName = "Long";
442         break;
443       case DOUBLE:
444         tName = "Double";
445         break;
446       case BIG_DEC:
447         tName = "Decimal";
448         break;
449       default:
450         throw new EvalException("Unknown type " + type);
451       }
452       return ValueSupport.toValue(tName);
453     }
454   });
455 
456   public static final Function VAL = registerStringFunc(new Func1NullIsNull("Val") {
457     @Override
458     protected Value../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval1(EvalContext ctx, Value param1) {
459 
460       // strip all whitespace from string
461       String str = ValueSupport.WHITESPACE_PAT.matcher(param1.getAsString(ctx))
462         .replaceAll("");
463 
464       if(str.length() == 0) {
465         return ValueSupport.ZERO_D_VAL;
466       }
467 
468       Matcher m = null;
469 
470       if(str.charAt(0) == ValueSupport.NUMBER_BASE_PREFIX) {
471 
472         // see if we can parse as a radix format
473         BigInteger bi = null;
474         if((m = ValueSupport.HEX_PAT.matcher(str)).find()) {
475           bi = ValueSupport.parseIntegerString(m.group(), 16);
476         } else if((m = ValueSupport.OCTAL_PAT.matcher(str)).find()) {
477           bi = ValueSupport.parseIntegerString(m.group(), 8);
478         }
479 
480         if(bi != null) {
481           // this function works differently than normal string to number
482           // conversion.  it seems to coerce these values to a short/long int
483           // depending on the size of the number (which creates
484           // positive/negative values dependent on the value length)
485           int iVal = ((bi.bitLength() <= 16) ? bi.shortValue() : bi.intValue());
486           return ValueSupport.toValue((double)iVal);
487         }
488 
489       } else {
490 
491         // parse as normal "decimal" number.
492         if((m = ValueSupport.NUMBER_PAT.matcher(str)).find()) {
493           BigDecimal bd = new BigDecimal(m.group());
494           return ValueSupport.toValue(bd.doubleValue());
495         }
496       }
497 
498       return ValueSupport.ZERO_D_VAL;
499     }
500   });
501 
502   private static boolean stringIsNumeric(LocaleContext ctx, Value param) {
503     return (maybeGetAsBigDecimal(ctx, param) != null);
504   }
505 
506   static BigDecimal maybeGetAsBigDecimal(LocaleContext ctx, Value param) {
507     try {
508       return param.getAsBigDecimal(ctx);
509     } catch(EvalException ignored) {
510       // not a number
511     }
512     return null;
513   }
514 
515   static Valuecience/jackcess/expr/Value.html#Value">Value maybeGetAsDateTimeValue(LocaleContext ctx, Value param) {
516     try {
517       // see if we can coerce to date/time
518       return param.getAsDateTimeValue(ctx);
519     } catch(EvalException ignored) {
520       // not a date/time
521     }
522     return null;
523   }
524 
525   private static boolean getOptionalTriStateBoolean(
526       EvalContext ctx, Value[] params, int idx, boolean defValue) {
527     boolean bv = defValue;
528     if(params.length > idx) {
529       int val = params[idx].getAsLongInt(ctx);
530       switch(val) {
531       case 0:
532         // vbFalse
533         bv = false;
534         break;
535       case -1:
536         // vbTrue
537         bv = true;
538         break;
539       case -2:
540         // vbUseDefault
541         bv = defValue;
542         break;
543       default:
544         throw new EvalException("Unsupported tri-state boolean value " + val);
545       }
546     }
547     return bv;
548   }
549 
550   private static Value formatNumber(
551       EvalContext ctx, Value[] params, FormatUtil.NumPatternType numPatType) {
552 
553     Value param1 = params[0];
554     if(param1.isNull()) {
555       return ValueSupport.NULL_VAL;
556     }
557 
558     NumericConfig cfg = ctx.getNumericConfig();
559     int numDecDigits = getOptionalIntParam(
560         ctx, params, 1, cfg.getNumDecimalDigits(), -1);
561     boolean incLeadDigit = getOptionalTriStateBoolean(
562         ctx, params, 2, cfg.includeLeadingDigit());
563     boolean defNegParens = numPatType.useParensForNegatives(cfg);
564     boolean negParens = getOptionalTriStateBoolean(
565         ctx, params, 3, defNegParens);
566     int defNumGroupDigits = cfg.getNumGroupingDigits();
567     boolean groupDigits = getOptionalTriStateBoolean(
568         ctx, params, 4, (defNumGroupDigits > 0));
569     int numGroupDigits = (groupDigits ? defNumGroupDigits : 0);
570 
571     String fmtStr = FormatUtil.createNumberFormatPattern(
572         numPatType, numDecDigits, incLeadDigit, negParens, numGroupDigits);
573 
574     DecimalFormat df = ctx.createDecimalFormat(fmtStr);
575 
576     return ValueSupport.toValue(df.format(param1.getAsBigDecimal(ctx)));
577   }
578 
579   // https://www.techonthenet.com/access/functions/
580   // https://support.office.com/en-us/article/Access-Functions-by-category-b8b136c3-2716-4d39-94a2-658ce330ed83
581 
582   static Function/../../../com/healthmarketscience/jackcess/expr/Function.html#Function">Function registerFunc(Function func) {
583     registerFunc(func.getName(), func);
584     return func;
585   }
586 
587   static Function/../com/healthmarketscience/jackcess/expr/Function.html#Function">Function registerStringFunc(Function func) {
588     registerFunc(func.getName(), func);
589     registerFunc(new StringFuncWrapper(func));
590     return func;
591   }
592 
593   private static void registerFunc(String fname, Function func) {
594     String lookupFname = DatabaseImpl.toLookupName(fname);
595     if(FUNCS.put(lookupFname, func) != null) {
596       throw new IllegalStateException("Duplicate function " + fname);
597     }
598   }
599 }