View Javadoc
1   /*
2   Copyright (c) 2018 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.text.DecimalFormat;
21  import java.text.NumberFormat;
22  import java.time.LocalDateTime;
23  import java.time.format.DateTimeFormatter;
24  import java.time.format.DateTimeFormatterBuilder;
25  import java.time.temporal.ChronoField;
26  import java.time.temporal.TemporalField;
27  import java.time.temporal.WeekFields;
28  import java.util.AbstractMap;
29  import java.util.AbstractSet;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.HashMap;
33  import java.util.Iterator;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Set;
37  import java.util.function.BiConsumer;
38  
39  import com.healthmarketscience.jackcess.expr.EvalContext;
40  import com.healthmarketscience.jackcess.expr.EvalException;
41  import com.healthmarketscience.jackcess.expr.NumericConfig;
42  import com.healthmarketscience.jackcess.expr.TemporalConfig;
43  import com.healthmarketscience.jackcess.expr.Value;
44  import org.apache.commons.lang3.StringUtils;
45  import static com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.ExprBuf;
46  
47  /**
48   *
49   * @author James Ahlborn
50   */
51  public class FormatUtil
52  {
53    public enum NumPatternType {
54      GENERAL,
55      CURRENCY {
56        @Override
57        protected void appendPrefix(StringBuilder fmt) {
58          fmt.append('\u00A4');
59        }
60        @Override
61        protected boolean useParensForNegatives(NumericConfig cfg) {
62          return cfg.useParensForCurrencyNegatives();
63        }
64      },
65      EURO {
66        @Override
67        protected void appendPrefix(StringBuilder fmt) {
68          fmt.append('\u20AC');
69        }
70        @Override
71        protected boolean useParensForNegatives(NumericConfig cfg) {
72          return cfg.useParensForCurrencyNegatives();
73        }
74      },
75      PERCENT {
76        @Override
77        protected void appendSuffix(StringBuilder fmt) {
78          fmt.append('%');
79        }
80      },
81      SCIENTIFIC {
82        @Override
83        protected void appendSuffix(StringBuilder fmt) {
84          fmt.append("E0");
85        }
86      };
87  
88      protected void appendPrefix(StringBuilder fmt) {}
89  
90      protected void appendSuffix(StringBuilder fmt) {}
91  
92      protected boolean useParensForNegatives(NumericConfig cfg) {
93        return cfg.useParensForNegatives();
94      }
95    }
96  
97    private enum TextCase {
98      NONE,
99      UPPER {
100       @Override public char apply(char c) {
101         return Character.toUpperCase(c);
102       }
103     },
104     LOWER {
105       @Override public char apply(char c) {
106         return Character.toLowerCase(c);
107       }
108     };
109 
110     public char apply(char c) {
111       return c;
112     }
113   }
114 
115   private static final Map<String,Fmt> PREDEF_FMTS = new HashMap<String,Fmt>();
116 
117   static {
118     putPredefFormat("General Date", args -> ValueSupport.toValue(
119                         args.coerceToDateTimeValue().getAsString()));
120     putPredefFormat("Long Date",
121                     new PredefDateFmt(TemporalConfig.Type.LONG_DATE));
122     putPredefFormat("Medium Date",
123                     new PredefDateFmt(TemporalConfig.Type.MEDIUM_DATE));
124     putPredefFormat("Short Date",
125                     new PredefDateFmt(TemporalConfig.Type.SHORT_DATE));
126     putPredefFormat("Long Time",
127                     new PredefDateFmt(TemporalConfig.Type.LONG_TIME));
128     putPredefFormat("Medium Time",
129                     new PredefDateFmt(TemporalConfig.Type.MEDIUM_TIME));
130     putPredefFormat("Short Time",
131                     new PredefDateFmt(TemporalConfig.Type.SHORT_TIME));
132 
133     putPredefFormat("General Number", args -> ValueSupport.toValue(
134                         args.coerceToNumberValue().getAsString()));
135     putPredefFormat("Currency",
136                     new PredefNumberFmt(NumericConfig.Type.CURRENCY));
137     putPredefFormat("Euro", new PredefNumberFmt(NumericConfig.Type.EURO));
138     putPredefFormat("Fixed",
139                     new PredefNumberFmt(NumericConfig.Type.FIXED));
140     putPredefFormat("Standard",
141                     new PredefNumberFmt(NumericConfig.Type.STANDARD));
142     putPredefFormat("Percent",
143                     new PredefNumberFmt(NumericConfig.Type.PERCENT));
144     putPredefFormat("Scientific", new ScientificPredefNumberFmt());
145 
146     putPredefFormat("True/False", new PredefBoolFmt("True", "False"));
147     putPredefFormat("Yes/No", new PredefBoolFmt("Yes", "No"));
148     putPredefFormat("On/Off", new PredefBoolFmt("On", "Off"));
149   }
150 
151   private static final Fmt NULL_FMT = args -> ValueSupport.EMPTY_STR_VAL;
152   private static final Fmt DUMMY_FMT = args -> args.getNonNullExpr();
153 
154   private static final char QUOTE_CHAR = '"';
155   private static final char ESCAPE_CHAR = '\\';
156   private static final char LEFT_ALIGN_CHAR = '!';
157   private static final char START_COLOR_CHAR = '[';
158   private static final char END_COLOR_CHAR = ']';
159   private static final char CHOICE_SEP_CHAR = ';';
160   // this only seems to be useful if you have fixed length string fields which
161   // isn't a normal thing in ms access
162   private static final char FILL_ESCAPE_CHAR = '*';
163   private static final char REQ_PLACEHOLDER_CHAR = '@';
164   private static final char OPT_PLACEHOLDER_CHAR = '&';
165   private static final char TO_UPPER_CHAR = '>';
166   private static final char TO_LOWER_CHAR = '<';
167   private static final char DT_LIT_COLON_CHAR = ':';
168   private static final char DT_LIT_SLASH_CHAR = '/';
169   private static final char SINGLE_QUOTE_CHAR = '\'';
170   private static final char EXP_E_CHAR = 'E';
171   private static final char EXP_e_CHAR = 'e';
172   private static final char PLUS_CHAR = '+';
173   private static final char MINUS_CHAR = '-';
174   private static final char REQ_DIGIT_CHAR = '0';
175   private static final int NO_CHAR = -1;
176 
177   private static final byte FCT_UNKNOWN = 0;
178   private static final byte FCT_LITERAL = 1;
179   private static final byte FCT_GENERAL = 2;
180   private static final byte FCT_DATE = 3;
181   private static final byte FCT_NUMBER = 4;
182   private static final byte FCT_TEXT = 5;
183 
184   private static final byte[] FORMAT_CODE_TYPES = new byte[127];
185   static {
186     setFormatCodeTypes(" $+-()", FCT_LITERAL);
187     setFormatCodeTypes("\"!*\\[];", FCT_GENERAL);
188     setFormatCodeTypes(":/cdwmqyhnstampmAMPM", FCT_DATE);
189     setFormatCodeTypes(".,0#%Ee", FCT_NUMBER);
190     setFormatCodeTypes("@&<>", FCT_TEXT);
191   }
192 
193   @FunctionalInterface
194   interface Fmt {
195     public Value format(Args args);
196   }
197 
198   @FunctionalInterface
199   interface DateFormatBuilder {
200     public void build(DateTimeFormatterBuilder dtfb, Args args,
201                       boolean hasAmPm, Value.Type dtType);
202   }
203 
204   private static final DateFormatBuilder PARTIAL_PREFIX =
205     (dtfb, args, hasAmPm, dtType) -> {
206       throw new UnsupportedOperationException();
207     };
208 
209   private static final Map<String,DateFormatBuilder> DATE_FMT_BUILDERS =
210     new HashMap<>();
211   static {
212     DATE_FMT_BUILDERS.put("c",
213       (dtfb, args, hasAmPm, dtType) ->
214         dtfb.append(ValueSupport.getDateFormatForType(
215                         args._ctx, dtType)));
216     DATE_FMT_BUILDERS.put("d", new SimpleDFB("d"));
217     DATE_FMT_BUILDERS.put("dd", new SimpleDFB("dd"));
218     DATE_FMT_BUILDERS.put("ddd", new SimpleDFB("eee"));
219     DATE_FMT_BUILDERS.put("dddd", new SimpleDFB("eeee"));
220     DATE_FMT_BUILDERS.put("ddddd", new PredefDFB(TemporalConfig.Type.SHORT_DATE));
221     DATE_FMT_BUILDERS.put("dddddd", new PredefDFB(TemporalConfig.Type.LONG_DATE));
222     DATE_FMT_BUILDERS.put("w", new WeekBasedDFB() {
223       @Override
224       protected TemporalField getField(WeekFields weekFields) {
225         return weekFields.dayOfWeek();
226       }
227     });
228     DATE_FMT_BUILDERS.put("ww", new WeekBasedDFB() {
229       @Override
230       protected TemporalField getField(WeekFields weekFields) {
231         return weekFields.weekOfWeekBasedYear();
232       }
233     });
234     DATE_FMT_BUILDERS.put("m", new SimpleDFB("L"));
235     DATE_FMT_BUILDERS.put("mm", new SimpleDFB("LL"));
236     DATE_FMT_BUILDERS.put("mmm", new SimpleDFB("MMM"));
237     DATE_FMT_BUILDERS.put("mmmm", new SimpleDFB("MMMM"));
238     DATE_FMT_BUILDERS.put("q", new SimpleDFB("Q"));
239     DATE_FMT_BUILDERS.put("y", new SimpleDFB("D"));
240     DATE_FMT_BUILDERS.put("yy", new SimpleDFB("yy"));
241     DATE_FMT_BUILDERS.put("yyyy", new SimpleDFB("yyyy"));
242     DATE_FMT_BUILDERS.put("h", new HourlyDFB("h", "H"));
243     DATE_FMT_BUILDERS.put("hh", new HourlyDFB("hh", "HH"));
244     DATE_FMT_BUILDERS.put("n", new SimpleDFB("m"));
245     DATE_FMT_BUILDERS.put("nn", new SimpleDFB("mm"));
246     DATE_FMT_BUILDERS.put("s", new SimpleDFB("s"));
247     DATE_FMT_BUILDERS.put("ss", new SimpleDFB("ss"));
248     DATE_FMT_BUILDERS.put("ttttt", new PredefDFB(TemporalConfig.Type.LONG_TIME));
249     DATE_FMT_BUILDERS.put("AM/PM", new AmPmDFB("AM", "PM"));
250     DATE_FMT_BUILDERS.put("am/pm", new AmPmDFB("am", "pm"));
251     DATE_FMT_BUILDERS.put("A/P", new AmPmDFB("A", "P"));
252     DATE_FMT_BUILDERS.put("a/p", new AmPmDFB("a", "p"));
253     DATE_FMT_BUILDERS.put("AMPM",
254       (dtfb, args, hasAmPm, dtType) -> {
255         String[] amPmStrs = args._ctx.getTemporalConfig().getAmPmStrings();
256         new AmPmDFB(amPmStrs[0], amPmStrs[1]).build(dtfb, args, hasAmPm, dtType);
257       }
258     );
259     fillInPartialPrefixes();
260   }
261 
262   private static final int NF_POS_IDX = 0;
263   private static final int NF_NEG_IDX = 1;
264   private static final int NF_ZERO_IDX = 2;
265   private static final int NF_NULL_IDX = 3;
266   private static final int NUM_NF_FMTS = 4;
267 
268   private static final NumberFormatter.NotationType[] NO_EXP_TYPES =
269     new NumberFormatter.NotationType[NUM_NF_FMTS];
270   private static final boolean[] NO_FMT_TYPES = new boolean[NUM_NF_FMTS];
271 
272 
273   private static final class Args
274   {
275     private final EvalContext _ctx;
276     private Value _expr;
277     private final int _firstDay;
278     private final int _firstWeekType;
279 
280     private Args(EvalContext ctx, Value expr, int firstDay, int firstWeekType) {
281       _ctx = ctx;
282       _expr = expr;
283       _firstDay = firstDay;
284       _firstWeekType = firstWeekType;
285     }
286 
287     public Args setExpr(Value expr) {
288       _expr = expr;
289       return this;
290     }
291 
292     public Value getNonNullExpr() {
293       return (_expr.isNull() ? ValueSupport.EMPTY_STR_VAL : _expr);
294     }
295 
296     public boolean isNullOrEmptyString() {
297       return(_expr.isNull() ||
298              // only a string value could ever be an empty string
299              (_expr.getType().isString() && getAsString().isEmpty()));
300     }
301 
302     public boolean maybeCoerceToEmptyString() {
303       if(isNullOrEmptyString()) {
304         // ensure that we have a non-null value when formatting (null acts
305         // like empty string)
306         _expr = ValueSupport.EMPTY_STR_VAL;
307         return true;
308       }
309       return false;
310     }
311 
312     public Args coerceToDateTimeValue() {
313       if(!_expr.getType().isTemporal()) {
314 
315         // format coerces boolean strings to numbers
316         Value boolExpr = null;
317         if(_expr.getType().isString() &&
318            ((boolExpr = maybeGetStringAsBooleanValue()) != null)) {
319           _expr = boolExpr;
320         }
321 
322         // StringValue already handles most String -> Number -> Date/Time, so
323         // most other convertions work here (and failures are thrown so that
324         // default handling kicks in)
325         _expr = _expr.getAsDateTimeValue(_ctx);
326       }
327       return this;
328     }
329 
330     public Args coerceToNumberValue() {
331       if(!_expr.getType().isNumeric()) {
332         if(_expr.getType().isString()) {
333 
334           // format coerces "true"/"false" to boolean values
335           Value boolExpr = maybeGetStringAsBooleanValue();
336           if(boolExpr != null) {
337             _expr = boolExpr;
338           } else {
339             BigDecimal bd = DefaultFunctions.maybeGetAsBigDecimal(_ctx, _expr);
340             if(bd != null) {
341               _expr = ValueSupport.toValue(bd);
342             } else {
343               // convert to date to number.  this doesn't happen as part of the
344               // default value coercion behavior, but the format method tries
345               // harder
346               Value maybe = DefaultFunctions.maybeGetAsDateTimeValue(
347                   _ctx, _expr);
348               if(maybe != null) {
349                 _expr = ValueSupport.toValue(maybe.getAsDouble(_ctx));
350               } else {
351                 // string which can't be converted to number force failure
352                 // here so default formatting will kick in
353                 throw new EvalException("invalid number value");
354               }
355             }
356           }
357         } else {
358           // convert date to number
359           _expr = ValueSupport.toValue(_expr.getAsDouble(_ctx));
360         }
361       }
362       return this;
363     }
364 
365     private Value maybeGetStringAsBooleanValue() {
366       // format coerces "true"/"false" to boolean values
367       String val = getAsString();
368       if("true".equalsIgnoreCase(val)) {
369         return ValueSupport.TRUE_VAL;
370       }
371       if("false".equalsIgnoreCase(val)) {
372         return ValueSupport.FALSE_VAL;
373       }
374       return null;
375     }
376 
377     public BigDecimal getAsBigDecimal() {
378       coerceToNumberValue();
379       return _expr.getAsBigDecimal(_ctx);
380     }
381 
382     public LocalDateTime getAsLocalDateTime() {
383       coerceToDateTimeValue();
384       return _expr.getAsLocalDateTime(_ctx);
385     }
386 
387     public boolean getAsBoolean() {
388       // even though string values have a "boolean" value, for formatting,
389       // strings which don't convert to valid boolean/number/date are just
390       // returned as is.  so we use coerceToNumberValue to force the exception
391       // to be thrown which results in the "default" formatting behavior.
392       coerceToNumberValue();
393       return _expr.getAsBoolean(_ctx);
394     }
395 
396     public String getAsString() {
397       return _expr.getAsString(_ctx);
398     }
399 
400     public Value format(Fmt fmt) {
401       Value origExpr = _expr;
402       try {
403         return fmt.format(this);
404       } catch(EvalException ee) {
405         // values which cannot be formatted as the target type are just
406         // returned "as is"
407         return origExpr;
408       }
409     }
410   }
411 
412   private FormatUtil() {}
413 
414   /**
415    * Utility for leveraging format support outside of expression evaluation.
416    */
417   public static class StandaloneFormatter
418   {
419     private final Fmt _fmt;
420     private final Args _args;
421 
422     private StandaloneFormatter(Fmt fmt, Args args) {
423       _fmt = fmt;
424       _args = args;
425     }
426 
427     public Valueef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value format(Value expr) {
428       return _args.setExpr(expr).format(_fmt);
429     }
430   }
431 
432   public static Value./com/healthmarketscience/jackcess/expr/Value.html#Value">Value format(EvalContext ctx, Value expr, String fmtStr,
433                              int firstDay, int firstWeekType) {
434     Args args = new Args(ctx, expr, firstDay, firstWeekType);
435     return args.format(createFormat(args, fmtStr));
436   }
437 
438   public static StandaloneFormatter createStandaloneFormatter(
439       EvalContext ctx, String fmtStr, int firstDay, int firstWeekType) {
440     Args args = new Args(ctx, null, firstDay, firstWeekType);
441     Fmt fmt = createFormat(args, fmtStr);
442     return new StandaloneFormatter(fmt, args);
443   }
444 
445   private static Fmt createFormat(Args args, String fmtStr) {
446     Fmt predefFmt = PREDEF_FMTS.get(fmtStr);
447     if(predefFmt != null) {
448       return predefFmt;
449     }
450 
451     if(StringUtils.isEmpty(fmtStr)) {
452       return DUMMY_FMT;
453     }
454 
455     // TODO implement caching for custom formats?  put into Bindings.  use
456     // special "cache" prefix to know which caches to clear when evalconfig
457     // is altered (could also cache other Format* functions)
458 
459     return parseCustomFormat(fmtStr, args);
460   }
461 
462   private static Fmt parseCustomFormat(String fmtStr, Args args) {
463 
464     ExprBuf buf = new ExprBuf(fmtStr, null);
465 
466     // do partial pass to determine what type of format this is
467     byte curFormatType = determineFormatType(buf);
468 
469     // reset buffer for real parse
470     buf.reset(0);
471 
472     switch(curFormatType) {
473     case FCT_GENERAL:
474       return parseCustomGeneralFormat(buf);
475     case FCT_DATE:
476       return parseCustomDateFormat(buf, args);
477     case FCT_NUMBER:
478       return parseCustomNumberFormat(buf, args);
479     case FCT_TEXT:
480       return parseCustomTextFormat(buf);
481     default:
482       throw new EvalException("Invalid format type " + curFormatType);
483     }
484   }
485 
486   private static byte determineFormatType(ExprBuf buf) {
487 
488     while(buf.hasNext()) {
489       char c = buf.next();
490       byte fmtType = getFormatCodeType(c);
491       switch(fmtType) {
492       case FCT_UNKNOWN:
493       case FCT_LITERAL:
494         // meaningless, ignore for now
495         break;
496       case FCT_GENERAL:
497         switch(c) {
498         case QUOTE_CHAR:
499           parseQuotedString(buf);
500           break;
501         case START_COLOR_CHAR:
502           parseColorString(buf);
503           break;
504         case ESCAPE_CHAR:
505         case FILL_ESCAPE_CHAR:
506           if(buf.hasNext()) {
507             buf.next();
508           }
509           break;
510         default:
511           // meaningless, ignore for now
512         }
513         break;
514       case FCT_DATE:
515       case FCT_NUMBER:
516       case FCT_TEXT:
517         // found specific type
518         return fmtType;
519       default:
520         throw new EvalException("Invalid format type " + fmtType);
521       }
522     }
523 
524     // no specific type
525     return FCT_GENERAL;
526   }
527 
528   private static Fmt parseCustomGeneralFormat(ExprBuf buf) {
529 
530     // a "general" format is actually a "yes/no" format which functions almost
531     // exactly like a number format (without any number format specific chars)
532 
533     StringBuilder sb = new StringBuilder();
534     String[] fmtStrs = new String[NUM_NF_FMTS];
535     int fmtIdx = 0;
536 
537     BUF_LOOP:
538     while(buf.hasNext()) {
539       char c = buf.next();
540       int fmtType = getFormatCodeType(c);
541       switch(fmtType) {
542       case FCT_GENERAL:
543         switch(c) {
544         case LEFT_ALIGN_CHAR:
545           // no effect
546           break;
547         case QUOTE_CHAR:
548           parseQuotedString(buf, sb);
549           break;
550         case START_COLOR_CHAR:
551           // color strings seem to be ignored
552           parseColorString(buf);
553           break;
554         case ESCAPE_CHAR:
555           if(buf.hasNext()) {
556             sb.append(buf.next());
557           }
558           break;
559         case FILL_ESCAPE_CHAR:
560           // unclear what this actually does.  online examples don't seem to
561           // match with experimental results.  for now, ignore
562           if(buf.hasNext()) {
563             buf.next();
564           }
565           break;
566         case CHOICE_SEP_CHAR:
567           // yes/no (number) format supports up to 4 formats: pos, neg, zero,
568           // null.  after that, ignore the rest
569           if(fmtIdx == (NUM_NF_FMTS - 1)) {
570             // last possible format, ignore remaining
571             break BUF_LOOP;
572           }
573           addCustomGeneralFormat(fmtStrs, fmtIdx++, sb);
574           break;
575         default:
576           sb.append(c);
577         }
578         break;
579       default:
580         sb.append(c);
581       }
582     }
583 
584     // fill in remaining formats
585     while(fmtIdx < NUM_NF_FMTS) {
586       addCustomGeneralFormat(fmtStrs, fmtIdx++, sb);
587     }
588 
589     return new CustomGeneralFmt(
590         ValueSupport.toValue(fmtStrs[NF_POS_IDX]),
591         ValueSupport.toValue(fmtStrs[NF_NEG_IDX]),
592         ValueSupport.toValue(fmtStrs[NF_ZERO_IDX]),
593         ValueSupport.toValue(fmtStrs[NF_NULL_IDX]));
594   }
595 
596   private static void addCustomGeneralFormat(String[] fmtStrs, int fmtIdx,
597                                              StringBuilder sb)
598   {
599     addCustomNumberFormat(fmtStrs, NO_EXP_TYPES, NO_FMT_TYPES, NO_FMT_TYPES,
600                           fmtIdx, sb);
601   }
602 
603   private static Fmt parseCustomDateFormat(ExprBuf buf, Args args) {
604 
605     // keep track of some extra state while parsing the format, whether or not
606     // there was an am/pm pattern and whether or not there was a general
607     // date/time pattern
608     boolean[] fmtState = new boolean[]{false, false};
609     List<DateFormatBuilder> dfbs = new ArrayList<>();
610 
611     BUF_LOOP:
612     while(buf.hasNext()) {
613       char c = buf.next();
614       int fmtType = getFormatCodeType(c);
615       switch(fmtType) {
616       case FCT_GENERAL:
617         switch(c) {
618         case QUOTE_CHAR:
619           String str = parseQuotedString(buf);
620           dfbs.add((dtfb, argsParam, hasAmPmParam, dtType) ->
621                    dtfb.appendLiteral(str));
622           break;
623         case START_COLOR_CHAR:
624           // color strings seem to be ignored
625           parseColorString(buf);
626           break;
627         case ESCAPE_CHAR:
628           if(buf.hasNext()) {
629             dfbs.add(buildLiteralCharDFB(buf.next()));
630           }
631           break;
632         case FILL_ESCAPE_CHAR:
633           // unclear what this actually does.  online examples don't seem to
634           // match with experimental results.  for now, ignore
635           if(buf.hasNext()) {
636             buf.next();
637           }
638           break;
639         case CHOICE_SEP_CHAR:
640           // date/time doesn't use multiple pattern choices, but it does
641           // respect the char.  ignore everything after the first choice
642           break BUF_LOOP;
643         default:
644           dfbs.add(buildLiteralCharDFB(c));
645         }
646         break;
647       case FCT_DATE:
648         parseCustomDateFormatPattern(c, buf, dfbs, fmtState);
649         break;
650       default:
651         dfbs.add(buildLiteralCharDFB(c));
652       }
653     }
654 
655     boolean hasAmPm = fmtState[0];
656     boolean hasGeneralFormat = fmtState[1];
657     if(!hasGeneralFormat) {
658       // simple situation, one format for every value
659       DateTimeFormatter dtf = createDateTimeFormatter(dfbs, args, hasAmPm, null);
660       return new CustomFmt(argsParam -> ValueSupport.toValue(
661                                dtf.format(argsParam.getAsLocalDateTime())));
662     }
663 
664     // we need separate formatters for date, time, and date/time values
665     DateTimeFormatter dateFmt = createDateTimeFormatter(dfbs, args, hasAmPm,
666                                                         Value.Type.DATE);
667     DateTimeFormatter timeFmt = createDateTimeFormatter(dfbs, args, hasAmPm,
668                                                         Value.Type.TIME);
669     DateTimeFormatter dtFmt = createDateTimeFormatter(dfbs, args, hasAmPm,
670                                                       Value.Type.DATE_TIME);
671 
672     return new CustomFmt(argsParam -> formatDateTime(
673                              argsParam, dateFmt, timeFmt, dtFmt));
674   }
675 
676   private static void parseCustomDateFormatPattern(
677       char c, ExprBuf buf, List<DateFormatBuilder> dfbs,
678       boolean[] fmtState) {
679 
680     if((c == DT_LIT_COLON_CHAR) || (c == DT_LIT_SLASH_CHAR)) {
681       // date/time literal char, nothing more to do
682       dfbs.add(buildLiteralCharDFB(c));
683       return;
684     }
685 
686     StringBuilder sb = buf.getScratchBuffer();
687     sb.append(c);
688 
689     char firstChar = c;
690     int firstPos = buf.curPos();
691     String bestMatchPat = sb.toString();
692 
693     DateFormatBuilder bestMatch = DATE_FMT_BUILDERS.get(bestMatchPat);
694     int bestPos = firstPos;
695     while(buf.hasNext()) {
696       sb.append(buf.next());
697       String tmpPat = sb.toString();
698       DateFormatBuilder dfb = DATE_FMT_BUILDERS.get(tmpPat);
699       if(dfb == null) {
700         // no more possible matches
701         break;
702       }
703       if(dfb != PARTIAL_PREFIX) {
704         // this is the longest, valid pattern we have seen so far
705         bestMatch = dfb;
706         bestPos = buf.curPos();
707         bestMatchPat = tmpPat;
708       }
709     }
710 
711     if(bestMatch != PARTIAL_PREFIX) {
712 
713       // apply valid pattern
714       buf.reset(bestPos);
715       dfbs.add(bestMatch);
716 
717       switch(firstChar) {
718       case 'a':
719       case 'A':
720         // this was an am/pm pattern
721         fmtState[0] = true;
722         break;
723       case 'c':
724         // this was a general date/time format
725         fmtState[1] = true;
726         break;
727       default:
728         // don't care
729       }
730 
731     } else {
732 
733       // just consume the first char
734       buf.reset(firstPos);
735       dfbs.add(buildLiteralCharDFB(firstChar));
736     }
737   }
738 
739   private static DateFormatBuilder buildLiteralCharDFB(char c) {
740     return (dtfb, args, hasAmPm, dtType) -> dtfb.appendLiteral(c);
741   }
742 
743   private static DateTimeFormatter createDateTimeFormatter(
744       List<DateFormatBuilder> dfbs, Args args, boolean hasAmPm,
745       Value.Type dtType)
746   {
747     DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder();
748     dfbs.forEach(d -> d.build(dtfb, args, hasAmPm, dtType));
749     return dtfb.toFormatter(args._ctx.getTemporalConfig().getLocale());
750   }
751 
752   private static Value formatDateTime(
753       Args args, DateTimeFormatter dateFmt,
754       DateTimeFormatter timeFmt, DateTimeFormatter dtFmt)
755   {
756     LocalDateTime ldt = args.getAsLocalDateTime();
757     DateTimeFormatter fmt = null;
758     switch(args._expr.getType()) {
759     case DATE:
760       fmt = dateFmt;
761       break;
762     case TIME:
763       fmt = timeFmt;
764       break;
765     default:
766       fmt = dtFmt;
767     }
768 
769     return ValueSupport.toValue(fmt.format(ldt));
770   }
771 
772   private static Fmt parseCustomNumberFormat(ExprBuf buf, Args args) {
773 
774     StringBuilder sb = new StringBuilder();
775     String[] fmtStrs = new String[NUM_NF_FMTS];
776     int fmtIdx = 0;
777     StringBuilder pendingLiteral = new StringBuilder();
778     NumberFormatter.NotationType[] expTypes =
779       new NumberFormatter.NotationType[NUM_NF_FMTS];
780     boolean[] hasFmts = new boolean[NUM_NF_FMTS];
781     boolean[] hasReqDigit = new boolean[NUM_NF_FMTS];
782 
783     BUF_LOOP:
784     while(buf.hasNext()) {
785       char c = buf.next();
786       int fmtType = getFormatCodeType(c);
787       switch(fmtType) {
788       case FCT_GENERAL:
789         switch(c) {
790         case LEFT_ALIGN_CHAR:
791           // no effect
792           break;
793         case QUOTE_CHAR:
794           parseQuotedString(buf, pendingLiteral);
795           break;
796         case START_COLOR_CHAR:
797           // color strings seem to be ignored
798           parseColorString(buf);
799           break;
800         case ESCAPE_CHAR:
801           if(buf.hasNext()) {
802             pendingLiteral.append(buf.next());
803           }
804           break;
805         case FILL_ESCAPE_CHAR:
806           // unclear what this actually does.  online examples don't seem to
807           // match with experimental results.  for now, ignore
808           if(buf.hasNext()) {
809             buf.next();
810           }
811           break;
812         case CHOICE_SEP_CHAR:
813           // number format supports up to 4 formats: pos, neg, zero, null.
814           // after that, ignore the rest
815           if(fmtIdx == (NUM_NF_FMTS - 1)) {
816             // last possible format, ignore remaining
817             break BUF_LOOP;
818           }
819           flushPendingNumberLiteral(pendingLiteral, sb);
820           addCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit,
821                                 fmtIdx++, sb);
822           break;
823         default:
824           pendingLiteral.append(c);
825         }
826         break;
827       case FCT_NUMBER:
828         hasFmts[fmtIdx] = true;
829         switch(c) {
830         case EXP_E_CHAR:
831           int signChar = buf.peekNext();
832           if((signChar == PLUS_CHAR) || (signChar == MINUS_CHAR)) {
833             buf.next();
834             expTypes[fmtIdx] = ((signChar == PLUS_CHAR) ?
835                                 NumberFormatter.NotationType.EXP_E_PLUS :
836                                 NumberFormatter.NotationType.EXP_E_MINUS);
837             flushPendingNumberLiteral(pendingLiteral, sb);
838             sb.append(EXP_E_CHAR);
839           } else {
840             pendingLiteral.append(c);
841           }
842           break;
843         case EXP_e_CHAR:
844           signChar = buf.peekNext();
845           if((signChar == PLUS_CHAR) || (signChar == MINUS_CHAR)) {
846             buf.next();
847             expTypes[fmtIdx] = ((signChar == PLUS_CHAR) ?
848                                 NumberFormatter.NotationType.EXP_e_PLUS :
849                                 NumberFormatter.NotationType.EXP_e_MINUS);
850             flushPendingNumberLiteral(pendingLiteral, sb);
851             sb.append(EXP_E_CHAR);
852           } else {
853             pendingLiteral.append(c);
854           }
855           break;
856         case REQ_DIGIT_CHAR:
857           hasReqDigit[fmtIdx] = true;
858           flushPendingNumberLiteral(pendingLiteral, sb);
859           sb.append(c);
860           break;
861         default:
862           // most number format chars pass straight through
863           flushPendingNumberLiteral(pendingLiteral, sb);
864           sb.append(c);
865         }
866         break;
867       default:
868         pendingLiteral.append(c);
869       }
870     }
871 
872     // fill in remaining formats
873     while(fmtIdx < NUM_NF_FMTS) {
874       flushPendingNumberLiteral(pendingLiteral, sb);
875       addCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit,
876                             fmtIdx++, sb);
877     }
878 
879     return new CustomNumberFmt(
880         createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit,
881                                  NF_POS_IDX, false, args, buf),
882         createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit,
883                                  NF_NEG_IDX, false, args, buf),
884         createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit,
885                                  NF_ZERO_IDX, true, args, buf),
886         createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit,
887                                  NF_NULL_IDX, true, args, buf));
888   }
889 
890   private static void addCustomNumberFormat(
891       String[] fmtStrs, NumberFormatter.NotationType[] expTypes,
892       boolean[] hasFmts, boolean[] hasReqDigit, int fmtIdx, StringBuilder sb)
893   {
894     if(sb.length() == 0) {
895       // do special empty format handling on a per-format-type basis
896       switch(fmtIdx) {
897       case NF_NEG_IDX:
898         // re-use "pos" format
899         sb.append('-').append(fmtStrs[NF_POS_IDX]);
900         expTypes[NF_NEG_IDX] = expTypes[NF_POS_IDX];
901         hasFmts[NF_NEG_IDX] = hasFmts[NF_POS_IDX];
902         hasReqDigit[NF_NEG_IDX] = hasReqDigit[NF_POS_IDX];
903         break;
904       case NF_ZERO_IDX:
905         // re-use "pos" format
906         sb.append(fmtStrs[NF_POS_IDX]);
907         expTypes[NF_ZERO_IDX] = expTypes[NF_POS_IDX];
908         hasFmts[NF_ZERO_IDX] = hasFmts[NF_POS_IDX];
909         hasReqDigit[NF_ZERO_IDX] = hasReqDigit[NF_POS_IDX];
910         break;
911       default:
912         // use empty string result
913       }
914     }
915 
916     fmtStrs[fmtIdx] = sb.toString();
917     sb.setLength(0);
918   }
919 
920   private static void flushPendingNumberLiteral(
921       StringBuilder pendingLiteral, StringBuilder sb) {
922     if(pendingLiteral.length() == 0) {
923       return;
924     }
925 
926     if((pendingLiteral.length() == 1) &&
927        pendingLiteral.charAt(0) == SINGLE_QUOTE_CHAR) {
928       // handle standalone single quote
929       sb.append(SINGLE_QUOTE_CHAR).append(SINGLE_QUOTE_CHAR);
930       pendingLiteral.setLength(0);
931       return;
932     }
933 
934     sb.append(SINGLE_QUOTE_CHAR);
935     int startPos = sb.length();
936     sb.append(pendingLiteral);
937 
938     // we need to quote any single quotes in the literal string
939     for(int i = startPos; i < sb.length(); ++i) {
940       char c = sb.charAt(i);
941       if(c == SINGLE_QUOTE_CHAR) {
942         sb.insert(++i, SINGLE_QUOTE_CHAR);
943       }
944     }
945 
946     sb.append(SINGLE_QUOTE_CHAR);
947     pendingLiteral.setLength(0);
948   }
949 
950   private static BDFormat createCustomNumberFormat(
951       String[] fmtStrs, NumberFormatter.NotationType[] expTypes,
952       boolean[] hasFmts, boolean[] hasReqDigit, int fmtIdx,
953       boolean isZeroFmt, Args args, ExprBuf buf) {
954 
955     String fmtStr = fmtStrs[fmtIdx];
956     if(!hasFmts[fmtIdx]) {
957       // convert the literal string to a dummy number format
958       if(fmtStr.length() > 0) {
959         // strip quoting
960         StringBuilder sb = buf.getScratchBuffer().append(fmtStr)
961           .deleteCharAt(fmtStr.length() - 1)
962           .deleteCharAt(0);
963         if(sb.length() > 0) {
964           for(int i = 0; i < sb.length(); ++i) {
965             if(sb.charAt(i) == SINGLE_QUOTE_CHAR) {
966               // delete next single quote char
967               sb.deleteCharAt(++i);
968             }
969           }
970         } else {
971           // this was a single, literal single quote
972           sb.append(SINGLE_QUOTE_CHAR);
973         }
974         fmtStr = sb.toString();
975       }
976       return new LiteralBDFormat(fmtStr);
977     }
978 
979     NumberFormatter.NotationType expType = expTypes[fmtIdx];
980     DecimalFormat df = args._ctx.createDecimalFormat(fmtStr);
981 
982     if(df.getMaximumFractionDigits() > 0) {
983       // if the decimal is included in the format, access always shows it
984       df.setDecimalSeparatorAlwaysShown(true);
985     }
986 
987     if(expType != null) {
988       NumberFormat nf = new NumberFormatter.ScientificFormat(df, expType);
989       if(isZeroFmt) {
990         return new LiteralBDFormat(nf.format(BigDecimal.ZERO));
991       }
992       return new BaseBDFormat(nf);
993     }
994 
995     if(!hasReqDigit[fmtIdx]) {
996       // java likes to force extra 0's while access doesn't
997       df.setMinimumIntegerDigits(0);
998     }
999 
1000     if(isZeroFmt) {
1001 
1002       String zeroVal = df.format(BigDecimal.ZERO);
1003       if(!hasReqDigit[fmtIdx]) {
1004         // java forces a 0 but access doesn't.  delete any 0 chars which were
1005         // inserted by the java format
1006         int prefLen = df.getPositivePrefix().length();
1007         int len = zeroVal.length() - df.getPositiveSuffix().length();
1008         StringBuilder sb = buf.getScratchBuffer().append(zeroVal);
1009         for(int i = len - 1; i >= prefLen; --i) {
1010           if(sb.charAt(i) == '0') {
1011             sb.deleteCharAt(i);
1012           }
1013         }
1014         zeroVal = sb.toString();
1015       }
1016 
1017       return  new LiteralBDFormat(zeroVal);
1018     }
1019 
1020     return new DecimalBDFormat(df);
1021   }
1022 
1023   private static Fmt parseCustomTextFormat(ExprBuf buf) {
1024 
1025     Fmt fmt = null;
1026 
1027     List<BiConsumer<StringBuilder,CharSource>> subFmts = new ArrayList<>();
1028     int numPlaceholders = 0;
1029     boolean rightAligned = true;
1030     TextCase textCase = TextCase.NONE;
1031     StringBuilder pendingLiteral = new StringBuilder();
1032     boolean hasFmtChars = false;
1033 
1034     BUF_LOOP:
1035     while(buf.hasNext()) {
1036       char c = buf.next();
1037       hasFmtChars = true;
1038       int fmtType = getFormatCodeType(c);
1039       switch(fmtType) {
1040       case FCT_GENERAL:
1041         switch(c) {
1042         case LEFT_ALIGN_CHAR:
1043           rightAligned = false;
1044           break;
1045         case QUOTE_CHAR:
1046           parseQuotedString(buf, pendingLiteral);
1047           break;
1048         case START_COLOR_CHAR:
1049           // color strings seem to be ignored
1050           parseColorString(buf);
1051           break;
1052         case ESCAPE_CHAR:
1053           if(buf.hasNext()) {
1054             pendingLiteral.append(buf.next());
1055           }
1056           break;
1057         case FILL_ESCAPE_CHAR:
1058           // unclear what this actually does.  online examples don't seem to
1059           // match with experimental results.  for now, ignore
1060           if(buf.hasNext()) {
1061             buf.next();
1062           }
1063           break;
1064         case CHOICE_SEP_CHAR:
1065           // text format supports up to 2 formats: normal and empty/null.
1066           // after that, ignore the rest
1067           if(fmt != null) {
1068             // ignore remaining format
1069             break BUF_LOOP;
1070           }
1071           flushPendingTextLiteral(pendingLiteral, subFmts);
1072           fmt = new CharSourceFmt(subFmts, numPlaceholders, rightAligned,
1073                                   textCase);
1074           // reset for next format
1075           subFmts = new ArrayList<>();
1076           numPlaceholders = 0;
1077           rightAligned = true;
1078           textCase = TextCase.NONE;
1079           hasFmtChars = false;
1080           break;
1081         default:
1082           pendingLiteral.append(c);
1083         }
1084         break;
1085       case FCT_TEXT:
1086         switch(c) {
1087         case REQ_PLACEHOLDER_CHAR:
1088           flushPendingTextLiteral(pendingLiteral, subFmts);
1089           ++numPlaceholders;
1090           subFmts.add((sb,cs) -> {
1091               int tmp = cs.next();
1092               sb.append((tmp != NO_CHAR) ? (char)tmp : ' ');
1093             });
1094           break;
1095         case OPT_PLACEHOLDER_CHAR:
1096           flushPendingTextLiteral(pendingLiteral, subFmts);
1097           ++numPlaceholders;
1098           subFmts.add((sb,cs) -> {
1099               int tmp = cs.next();
1100               if(tmp != NO_CHAR) {
1101                 sb.append((char)tmp);
1102               }
1103             });
1104           break;
1105         case TO_UPPER_CHAR:
1106           // an uppper and lower symbol cancel each other out
1107           textCase = ((textCase == TextCase.LOWER) ?
1108                       TextCase.NONE : TextCase.UPPER);
1109           break;
1110         case TO_LOWER_CHAR:
1111           // an uppper and lower symbol cancel each other out
1112           textCase = ((textCase == TextCase.UPPER) ?
1113                       TextCase.NONE : TextCase.LOWER);
1114           break;
1115         default:
1116           pendingLiteral.append(c);
1117         }
1118         break;
1119       default:
1120         pendingLiteral.append(c);
1121       }
1122     }
1123 
1124     flushPendingTextLiteral(pendingLiteral, subFmts);
1125 
1126     Fmt emptyFmt = null;
1127     if(fmt == null) {
1128       fmt = new CharSourceFmt(subFmts, numPlaceholders, rightAligned,
1129                               textCase);
1130       emptyFmt = NULL_FMT;
1131     } else {
1132       emptyFmt = (hasFmtChars ?
1133                   new CharSourceFmt(subFmts, numPlaceholders, rightAligned,
1134                                     textCase) :
1135                   NULL_FMT);
1136     }
1137 
1138     return new CustomFmt(fmt, emptyFmt);
1139   }
1140 
1141   private static void flushPendingTextLiteral(
1142       StringBuilder pendingLiteral,
1143       List<BiConsumer<StringBuilder,CharSource>> subFmts) {
1144     if(pendingLiteral.length() == 0) {
1145       return;
1146     }
1147 
1148     String literal = pendingLiteral.toString();
1149     pendingLiteral.setLength(0);
1150     subFmts.add((sb, cs) -> sb.append(literal));
1151   }
1152 
1153   public static String createNumberFormatPattern(
1154       NumPatternType numPatType, int numDecDigits, boolean incLeadDigit,
1155       boolean negParens, int numGroupDigits) {
1156 
1157     StringBuilder fmt = new StringBuilder();
1158 
1159     numPatType.appendPrefix(fmt);
1160 
1161     if(numGroupDigits > 0) {
1162       fmt.append("#,");
1163       DefaultTextFunctions.nchars(fmt, numGroupDigits - 1, '#');
1164     }
1165 
1166     fmt.append(incLeadDigit ? "0" : "#");
1167     if(numDecDigits > 0) {
1168       fmt.append(".");
1169       DefaultTextFunctions.nchars(fmt, numDecDigits, '0');
1170     }
1171 
1172     numPatType.appendSuffix(fmt);
1173 
1174     if(negParens) {
1175       // the javadocs claim the second pattern does not need to be fully
1176       // defined, but it doesn't seem to work that way
1177       String mainPat = fmt.toString();
1178       fmt.append(";(").append(mainPat).append(")");
1179     }
1180 
1181     return fmt.toString();
1182   }
1183 
1184   private static byte getFormatCodeType(char c) {
1185     if((c >= 0) && (c < 127)) {
1186       return FORMAT_CODE_TYPES[c];
1187     }
1188     return FCT_UNKNOWN;
1189   }
1190 
1191   private static void setFormatCodeTypes(String chars, byte type) {
1192     for(char c : chars.toCharArray()) {
1193       FORMAT_CODE_TYPES[c] = type;
1194     }
1195   }
1196 
1197   private static String parseQuotedString(ExprBuf buf) {
1198     return ExpressionTokenizer.parseStringUntil(buf, null, QUOTE_CHAR, true);
1199   }
1200 
1201   private static void parseQuotedString(ExprBuf buf, StringBuilder sb) {
1202     ExpressionTokenizer.parseStringUntil(buf, null, QUOTE_CHAR, true, sb);
1203   }
1204 
1205   private static String parseColorString(ExprBuf buf) {
1206     return ExpressionTokenizer.parseStringUntil(
1207         buf, START_COLOR_CHAR, END_COLOR_CHAR, false);
1208   }
1209 
1210   private static void fillInPartialPrefixes() {
1211     List<String> validPrefixes = new ArrayList<>(DATE_FMT_BUILDERS.keySet());
1212     for(String validPrefix : validPrefixes) {
1213       int len = validPrefix.length();
1214       while(len > 1) {
1215         --len;
1216         validPrefix = validPrefix.substring(0, len);
1217         DATE_FMT_BUILDERS.putIfAbsent(validPrefix, PARTIAL_PREFIX);
1218       }
1219     }
1220   }
1221 
1222   private static void putPredefFormat(String key, Fmt fmt) {
1223     // predefined formats return empty string for null
1224     Fmt wrapFmt = args -> (args.isNullOrEmptyString() ?
1225                            ValueSupport.EMPTY_STR_VAL :
1226                            fmt.format(args));
1227     PREDEF_FMTS.put(key, wrapFmt);
1228   }
1229 
1230   private static final class PredefDateFmt implements Fmt
1231   {
1232     private final TemporalConfig.Type _type;
1233 
1234     private PredefDateFmt(TemporalConfig.Type type) {
1235       _type = type;
1236     }
1237 
1238     @Override
1239     public Value format(Args args) {
1240       DateTimeFormatter dtf = args._ctx.createDateFormatter(
1241           args._ctx.getTemporalConfig().getDateTimeFormat(_type));
1242       return ValueSupport.toValue(dtf.format(args.getAsLocalDateTime()));
1243     }
1244   }
1245 
1246   private static final class PredefBoolFmt implements Fmt
1247   {
1248     private final Value _trueVal;
1249     private final Value _falseVal;
1250 
1251     private PredefBoolFmt(String trueStr, String falseStr) {
1252       _trueVal = ValueSupport.toValue(trueStr);
1253       _falseVal = ValueSupport.toValue(falseStr);
1254     }
1255 
1256     @Override
1257     public Value format(Args args) {
1258       return(args.getAsBoolean() ? _trueVal : _falseVal);
1259     }
1260   }
1261 
1262   private static abstract class BaseNumberFmt implements Fmt
1263   {
1264     @Override
1265     public Value format(Args args) {
1266       NumberFormat df = getNumberFormat(args);
1267       return ValueSupport.toValue(df.format(args.getAsBigDecimal()));
1268     }
1269 
1270     protected abstract NumberFormat getNumberFormat(Args args);
1271   }
1272 
1273   private static final class PredefNumberFmt extends BaseNumberFmt
1274   {
1275     private final NumericConfig.Type _type;
1276 
1277     private PredefNumberFmt(NumericConfig.Type type) {
1278       _type = type;
1279     }
1280 
1281     @Override
1282     protected NumberFormat getNumberFormat(Args args) {
1283       return args._ctx.createDecimalFormat(
1284           args._ctx.getNumericConfig().getNumberFormat(_type));
1285     }
1286   }
1287 
1288   private static final class ScientificPredefNumberFmt extends BaseNumberFmt
1289   {
1290     @Override
1291     protected NumberFormat getNumberFormat(Args args) {
1292       NumberFormat df = args._ctx.createDecimalFormat(
1293           args._ctx.getNumericConfig().getNumberFormat(
1294               NumericConfig.Type.SCIENTIFIC));
1295       df = new NumberFormatter.ScientificFormat(df);
1296       return df;
1297     }
1298   }
1299 
1300   private static final class SimpleDFB implements DateFormatBuilder
1301   {
1302     private final String _pat;
1303 
1304     private SimpleDFB(String pat) {
1305       _pat = pat;
1306     }
1307     @Override
1308     public void build(DateTimeFormatterBuilder dtfb, Args args,
1309                       boolean hasAmPm, Value.Type dtType) {
1310       dtfb.appendPattern(_pat);
1311     }
1312   }
1313 
1314   private static final class HourlyDFB implements DateFormatBuilder
1315   {
1316     private final String _pat12;
1317     private final String _pat24;
1318 
1319     private HourlyDFB(String pat12, String pat24) {
1320       _pat12 = pat12;
1321       _pat24 = pat24;
1322     }
1323     @Override
1324     public void build(DateTimeFormatterBuilder dtfb, Args args,
1325                       boolean hasAmPm, Value.Type dtTypePm) {
1326       // annoyingly the "hour" patterns are the same and depend on the
1327       // existence of the am/pm pattern to determine how they function (12 vs
1328       // 24 hour).
1329       dtfb.appendPattern(hasAmPm ? _pat12 : _pat24);
1330     }
1331   }
1332 
1333   private static final class PredefDFB implements DateFormatBuilder
1334   {
1335     private final TemporalConfig.Type _type;
1336 
1337     private PredefDFB(TemporalConfig.Type type) {
1338       _type = type;
1339     }
1340     @Override
1341     public void build(DateTimeFormatterBuilder dtfb, Args args,
1342                       boolean hasAmPm, Value.Type dtType) {
1343       dtfb.appendPattern(args._ctx.getTemporalConfig().getDateTimeFormat(_type));
1344     }
1345   }
1346 
1347   private static abstract class WeekBasedDFB implements DateFormatBuilder
1348   {
1349     @Override
1350     public void build(DateTimeFormatterBuilder dtfb, Args args,
1351                       boolean hasAmPm, Value.Type dtType) {
1352       dtfb.appendValue(getField(DefaultDateFunctions.weekFields(
1353                                     args._firstDay, args._firstWeekType)));
1354     }
1355 
1356     protected abstract TemporalField getField(WeekFields weekFields);
1357   }
1358 
1359   private static final class AmPmDFB extends AbstractMap<Long,String>
1360     implements DateFormatBuilder
1361   {
1362     private static final Long ZERO_KEY = 0L;
1363     private final String _am;
1364     private final String _pm;
1365 
1366     private AmPmDFB(String am, String pm) {
1367       _am = am;
1368       _pm = pm;
1369     }
1370     @Override
1371     public void build(DateTimeFormatterBuilder dtfb, Args args,
1372                       boolean hasAmPm, Value.Type dtType) {
1373       dtfb.appendText(ChronoField.AMPM_OF_DAY, this);
1374     }
1375     @Override
1376     public int size() {
1377       return 2;
1378     }
1379     @Override
1380     public String get(Object key) {
1381       return(ZERO_KEY.equals(key) ? _am : _pm);
1382     }
1383     @Override
1384     public Set<Map.Entry<Long,String>> entrySet() {
1385       return new AbstractSet<Map.Entry<Long,String>>() {
1386         @Override
1387         public int size() {
1388           return 2;
1389         }
1390         @Override
1391         public Iterator<Map.Entry<Long,String>> iterator() {
1392           return Arrays.<Map.Entry<Long,String>>asList(
1393               new AbstractMap.SimpleImmutableEntry<Long,String>(0L, _am),
1394               new AbstractMap.SimpleImmutableEntry<Long,String>(1L, _pm))
1395             .iterator();
1396         }
1397       };
1398     }
1399   }
1400 
1401   private static final class CustomFmt implements Fmt
1402   {
1403     private final Fmt _fmt;
1404     private final Fmt _emptyFmt;
1405 
1406     private CustomFmt(Fmt fmt) {
1407       this(fmt, NULL_FMT);
1408     }
1409 
1410     private CustomFmt(Fmt fmt, Fmt emptyFmt) {
1411       _fmt = fmt;
1412       _emptyFmt = emptyFmt;
1413     }
1414 
1415     @Override
1416     public Value format(Args args) {
1417       Fmt fmt = _fmt;
1418       if(args.maybeCoerceToEmptyString()) {
1419         fmt = _emptyFmt;
1420       }
1421       return fmt.format(args);
1422     }
1423   }
1424 
1425   private static final class CharSourceFmt implements Fmt
1426   {
1427     private final List<BiConsumer<StringBuilder,CharSource>> _subFmts;
1428     private final int _numPlaceholders;
1429     private final boolean _rightAligned;
1430     private final TextCase _textCase;
1431 
1432     private CharSourceFmt(List<BiConsumer<StringBuilder,CharSource>> subFmts,
1433                           int numPlaceholders, boolean rightAligned,
1434                           TextCase textCase) {
1435       _subFmts = subFmts;
1436       _numPlaceholders = numPlaceholders;
1437       _rightAligned = rightAligned;
1438       _textCase = textCase;
1439     }
1440 
1441     @Override
1442     public Value format(Args args) {
1443       CharSource cs = new CharSource(args.getAsString(), _numPlaceholders,
1444                                      _rightAligned, _textCase);
1445       StringBuilder sb = new StringBuilder();
1446       _subFmts.stream().forEach(fmt -> fmt.accept(sb, cs));
1447       cs.appendRemaining(sb);
1448       return ValueSupport.toValue(sb.toString());
1449     }
1450   }
1451 
1452   private static final class CharSource
1453   {
1454     private int _prefLen;
1455     private final String _str;
1456     private int _strPos;
1457     private final TextCase _textCase;
1458 
1459     private CharSource(String str, int len, boolean rightAligned,
1460                        TextCase textCase) {
1461       _str = str;
1462       _textCase = textCase;
1463       int strLen = str.length();
1464       if(len > strLen) {
1465         if(rightAligned) {
1466           _prefLen = len - strLen;
1467         }
1468       } else if(len < strLen) {
1469         // it doesn't make sense to me, but the meaning of "right aligned"
1470         // seems to flip when the string is longer than the format length
1471         if(!rightAligned) {
1472           _strPos = strLen - len;
1473         }
1474       }
1475     }
1476 
1477     public int next() {
1478       if(_prefLen > 0) {
1479         --_prefLen;
1480         return NO_CHAR;
1481       }
1482       if(_strPos < _str.length()) {
1483         return _textCase.apply(_str.charAt(_strPos++));
1484       }
1485       return NO_CHAR;
1486     }
1487 
1488     public void appendRemaining(StringBuilder sb) {
1489       int strLen = _str.length();
1490       while(_strPos < strLen) {
1491         sb.append(_textCase.apply(_str.charAt(_strPos++)));
1492       }
1493     }
1494   }
1495 
1496   private static abstract class BaseCustomNumberFmt implements Fmt
1497   {
1498     @Override
1499     public Value format(Args args) {
1500       if(args._expr.isNull()) {
1501         return formatNull(args);
1502       }
1503 
1504       BigDecimal bd = args.getAsBigDecimal();
1505       int cmp = BigDecimal.ZERO.compareTo(bd);
1506 
1507       return ((cmp < 0) ? formatPos(bd, args) :
1508               ((cmp > 0) ? formatNeg(bd, args) :
1509                formatZero(bd, args)));
1510     }
1511 
1512     protected abstract Value formatNull(Args args);
1513     protected abstract Value formatPos(BigDecimal bd, Args args);
1514     protected abstract Value formatNeg(BigDecimal bd, Args args);
1515     protected abstract Value formatZero(BigDecimal bd, Args args);
1516   }
1517 
1518   private static final class CustomGeneralFmt extends BaseCustomNumberFmt
1519   {
1520     private final Value _posVal;
1521     private final Value _negVal;
1522     private final Value _zeroVal;
1523     private final Value _nullVal;
1524 
1525     private CustomGeneralFmt(Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value posVal, Value negVal,
1526                              Value="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value zeroVal, Value nullVal) {
1527       _posVal = posVal;
1528       _negVal = negVal;
1529       _zeroVal = zeroVal;
1530       _nullVal = nullVal;
1531     }
1532 
1533     @Override
1534     protected Value formatNull(Args args) {
1535       return _nullVal;
1536     }
1537     @Override
1538     protected Value formatPos(BigDecimal bd, Args args) {
1539       return _posVal;
1540     }
1541     @Override
1542     protected Value formatNeg(BigDecimal bd, Args args) {
1543       return _negVal;
1544     }
1545     @Override
1546     protected Value formatZero(BigDecimal bd, Args args) {
1547       return _zeroVal;
1548     }
1549   }
1550 
1551   private static final class CustomNumberFmt extends BaseCustomNumberFmt
1552   {
1553     private final BDFormat _posFmt;
1554     private final BDFormat _negFmt;
1555     private final BDFormat _zeroFmt;
1556     private final BDFormat _nullFmt;
1557 
1558     private CustomNumberFmt(BDFormat posFmt, BDFormat negFmt,
1559                             BDFormat zeroFmt, BDFormat nullFmt) {
1560       _posFmt = posFmt;
1561       _negFmt = negFmt;
1562       _zeroFmt = zeroFmt;
1563       _nullFmt = nullFmt;
1564     }
1565 
1566     private Value formatMaybeZero(BigDecimal bd, BDFormat fmt) {
1567       // in theory we want to use the given format.  however, if, due to
1568       // rounding, we end up with a number equivalent to zero, then we fall
1569       // back to the zero format.  if we are using scientific notation,
1570       // however, then don't worry about this
1571       int maxDecDigits = fmt.getMaxDecimalDigits();
1572       if(maxDecDigits < bd.scale()) {
1573           bd = bd.setScale(maxDecDigits, NumberFormatter.ROUND_MODE);
1574           if(BigDecimal.ZERO.compareTo(bd) == 0) {
1575             // fall back to zero format
1576             fmt = _zeroFmt;
1577           }
1578       }
1579 
1580       return ValueSupport.toValue(fmt.format(bd));
1581     }
1582 
1583     @Override
1584     protected Value formatNull(Args args) {
1585         return ValueSupport.toValue(_nullFmt.format(BigDecimal.ZERO));
1586     }
1587     @Override
1588     protected Value formatPos(BigDecimal bd, Args args) {
1589       return formatMaybeZero(bd, _posFmt);
1590     }
1591     @Override
1592     protected Value formatNeg(BigDecimal bd, Args args) {
1593       return formatMaybeZero(bd.negate(), _negFmt);
1594     }
1595     @Override
1596     protected Value formatZero(BigDecimal bd, Args args) {
1597       return ValueSupport.toValue(_zeroFmt.format(bd));
1598     }
1599   }
1600 
1601   private static abstract class BDFormat
1602   {
1603     public int getMaxDecimalDigits() {
1604       return Integer.MAX_VALUE;
1605     }
1606 
1607     public abstract String format(BigDecimal bd);
1608   }
1609 
1610   private static final class LiteralBDFormat extends BDFormat
1611   {
1612     private final String _str;
1613 
1614     private LiteralBDFormat(String str) {
1615       _str = str;
1616     }
1617 
1618     @Override
1619     public String format(BigDecimal bd) {
1620       return _str;
1621     }
1622   }
1623 
1624   private static class BaseBDFormat extends BDFormat
1625   {
1626     private final NumberFormat _nf;
1627 
1628     private BaseBDFormat(NumberFormat nf) {
1629       _nf = nf;
1630     }
1631 
1632     @Override
1633     public String format(BigDecimal bd) {
1634       return _nf.format(bd);
1635     }
1636   }
1637 
1638   private static final class DecimalBDFormat extends BaseBDFormat
1639   {
1640     private final int _maxDecDigits;
1641 
1642     private DecimalBDFormat(DecimalFormat df) {
1643       super(df);
1644 
1645       int maxDecDigits = df.getMaximumFractionDigits();
1646       int mult = df.getMultiplier();
1647       while(mult > 1) {
1648         ++maxDecDigits;
1649         mult /= 10;
1650       }
1651       _maxDecDigits = maxDecDigits;
1652     }
1653 
1654     @Override
1655     public int getMaxDecimalDigits() {
1656       return _maxDecDigits;
1657     }
1658   }
1659 }