123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659 |
- /*
- Copyright (c) 2018 James Ahlborn
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
-
- package com.healthmarketscience.jackcess.impl.expr;
-
- import java.math.BigDecimal;
- import java.text.DecimalFormat;
- import java.text.NumberFormat;
- import java.time.LocalDateTime;
- import java.time.format.DateTimeFormatter;
- import java.time.format.DateTimeFormatterBuilder;
- import java.time.temporal.ChronoField;
- import java.time.temporal.TemporalField;
- import java.time.temporal.WeekFields;
- import java.util.AbstractMap;
- import java.util.AbstractSet;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.function.BiConsumer;
-
- import com.healthmarketscience.jackcess.expr.EvalContext;
- import com.healthmarketscience.jackcess.expr.EvalException;
- import com.healthmarketscience.jackcess.expr.NumericConfig;
- import com.healthmarketscience.jackcess.expr.TemporalConfig;
- import com.healthmarketscience.jackcess.expr.Value;
- import org.apache.commons.lang3.StringUtils;
- import static com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.ExprBuf;
-
- /**
- *
- * @author James Ahlborn
- */
- public class FormatUtil
- {
- public enum NumPatternType {
- GENERAL,
- CURRENCY {
- @Override
- protected void appendPrefix(StringBuilder fmt) {
- fmt.append('\u00A4');
- }
- @Override
- protected boolean useParensForNegatives(NumericConfig cfg) {
- return cfg.useParensForCurrencyNegatives();
- }
- },
- EURO {
- @Override
- protected void appendPrefix(StringBuilder fmt) {
- fmt.append('\u20AC');
- }
- @Override
- protected boolean useParensForNegatives(NumericConfig cfg) {
- return cfg.useParensForCurrencyNegatives();
- }
- },
- PERCENT {
- @Override
- protected void appendSuffix(StringBuilder fmt) {
- fmt.append('%');
- }
- },
- SCIENTIFIC {
- @Override
- protected void appendSuffix(StringBuilder fmt) {
- fmt.append("E0");
- }
- };
-
- protected void appendPrefix(StringBuilder fmt) {}
-
- protected void appendSuffix(StringBuilder fmt) {}
-
- protected boolean useParensForNegatives(NumericConfig cfg) {
- return cfg.useParensForNegatives();
- }
- }
-
- private enum TextCase {
- NONE,
- UPPER {
- @Override public char apply(char c) {
- return Character.toUpperCase(c);
- }
- },
- LOWER {
- @Override public char apply(char c) {
- return Character.toLowerCase(c);
- }
- };
-
- public char apply(char c) {
- return c;
- }
- }
-
- private static final Map<String,Fmt> PREDEF_FMTS = new HashMap<String,Fmt>();
-
- static {
- putPredefFormat("General Date", args -> ValueSupport.toValue(
- args.coerceToDateTimeValue().getAsString()));
- putPredefFormat("Long Date",
- new PredefDateFmt(TemporalConfig.Type.LONG_DATE));
- putPredefFormat("Medium Date",
- new PredefDateFmt(TemporalConfig.Type.MEDIUM_DATE));
- putPredefFormat("Short Date",
- new PredefDateFmt(TemporalConfig.Type.SHORT_DATE));
- putPredefFormat("Long Time",
- new PredefDateFmt(TemporalConfig.Type.LONG_TIME));
- putPredefFormat("Medium Time",
- new PredefDateFmt(TemporalConfig.Type.MEDIUM_TIME));
- putPredefFormat("Short Time",
- new PredefDateFmt(TemporalConfig.Type.SHORT_TIME));
-
- putPredefFormat("General Number", args -> ValueSupport.toValue(
- args.coerceToNumberValue().getAsString()));
- putPredefFormat("Currency",
- new PredefNumberFmt(NumericConfig.Type.CURRENCY));
- putPredefFormat("Euro", new PredefNumberFmt(NumericConfig.Type.EURO));
- putPredefFormat("Fixed",
- new PredefNumberFmt(NumericConfig.Type.FIXED));
- putPredefFormat("Standard",
- new PredefNumberFmt(NumericConfig.Type.STANDARD));
- putPredefFormat("Percent",
- new PredefNumberFmt(NumericConfig.Type.PERCENT));
- putPredefFormat("Scientific", new ScientificPredefNumberFmt());
-
- putPredefFormat("True/False", new PredefBoolFmt("True", "False"));
- putPredefFormat("Yes/No", new PredefBoolFmt("Yes", "No"));
- putPredefFormat("On/Off", new PredefBoolFmt("On", "Off"));
- }
-
- private static final Fmt NULL_FMT = args -> ValueSupport.EMPTY_STR_VAL;
- private static final Fmt DUMMY_FMT = args -> args.getNonNullExpr();
-
- private static final char QUOTE_CHAR = '"';
- private static final char ESCAPE_CHAR = '\\';
- private static final char LEFT_ALIGN_CHAR = '!';
- private static final char START_COLOR_CHAR = '[';
- private static final char END_COLOR_CHAR = ']';
- private static final char CHOICE_SEP_CHAR = ';';
- // this only seems to be useful if you have fixed length string fields which
- // isn't a normal thing in ms access
- private static final char FILL_ESCAPE_CHAR = '*';
- private static final char REQ_PLACEHOLDER_CHAR = '@';
- private static final char OPT_PLACEHOLDER_CHAR = '&';
- private static final char TO_UPPER_CHAR = '>';
- private static final char TO_LOWER_CHAR = '<';
- private static final char DT_LIT_COLON_CHAR = ':';
- private static final char DT_LIT_SLASH_CHAR = '/';
- private static final char SINGLE_QUOTE_CHAR = '\'';
- private static final char EXP_E_CHAR = 'E';
- private static final char EXP_e_CHAR = 'e';
- private static final char PLUS_CHAR = '+';
- private static final char MINUS_CHAR = '-';
- private static final char REQ_DIGIT_CHAR = '0';
- private static final int NO_CHAR = -1;
-
- private static final byte FCT_UNKNOWN = 0;
- private static final byte FCT_LITERAL = 1;
- private static final byte FCT_GENERAL = 2;
- private static final byte FCT_DATE = 3;
- private static final byte FCT_NUMBER = 4;
- private static final byte FCT_TEXT = 5;
-
- private static final byte[] FORMAT_CODE_TYPES = new byte[127];
- static {
- setFormatCodeTypes(" $+-()", FCT_LITERAL);
- setFormatCodeTypes("\"!*\\[];", FCT_GENERAL);
- setFormatCodeTypes(":/cdwmqyhnstampmAMPM", FCT_DATE);
- setFormatCodeTypes(".,0#%Ee", FCT_NUMBER);
- setFormatCodeTypes("@&<>", FCT_TEXT);
- }
-
- @FunctionalInterface
- interface Fmt {
- public Value format(Args args);
- }
-
- @FunctionalInterface
- interface DateFormatBuilder {
- public void build(DateTimeFormatterBuilder dtfb, Args args,
- boolean hasAmPm, Value.Type dtType);
- }
-
- private static final DateFormatBuilder PARTIAL_PREFIX =
- (dtfb, args, hasAmPm, dtType) -> {
- throw new UnsupportedOperationException();
- };
-
- private static final Map<String,DateFormatBuilder> DATE_FMT_BUILDERS =
- new HashMap<>();
- static {
- DATE_FMT_BUILDERS.put("c",
- (dtfb, args, hasAmPm, dtType) ->
- dtfb.append(ValueSupport.getDateFormatForType(
- args._ctx, dtType)));
- DATE_FMT_BUILDERS.put("d", new SimpleDFB("d"));
- DATE_FMT_BUILDERS.put("dd", new SimpleDFB("dd"));
- DATE_FMT_BUILDERS.put("ddd", new SimpleDFB("eee"));
- DATE_FMT_BUILDERS.put("dddd", new SimpleDFB("eeee"));
- DATE_FMT_BUILDERS.put("ddddd", new PredefDFB(TemporalConfig.Type.SHORT_DATE));
- DATE_FMT_BUILDERS.put("dddddd", new PredefDFB(TemporalConfig.Type.LONG_DATE));
- DATE_FMT_BUILDERS.put("w", new WeekBasedDFB() {
- @Override
- protected TemporalField getField(WeekFields weekFields) {
- return weekFields.dayOfWeek();
- }
- });
- DATE_FMT_BUILDERS.put("ww", new WeekBasedDFB() {
- @Override
- protected TemporalField getField(WeekFields weekFields) {
- return weekFields.weekOfWeekBasedYear();
- }
- });
- DATE_FMT_BUILDERS.put("m", new SimpleDFB("L"));
- DATE_FMT_BUILDERS.put("mm", new SimpleDFB("LL"));
- DATE_FMT_BUILDERS.put("mmm", new SimpleDFB("MMM"));
- DATE_FMT_BUILDERS.put("mmmm", new SimpleDFB("MMMM"));
- DATE_FMT_BUILDERS.put("q", new SimpleDFB("Q"));
- DATE_FMT_BUILDERS.put("y", new SimpleDFB("D"));
- DATE_FMT_BUILDERS.put("yy", new SimpleDFB("yy"));
- DATE_FMT_BUILDERS.put("yyyy", new SimpleDFB("yyyy"));
- DATE_FMT_BUILDERS.put("h", new HourlyDFB("h", "H"));
- DATE_FMT_BUILDERS.put("hh", new HourlyDFB("hh", "HH"));
- DATE_FMT_BUILDERS.put("n", new SimpleDFB("m"));
- DATE_FMT_BUILDERS.put("nn", new SimpleDFB("mm"));
- DATE_FMT_BUILDERS.put("s", new SimpleDFB("s"));
- DATE_FMT_BUILDERS.put("ss", new SimpleDFB("ss"));
- DATE_FMT_BUILDERS.put("ttttt", new PredefDFB(TemporalConfig.Type.LONG_TIME));
- DATE_FMT_BUILDERS.put("AM/PM", new AmPmDFB("AM", "PM"));
- DATE_FMT_BUILDERS.put("am/pm", new AmPmDFB("am", "pm"));
- DATE_FMT_BUILDERS.put("A/P", new AmPmDFB("A", "P"));
- DATE_FMT_BUILDERS.put("a/p", new AmPmDFB("a", "p"));
- DATE_FMT_BUILDERS.put("AMPM",
- (dtfb, args, hasAmPm, dtType) -> {
- String[] amPmStrs = args._ctx.getTemporalConfig().getAmPmStrings();
- new AmPmDFB(amPmStrs[0], amPmStrs[1]).build(dtfb, args, hasAmPm, dtType);
- }
- );
- fillInPartialPrefixes();
- }
-
- private static final int NF_POS_IDX = 0;
- private static final int NF_NEG_IDX = 1;
- private static final int NF_ZERO_IDX = 2;
- private static final int NF_NULL_IDX = 3;
- private static final int NUM_NF_FMTS = 4;
-
- private static final NumberFormatter.NotationType[] NO_EXP_TYPES =
- new NumberFormatter.NotationType[NUM_NF_FMTS];
- private static final boolean[] NO_FMT_TYPES = new boolean[NUM_NF_FMTS];
-
-
- private static final class Args
- {
- private final EvalContext _ctx;
- private Value _expr;
- private final int _firstDay;
- private final int _firstWeekType;
-
- private Args(EvalContext ctx, Value expr, int firstDay, int firstWeekType) {
- _ctx = ctx;
- _expr = expr;
- _firstDay = firstDay;
- _firstWeekType = firstWeekType;
- }
-
- public Args setExpr(Value expr) {
- _expr = expr;
- return this;
- }
-
- public Value getNonNullExpr() {
- return (_expr.isNull() ? ValueSupport.EMPTY_STR_VAL : _expr);
- }
-
- public boolean isNullOrEmptyString() {
- return(_expr.isNull() ||
- // only a string value could ever be an empty string
- (_expr.getType().isString() && getAsString().isEmpty()));
- }
-
- public boolean maybeCoerceToEmptyString() {
- if(isNullOrEmptyString()) {
- // ensure that we have a non-null value when formatting (null acts
- // like empty string)
- _expr = ValueSupport.EMPTY_STR_VAL;
- return true;
- }
- return false;
- }
-
- public Args coerceToDateTimeValue() {
- if(!_expr.getType().isTemporal()) {
-
- // format coerces boolean strings to numbers
- Value boolExpr = null;
- if(_expr.getType().isString() &&
- ((boolExpr = maybeGetStringAsBooleanValue()) != null)) {
- _expr = boolExpr;
- }
-
- // StringValue already handles most String -> Number -> Date/Time, so
- // most other convertions work here (and failures are thrown so that
- // default handling kicks in)
- _expr = _expr.getAsDateTimeValue(_ctx);
- }
- return this;
- }
-
- public Args coerceToNumberValue() {
- if(!_expr.getType().isNumeric()) {
- if(_expr.getType().isString()) {
-
- // format coerces "true"/"false" to boolean values
- Value boolExpr = maybeGetStringAsBooleanValue();
- if(boolExpr != null) {
- _expr = boolExpr;
- } else {
- BigDecimal bd = DefaultFunctions.maybeGetAsBigDecimal(_ctx, _expr);
- if(bd != null) {
- _expr = ValueSupport.toValue(bd);
- } else {
- // convert to date to number. this doesn't happen as part of the
- // default value coercion behavior, but the format method tries
- // harder
- Value maybe = DefaultFunctions.maybeGetAsDateTimeValue(
- _ctx, _expr);
- if(maybe != null) {
- _expr = ValueSupport.toValue(maybe.getAsDouble(_ctx));
- } else {
- // string which can't be converted to number force failure
- // here so default formatting will kick in
- throw new EvalException("invalid number value");
- }
- }
- }
- } else {
- // convert date to number
- _expr = ValueSupport.toValue(_expr.getAsDouble(_ctx));
- }
- }
- return this;
- }
-
- private Value maybeGetStringAsBooleanValue() {
- // format coerces "true"/"false" to boolean values
- String val = getAsString();
- if("true".equalsIgnoreCase(val)) {
- return ValueSupport.TRUE_VAL;
- }
- if("false".equalsIgnoreCase(val)) {
- return ValueSupport.FALSE_VAL;
- }
- return null;
- }
-
- public BigDecimal getAsBigDecimal() {
- coerceToNumberValue();
- return _expr.getAsBigDecimal(_ctx);
- }
-
- public LocalDateTime getAsLocalDateTime() {
- coerceToDateTimeValue();
- return _expr.getAsLocalDateTime(_ctx);
- }
-
- public boolean getAsBoolean() {
- // even though string values have a "boolean" value, for formatting,
- // strings which don't convert to valid boolean/number/date are just
- // returned as is. so we use coerceToNumberValue to force the exception
- // to be thrown which results in the "default" formatting behavior.
- coerceToNumberValue();
- return _expr.getAsBoolean(_ctx);
- }
-
- public String getAsString() {
- return _expr.getAsString(_ctx);
- }
-
- public Value format(Fmt fmt) {
- Value origExpr = _expr;
- try {
- return fmt.format(this);
- } catch(EvalException ee) {
- // values which cannot be formatted as the target type are just
- // returned "as is"
- return origExpr;
- }
- }
- }
-
- private FormatUtil() {}
-
- /**
- * Utility for leveraging format support outside of expression evaluation.
- */
- public static class StandaloneFormatter
- {
- private final Fmt _fmt;
- private final Args _args;
-
- private StandaloneFormatter(Fmt fmt, Args args) {
- _fmt = fmt;
- _args = args;
- }
-
- public Value format(Value expr) {
- return _args.setExpr(expr).format(_fmt);
- }
- }
-
- public static Value format(EvalContext ctx, Value expr, String fmtStr,
- int firstDay, int firstWeekType) {
- Args args = new Args(ctx, expr, firstDay, firstWeekType);
- return args.format(createFormat(args, fmtStr));
- }
-
- public static StandaloneFormatter createStandaloneFormatter(
- EvalContext ctx, String fmtStr, int firstDay, int firstWeekType) {
- Args args = new Args(ctx, null, firstDay, firstWeekType);
- Fmt fmt = createFormat(args, fmtStr);
- return new StandaloneFormatter(fmt, args);
- }
-
- private static Fmt createFormat(Args args, String fmtStr) {
- Fmt predefFmt = PREDEF_FMTS.get(fmtStr);
- if(predefFmt != null) {
- return predefFmt;
- }
-
- if(StringUtils.isEmpty(fmtStr)) {
- return DUMMY_FMT;
- }
-
- // TODO implement caching for custom formats? put into Bindings. use
- // special "cache" prefix to know which caches to clear when evalconfig
- // is altered (could also cache other Format* functions)
-
- return parseCustomFormat(fmtStr, args);
- }
-
- private static Fmt parseCustomFormat(String fmtStr, Args args) {
-
- ExprBuf buf = new ExprBuf(fmtStr, null);
-
- // do partial pass to determine what type of format this is
- byte curFormatType = determineFormatType(buf);
-
- // reset buffer for real parse
- buf.reset(0);
-
- switch(curFormatType) {
- case FCT_GENERAL:
- return parseCustomGeneralFormat(buf);
- case FCT_DATE:
- return parseCustomDateFormat(buf, args);
- case FCT_NUMBER:
- return parseCustomNumberFormat(buf, args);
- case FCT_TEXT:
- return parseCustomTextFormat(buf);
- default:
- throw new EvalException("Invalid format type " + curFormatType);
- }
- }
-
- private static byte determineFormatType(ExprBuf buf) {
-
- while(buf.hasNext()) {
- char c = buf.next();
- byte fmtType = getFormatCodeType(c);
- switch(fmtType) {
- case FCT_UNKNOWN:
- case FCT_LITERAL:
- // meaningless, ignore for now
- break;
- case FCT_GENERAL:
- switch(c) {
- case QUOTE_CHAR:
- parseQuotedString(buf);
- break;
- case START_COLOR_CHAR:
- parseColorString(buf);
- break;
- case ESCAPE_CHAR:
- case FILL_ESCAPE_CHAR:
- if(buf.hasNext()) {
- buf.next();
- }
- break;
- default:
- // meaningless, ignore for now
- }
- break;
- case FCT_DATE:
- case FCT_NUMBER:
- case FCT_TEXT:
- // found specific type
- return fmtType;
- default:
- throw new EvalException("Invalid format type " + fmtType);
- }
- }
-
- // no specific type
- return FCT_GENERAL;
- }
-
- private static Fmt parseCustomGeneralFormat(ExprBuf buf) {
-
- // a "general" format is actually a "yes/no" format which functions almost
- // exactly like a number format (without any number format specific chars)
-
- StringBuilder sb = new StringBuilder();
- String[] fmtStrs = new String[NUM_NF_FMTS];
- int fmtIdx = 0;
-
- BUF_LOOP:
- while(buf.hasNext()) {
- char c = buf.next();
- int fmtType = getFormatCodeType(c);
- switch(fmtType) {
- case FCT_GENERAL:
- switch(c) {
- case LEFT_ALIGN_CHAR:
- // no effect
- break;
- case QUOTE_CHAR:
- parseQuotedString(buf, sb);
- break;
- case START_COLOR_CHAR:
- // color strings seem to be ignored
- parseColorString(buf);
- break;
- case ESCAPE_CHAR:
- if(buf.hasNext()) {
- sb.append(buf.next());
- }
- break;
- case FILL_ESCAPE_CHAR:
- // unclear what this actually does. online examples don't seem to
- // match with experimental results. for now, ignore
- if(buf.hasNext()) {
- buf.next();
- }
- break;
- case CHOICE_SEP_CHAR:
- // yes/no (number) format supports up to 4 formats: pos, neg, zero,
- // null. after that, ignore the rest
- if(fmtIdx == (NUM_NF_FMTS - 1)) {
- // last possible format, ignore remaining
- break BUF_LOOP;
- }
- addCustomGeneralFormat(fmtStrs, fmtIdx++, sb);
- break;
- default:
- sb.append(c);
- }
- break;
- default:
- sb.append(c);
- }
- }
-
- // fill in remaining formats
- while(fmtIdx < NUM_NF_FMTS) {
- addCustomGeneralFormat(fmtStrs, fmtIdx++, sb);
- }
-
- return new CustomGeneralFmt(
- ValueSupport.toValue(fmtStrs[NF_POS_IDX]),
- ValueSupport.toValue(fmtStrs[NF_NEG_IDX]),
- ValueSupport.toValue(fmtStrs[NF_ZERO_IDX]),
- ValueSupport.toValue(fmtStrs[NF_NULL_IDX]));
- }
-
- private static void addCustomGeneralFormat(String[] fmtStrs, int fmtIdx,
- StringBuilder sb)
- {
- addCustomNumberFormat(fmtStrs, NO_EXP_TYPES, NO_FMT_TYPES, NO_FMT_TYPES,
- fmtIdx, sb);
- }
-
- private static Fmt parseCustomDateFormat(ExprBuf buf, Args args) {
-
- // keep track of some extra state while parsing the format, whether or not
- // there was an am/pm pattern and whether or not there was a general
- // date/time pattern
- boolean[] fmtState = new boolean[]{false, false};
- List<DateFormatBuilder> dfbs = new ArrayList<>();
-
- BUF_LOOP:
- while(buf.hasNext()) {
- char c = buf.next();
- int fmtType = getFormatCodeType(c);
- switch(fmtType) {
- case FCT_GENERAL:
- switch(c) {
- case QUOTE_CHAR:
- String str = parseQuotedString(buf);
- dfbs.add((dtfb, argsParam, hasAmPmParam, dtType) ->
- dtfb.appendLiteral(str));
- break;
- case START_COLOR_CHAR:
- // color strings seem to be ignored
- parseColorString(buf);
- break;
- case ESCAPE_CHAR:
- if(buf.hasNext()) {
- dfbs.add(buildLiteralCharDFB(buf.next()));
- }
- break;
- case FILL_ESCAPE_CHAR:
- // unclear what this actually does. online examples don't seem to
- // match with experimental results. for now, ignore
- if(buf.hasNext()) {
- buf.next();
- }
- break;
- case CHOICE_SEP_CHAR:
- // date/time doesn't use multiple pattern choices, but it does
- // respect the char. ignore everything after the first choice
- break BUF_LOOP;
- default:
- dfbs.add(buildLiteralCharDFB(c));
- }
- break;
- case FCT_DATE:
- parseCustomDateFormatPattern(c, buf, dfbs, fmtState);
- break;
- default:
- dfbs.add(buildLiteralCharDFB(c));
- }
- }
-
- boolean hasAmPm = fmtState[0];
- boolean hasGeneralFormat = fmtState[1];
- if(!hasGeneralFormat) {
- // simple situation, one format for every value
- DateTimeFormatter dtf = createDateTimeFormatter(dfbs, args, hasAmPm, null);
- return new CustomFmt(argsParam -> ValueSupport.toValue(
- dtf.format(argsParam.getAsLocalDateTime())));
- }
-
- // we need separate formatters for date, time, and date/time values
- DateTimeFormatter dateFmt = createDateTimeFormatter(dfbs, args, hasAmPm,
- Value.Type.DATE);
- DateTimeFormatter timeFmt = createDateTimeFormatter(dfbs, args, hasAmPm,
- Value.Type.TIME);
- DateTimeFormatter dtFmt = createDateTimeFormatter(dfbs, args, hasAmPm,
- Value.Type.DATE_TIME);
-
- return new CustomFmt(argsParam -> formatDateTime(
- argsParam, dateFmt, timeFmt, dtFmt));
- }
-
- private static void parseCustomDateFormatPattern(
- char c, ExprBuf buf, List<DateFormatBuilder> dfbs,
- boolean[] fmtState) {
-
- if((c == DT_LIT_COLON_CHAR) || (c == DT_LIT_SLASH_CHAR)) {
- // date/time literal char, nothing more to do
- dfbs.add(buildLiteralCharDFB(c));
- return;
- }
-
- StringBuilder sb = buf.getScratchBuffer();
- sb.append(c);
-
- char firstChar = c;
- int firstPos = buf.curPos();
- String bestMatchPat = sb.toString();
-
- DateFormatBuilder bestMatch = DATE_FMT_BUILDERS.get(bestMatchPat);
- int bestPos = firstPos;
- while(buf.hasNext()) {
- sb.append(buf.next());
- String tmpPat = sb.toString();
- DateFormatBuilder dfb = DATE_FMT_BUILDERS.get(tmpPat);
- if(dfb == null) {
- // no more possible matches
- break;
- }
- if(dfb != PARTIAL_PREFIX) {
- // this is the longest, valid pattern we have seen so far
- bestMatch = dfb;
- bestPos = buf.curPos();
- bestMatchPat = tmpPat;
- }
- }
-
- if(bestMatch != PARTIAL_PREFIX) {
-
- // apply valid pattern
- buf.reset(bestPos);
- dfbs.add(bestMatch);
-
- switch(firstChar) {
- case 'a':
- case 'A':
- // this was an am/pm pattern
- fmtState[0] = true;
- break;
- case 'c':
- // this was a general date/time format
- fmtState[1] = true;
- break;
- default:
- // don't care
- }
-
- } else {
-
- // just consume the first char
- buf.reset(firstPos);
- dfbs.add(buildLiteralCharDFB(firstChar));
- }
- }
-
- private static DateFormatBuilder buildLiteralCharDFB(char c) {
- return (dtfb, args, hasAmPm, dtType) -> dtfb.appendLiteral(c);
- }
-
- private static DateTimeFormatter createDateTimeFormatter(
- List<DateFormatBuilder> dfbs, Args args, boolean hasAmPm,
- Value.Type dtType)
- {
- DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder();
- dfbs.forEach(d -> d.build(dtfb, args, hasAmPm, dtType));
- return dtfb.toFormatter(args._ctx.getTemporalConfig().getLocale());
- }
-
- private static Value formatDateTime(
- Args args, DateTimeFormatter dateFmt,
- DateTimeFormatter timeFmt, DateTimeFormatter dtFmt)
- {
- LocalDateTime ldt = args.getAsLocalDateTime();
- DateTimeFormatter fmt = null;
- switch(args._expr.getType()) {
- case DATE:
- fmt = dateFmt;
- break;
- case TIME:
- fmt = timeFmt;
- break;
- default:
- fmt = dtFmt;
- }
-
- return ValueSupport.toValue(fmt.format(ldt));
- }
-
- private static Fmt parseCustomNumberFormat(ExprBuf buf, Args args) {
-
- StringBuilder sb = new StringBuilder();
- String[] fmtStrs = new String[NUM_NF_FMTS];
- int fmtIdx = 0;
- StringBuilder pendingLiteral = new StringBuilder();
- NumberFormatter.NotationType[] expTypes =
- new NumberFormatter.NotationType[NUM_NF_FMTS];
- boolean[] hasFmts = new boolean[NUM_NF_FMTS];
- boolean[] hasReqDigit = new boolean[NUM_NF_FMTS];
-
- BUF_LOOP:
- while(buf.hasNext()) {
- char c = buf.next();
- int fmtType = getFormatCodeType(c);
- switch(fmtType) {
- case FCT_GENERAL:
- switch(c) {
- case LEFT_ALIGN_CHAR:
- // no effect
- break;
- case QUOTE_CHAR:
- parseQuotedString(buf, pendingLiteral);
- break;
- case START_COLOR_CHAR:
- // color strings seem to be ignored
- parseColorString(buf);
- break;
- case ESCAPE_CHAR:
- if(buf.hasNext()) {
- pendingLiteral.append(buf.next());
- }
- break;
- case FILL_ESCAPE_CHAR:
- // unclear what this actually does. online examples don't seem to
- // match with experimental results. for now, ignore
- if(buf.hasNext()) {
- buf.next();
- }
- break;
- case CHOICE_SEP_CHAR:
- // number format supports up to 4 formats: pos, neg, zero, null.
- // after that, ignore the rest
- if(fmtIdx == (NUM_NF_FMTS - 1)) {
- // last possible format, ignore remaining
- break BUF_LOOP;
- }
- flushPendingNumberLiteral(pendingLiteral, sb);
- addCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit,
- fmtIdx++, sb);
- break;
- default:
- pendingLiteral.append(c);
- }
- break;
- case FCT_NUMBER:
- hasFmts[fmtIdx] = true;
- switch(c) {
- case EXP_E_CHAR:
- int signChar = buf.peekNext();
- if((signChar == PLUS_CHAR) || (signChar == MINUS_CHAR)) {
- buf.next();
- expTypes[fmtIdx] = ((signChar == PLUS_CHAR) ?
- NumberFormatter.NotationType.EXP_E_PLUS :
- NumberFormatter.NotationType.EXP_E_MINUS);
- flushPendingNumberLiteral(pendingLiteral, sb);
- sb.append(EXP_E_CHAR);
- } else {
- pendingLiteral.append(c);
- }
- break;
- case EXP_e_CHAR:
- signChar = buf.peekNext();
- if((signChar == PLUS_CHAR) || (signChar == MINUS_CHAR)) {
- buf.next();
- expTypes[fmtIdx] = ((signChar == PLUS_CHAR) ?
- NumberFormatter.NotationType.EXP_e_PLUS :
- NumberFormatter.NotationType.EXP_e_MINUS);
- flushPendingNumberLiteral(pendingLiteral, sb);
- sb.append(EXP_E_CHAR);
- } else {
- pendingLiteral.append(c);
- }
- break;
- case REQ_DIGIT_CHAR:
- hasReqDigit[fmtIdx] = true;
- flushPendingNumberLiteral(pendingLiteral, sb);
- sb.append(c);
- break;
- default:
- // most number format chars pass straight through
- flushPendingNumberLiteral(pendingLiteral, sb);
- sb.append(c);
- }
- break;
- default:
- pendingLiteral.append(c);
- }
- }
-
- // fill in remaining formats
- while(fmtIdx < NUM_NF_FMTS) {
- flushPendingNumberLiteral(pendingLiteral, sb);
- addCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit,
- fmtIdx++, sb);
- }
-
- return new CustomNumberFmt(
- createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit,
- NF_POS_IDX, false, args, buf),
- createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit,
- NF_NEG_IDX, false, args, buf),
- createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit,
- NF_ZERO_IDX, true, args, buf),
- createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit,
- NF_NULL_IDX, true, args, buf));
- }
-
- private static void addCustomNumberFormat(
- String[] fmtStrs, NumberFormatter.NotationType[] expTypes,
- boolean[] hasFmts, boolean[] hasReqDigit, int fmtIdx, StringBuilder sb)
- {
- if(sb.length() == 0) {
- // do special empty format handling on a per-format-type basis
- switch(fmtIdx) {
- case NF_NEG_IDX:
- // re-use "pos" format
- sb.append('-').append(fmtStrs[NF_POS_IDX]);
- expTypes[NF_NEG_IDX] = expTypes[NF_POS_IDX];
- hasFmts[NF_NEG_IDX] = hasFmts[NF_POS_IDX];
- hasReqDigit[NF_NEG_IDX] = hasReqDigit[NF_POS_IDX];
- break;
- case NF_ZERO_IDX:
- // re-use "pos" format
- sb.append(fmtStrs[NF_POS_IDX]);
- expTypes[NF_ZERO_IDX] = expTypes[NF_POS_IDX];
- hasFmts[NF_ZERO_IDX] = hasFmts[NF_POS_IDX];
- hasReqDigit[NF_ZERO_IDX] = hasReqDigit[NF_POS_IDX];
- break;
- default:
- // use empty string result
- }
- }
-
- fmtStrs[fmtIdx] = sb.toString();
- sb.setLength(0);
- }
-
- private static void flushPendingNumberLiteral(
- StringBuilder pendingLiteral, StringBuilder sb) {
- if(pendingLiteral.length() == 0) {
- return;
- }
-
- if((pendingLiteral.length() == 1) &&
- pendingLiteral.charAt(0) == SINGLE_QUOTE_CHAR) {
- // handle standalone single quote
- sb.append(SINGLE_QUOTE_CHAR).append(SINGLE_QUOTE_CHAR);
- pendingLiteral.setLength(0);
- return;
- }
-
- sb.append(SINGLE_QUOTE_CHAR);
- int startPos = sb.length();
- sb.append(pendingLiteral);
-
- // we need to quote any single quotes in the literal string
- for(int i = startPos; i < sb.length(); ++i) {
- char c = sb.charAt(i);
- if(c == SINGLE_QUOTE_CHAR) {
- sb.insert(++i, SINGLE_QUOTE_CHAR);
- }
- }
-
- sb.append(SINGLE_QUOTE_CHAR);
- pendingLiteral.setLength(0);
- }
-
- private static BDFormat createCustomNumberFormat(
- String[] fmtStrs, NumberFormatter.NotationType[] expTypes,
- boolean[] hasFmts, boolean[] hasReqDigit, int fmtIdx,
- boolean isZeroFmt, Args args, ExprBuf buf) {
-
- String fmtStr = fmtStrs[fmtIdx];
- if(!hasFmts[fmtIdx]) {
- // convert the literal string to a dummy number format
- if(fmtStr.length() > 0) {
- // strip quoting
- StringBuilder sb = buf.getScratchBuffer().append(fmtStr)
- .deleteCharAt(fmtStr.length() - 1)
- .deleteCharAt(0);
- if(sb.length() > 0) {
- for(int i = 0; i < sb.length(); ++i) {
- if(sb.charAt(i) == SINGLE_QUOTE_CHAR) {
- // delete next single quote char
- sb.deleteCharAt(++i);
- }
- }
- } else {
- // this was a single, literal single quote
- sb.append(SINGLE_QUOTE_CHAR);
- }
- fmtStr = sb.toString();
- }
- return new LiteralBDFormat(fmtStr);
- }
-
- NumberFormatter.NotationType expType = expTypes[fmtIdx];
- DecimalFormat df = args._ctx.createDecimalFormat(fmtStr);
-
- if(df.getMaximumFractionDigits() > 0) {
- // if the decimal is included in the format, access always shows it
- df.setDecimalSeparatorAlwaysShown(true);
- }
-
- if(expType != null) {
- NumberFormat nf = new NumberFormatter.ScientificFormat(df, expType);
- if(isZeroFmt) {
- return new LiteralBDFormat(nf.format(BigDecimal.ZERO));
- }
- return new BaseBDFormat(nf);
- }
-
- if(!hasReqDigit[fmtIdx]) {
- // java likes to force extra 0's while access doesn't
- df.setMinimumIntegerDigits(0);
- }
-
- if(isZeroFmt) {
-
- String zeroVal = df.format(BigDecimal.ZERO);
- if(!hasReqDigit[fmtIdx]) {
- // java forces a 0 but access doesn't. delete any 0 chars which were
- // inserted by the java format
- int prefLen = df.getPositivePrefix().length();
- int len = zeroVal.length() - df.getPositiveSuffix().length();
- StringBuilder sb = buf.getScratchBuffer().append(zeroVal);
- for(int i = len - 1; i >= prefLen; --i) {
- if(sb.charAt(i) == '0') {
- sb.deleteCharAt(i);
- }
- }
- zeroVal = sb.toString();
- }
-
- return new LiteralBDFormat(zeroVal);
- }
-
- return new DecimalBDFormat(df);
- }
-
- private static Fmt parseCustomTextFormat(ExprBuf buf) {
-
- Fmt fmt = null;
-
- List<BiConsumer<StringBuilder,CharSource>> subFmts = new ArrayList<>();
- int numPlaceholders = 0;
- boolean rightAligned = true;
- TextCase textCase = TextCase.NONE;
- StringBuilder pendingLiteral = new StringBuilder();
- boolean hasFmtChars = false;
-
- BUF_LOOP:
- while(buf.hasNext()) {
- char c = buf.next();
- hasFmtChars = true;
- int fmtType = getFormatCodeType(c);
- switch(fmtType) {
- case FCT_GENERAL:
- switch(c) {
- case LEFT_ALIGN_CHAR:
- rightAligned = false;
- break;
- case QUOTE_CHAR:
- parseQuotedString(buf, pendingLiteral);
- break;
- case START_COLOR_CHAR:
- // color strings seem to be ignored
- parseColorString(buf);
- break;
- case ESCAPE_CHAR:
- if(buf.hasNext()) {
- pendingLiteral.append(buf.next());
- }
- break;
- case FILL_ESCAPE_CHAR:
- // unclear what this actually does. online examples don't seem to
- // match with experimental results. for now, ignore
- if(buf.hasNext()) {
- buf.next();
- }
- break;
- case CHOICE_SEP_CHAR:
- // text format supports up to 2 formats: normal and empty/null.
- // after that, ignore the rest
- if(fmt != null) {
- // ignore remaining format
- break BUF_LOOP;
- }
- flushPendingTextLiteral(pendingLiteral, subFmts);
- fmt = new CharSourceFmt(subFmts, numPlaceholders, rightAligned,
- textCase);
- // reset for next format
- subFmts = new ArrayList<>();
- numPlaceholders = 0;
- rightAligned = true;
- textCase = TextCase.NONE;
- hasFmtChars = false;
- break;
- default:
- pendingLiteral.append(c);
- }
- break;
- case FCT_TEXT:
- switch(c) {
- case REQ_PLACEHOLDER_CHAR:
- flushPendingTextLiteral(pendingLiteral, subFmts);
- ++numPlaceholders;
- subFmts.add((sb,cs) -> {
- int tmp = cs.next();
- sb.append((tmp != NO_CHAR) ? (char)tmp : ' ');
- });
- break;
- case OPT_PLACEHOLDER_CHAR:
- flushPendingTextLiteral(pendingLiteral, subFmts);
- ++numPlaceholders;
- subFmts.add((sb,cs) -> {
- int tmp = cs.next();
- if(tmp != NO_CHAR) {
- sb.append((char)tmp);
- }
- });
- break;
- case TO_UPPER_CHAR:
- // an uppper and lower symbol cancel each other out
- textCase = ((textCase == TextCase.LOWER) ?
- TextCase.NONE : TextCase.UPPER);
- break;
- case TO_LOWER_CHAR:
- // an uppper and lower symbol cancel each other out
- textCase = ((textCase == TextCase.UPPER) ?
- TextCase.NONE : TextCase.LOWER);
- break;
- default:
- pendingLiteral.append(c);
- }
- break;
- default:
- pendingLiteral.append(c);
- }
- }
-
- flushPendingTextLiteral(pendingLiteral, subFmts);
-
- Fmt emptyFmt = null;
- if(fmt == null) {
- fmt = new CharSourceFmt(subFmts, numPlaceholders, rightAligned,
- textCase);
- emptyFmt = NULL_FMT;
- } else {
- emptyFmt = (hasFmtChars ?
- new CharSourceFmt(subFmts, numPlaceholders, rightAligned,
- textCase) :
- NULL_FMT);
- }
-
- return new CustomFmt(fmt, emptyFmt);
- }
-
- private static void flushPendingTextLiteral(
- StringBuilder pendingLiteral,
- List<BiConsumer<StringBuilder,CharSource>> subFmts) {
- if(pendingLiteral.length() == 0) {
- return;
- }
-
- String literal = pendingLiteral.toString();
- pendingLiteral.setLength(0);
- subFmts.add((sb, cs) -> sb.append(literal));
- }
-
- public static String createNumberFormatPattern(
- NumPatternType numPatType, int numDecDigits, boolean incLeadDigit,
- boolean negParens, int numGroupDigits) {
-
- StringBuilder fmt = new StringBuilder();
-
- numPatType.appendPrefix(fmt);
-
- if(numGroupDigits > 0) {
- fmt.append("#,");
- DefaultTextFunctions.nchars(fmt, numGroupDigits - 1, '#');
- }
-
- fmt.append(incLeadDigit ? "0" : "#");
- if(numDecDigits > 0) {
- fmt.append(".");
- DefaultTextFunctions.nchars(fmt, numDecDigits, '0');
- }
-
- numPatType.appendSuffix(fmt);
-
- if(negParens) {
- // the javadocs claim the second pattern does not need to be fully
- // defined, but it doesn't seem to work that way
- String mainPat = fmt.toString();
- fmt.append(";(").append(mainPat).append(")");
- }
-
- return fmt.toString();
- }
-
- private static byte getFormatCodeType(char c) {
- if((c >= 0) && (c < 127)) {
- return FORMAT_CODE_TYPES[c];
- }
- return FCT_UNKNOWN;
- }
-
- private static void setFormatCodeTypes(String chars, byte type) {
- for(char c : chars.toCharArray()) {
- FORMAT_CODE_TYPES[c] = type;
- }
- }
-
- private static String parseQuotedString(ExprBuf buf) {
- return ExpressionTokenizer.parseStringUntil(buf, null, QUOTE_CHAR, true);
- }
-
- private static void parseQuotedString(ExprBuf buf, StringBuilder sb) {
- ExpressionTokenizer.parseStringUntil(buf, null, QUOTE_CHAR, true, sb);
- }
-
- private static String parseColorString(ExprBuf buf) {
- return ExpressionTokenizer.parseStringUntil(
- buf, START_COLOR_CHAR, END_COLOR_CHAR, false);
- }
-
- private static void fillInPartialPrefixes() {
- List<String> validPrefixes = new ArrayList<>(DATE_FMT_BUILDERS.keySet());
- for(String validPrefix : validPrefixes) {
- int len = validPrefix.length();
- while(len > 1) {
- --len;
- validPrefix = validPrefix.substring(0, len);
- DATE_FMT_BUILDERS.putIfAbsent(validPrefix, PARTIAL_PREFIX);
- }
- }
- }
-
- private static void putPredefFormat(String key, Fmt fmt) {
- // predefined formats return empty string for null
- Fmt wrapFmt = args -> (args.isNullOrEmptyString() ?
- ValueSupport.EMPTY_STR_VAL :
- fmt.format(args));
- PREDEF_FMTS.put(key, wrapFmt);
- }
-
- private static final class PredefDateFmt implements Fmt
- {
- private final TemporalConfig.Type _type;
-
- private PredefDateFmt(TemporalConfig.Type type) {
- _type = type;
- }
-
- @Override
- public Value format(Args args) {
- DateTimeFormatter dtf = args._ctx.createDateFormatter(
- args._ctx.getTemporalConfig().getDateTimeFormat(_type));
- return ValueSupport.toValue(dtf.format(args.getAsLocalDateTime()));
- }
- }
-
- private static final class PredefBoolFmt implements Fmt
- {
- private final Value _trueVal;
- private final Value _falseVal;
-
- private PredefBoolFmt(String trueStr, String falseStr) {
- _trueVal = ValueSupport.toValue(trueStr);
- _falseVal = ValueSupport.toValue(falseStr);
- }
-
- @Override
- public Value format(Args args) {
- return(args.getAsBoolean() ? _trueVal : _falseVal);
- }
- }
-
- private static abstract class BaseNumberFmt implements Fmt
- {
- @Override
- public Value format(Args args) {
- NumberFormat df = getNumberFormat(args);
- return ValueSupport.toValue(df.format(args.getAsBigDecimal()));
- }
-
- protected abstract NumberFormat getNumberFormat(Args args);
- }
-
- private static final class PredefNumberFmt extends BaseNumberFmt
- {
- private final NumericConfig.Type _type;
-
- private PredefNumberFmt(NumericConfig.Type type) {
- _type = type;
- }
-
- @Override
- protected NumberFormat getNumberFormat(Args args) {
- return args._ctx.createDecimalFormat(
- args._ctx.getNumericConfig().getNumberFormat(_type));
- }
- }
-
- private static final class ScientificPredefNumberFmt extends BaseNumberFmt
- {
- @Override
- protected NumberFormat getNumberFormat(Args args) {
- NumberFormat df = args._ctx.createDecimalFormat(
- args._ctx.getNumericConfig().getNumberFormat(
- NumericConfig.Type.SCIENTIFIC));
- df = new NumberFormatter.ScientificFormat(df);
- return df;
- }
- }
-
- private static final class SimpleDFB implements DateFormatBuilder
- {
- private final String _pat;
-
- private SimpleDFB(String pat) {
- _pat = pat;
- }
- @Override
- public void build(DateTimeFormatterBuilder dtfb, Args args,
- boolean hasAmPm, Value.Type dtType) {
- dtfb.appendPattern(_pat);
- }
- }
-
- private static final class HourlyDFB implements DateFormatBuilder
- {
- private final String _pat12;
- private final String _pat24;
-
- private HourlyDFB(String pat12, String pat24) {
- _pat12 = pat12;
- _pat24 = pat24;
- }
- @Override
- public void build(DateTimeFormatterBuilder dtfb, Args args,
- boolean hasAmPm, Value.Type dtTypePm) {
- // annoyingly the "hour" patterns are the same and depend on the
- // existence of the am/pm pattern to determine how they function (12 vs
- // 24 hour).
- dtfb.appendPattern(hasAmPm ? _pat12 : _pat24);
- }
- }
-
- private static final class PredefDFB implements DateFormatBuilder
- {
- private final TemporalConfig.Type _type;
-
- private PredefDFB(TemporalConfig.Type type) {
- _type = type;
- }
- @Override
- public void build(DateTimeFormatterBuilder dtfb, Args args,
- boolean hasAmPm, Value.Type dtType) {
- dtfb.appendPattern(args._ctx.getTemporalConfig().getDateTimeFormat(_type));
- }
- }
-
- private static abstract class WeekBasedDFB implements DateFormatBuilder
- {
- @Override
- public void build(DateTimeFormatterBuilder dtfb, Args args,
- boolean hasAmPm, Value.Type dtType) {
- dtfb.appendValue(getField(DefaultDateFunctions.weekFields(
- args._firstDay, args._firstWeekType)));
- }
-
- protected abstract TemporalField getField(WeekFields weekFields);
- }
-
- private static final class AmPmDFB extends AbstractMap<Long,String>
- implements DateFormatBuilder
- {
- private static final Long ZERO_KEY = 0L;
- private final String _am;
- private final String _pm;
-
- private AmPmDFB(String am, String pm) {
- _am = am;
- _pm = pm;
- }
- @Override
- public void build(DateTimeFormatterBuilder dtfb, Args args,
- boolean hasAmPm, Value.Type dtType) {
- dtfb.appendText(ChronoField.AMPM_OF_DAY, this);
- }
- @Override
- public int size() {
- return 2;
- }
- @Override
- public String get(Object key) {
- return(ZERO_KEY.equals(key) ? _am : _pm);
- }
- @Override
- public Set<Map.Entry<Long,String>> entrySet() {
- return new AbstractSet<Map.Entry<Long,String>>() {
- @Override
- public int size() {
- return 2;
- }
- @Override
- public Iterator<Map.Entry<Long,String>> iterator() {
- return Arrays.<Map.Entry<Long,String>>asList(
- new AbstractMap.SimpleImmutableEntry<Long,String>(0L, _am),
- new AbstractMap.SimpleImmutableEntry<Long,String>(1L, _pm))
- .iterator();
- }
- };
- }
- }
-
- private static final class CustomFmt implements Fmt
- {
- private final Fmt _fmt;
- private final Fmt _emptyFmt;
-
- private CustomFmt(Fmt fmt) {
- this(fmt, NULL_FMT);
- }
-
- private CustomFmt(Fmt fmt, Fmt emptyFmt) {
- _fmt = fmt;
- _emptyFmt = emptyFmt;
- }
-
- @Override
- public Value format(Args args) {
- Fmt fmt = _fmt;
- if(args.maybeCoerceToEmptyString()) {
- fmt = _emptyFmt;
- }
- return fmt.format(args);
- }
- }
-
- private static final class CharSourceFmt implements Fmt
- {
- private final List<BiConsumer<StringBuilder,CharSource>> _subFmts;
- private final int _numPlaceholders;
- private final boolean _rightAligned;
- private final TextCase _textCase;
-
- private CharSourceFmt(List<BiConsumer<StringBuilder,CharSource>> subFmts,
- int numPlaceholders, boolean rightAligned,
- TextCase textCase) {
- _subFmts = subFmts;
- _numPlaceholders = numPlaceholders;
- _rightAligned = rightAligned;
- _textCase = textCase;
- }
-
- @Override
- public Value format(Args args) {
- CharSource cs = new CharSource(args.getAsString(), _numPlaceholders,
- _rightAligned, _textCase);
- StringBuilder sb = new StringBuilder();
- _subFmts.stream().forEach(fmt -> fmt.accept(sb, cs));
- cs.appendRemaining(sb);
- return ValueSupport.toValue(sb.toString());
- }
- }
-
- private static final class CharSource
- {
- private int _prefLen;
- private final String _str;
- private int _strPos;
- private final TextCase _textCase;
-
- private CharSource(String str, int len, boolean rightAligned,
- TextCase textCase) {
- _str = str;
- _textCase = textCase;
- int strLen = str.length();
- if(len > strLen) {
- if(rightAligned) {
- _prefLen = len - strLen;
- }
- } else if(len < strLen) {
- // it doesn't make sense to me, but the meaning of "right aligned"
- // seems to flip when the string is longer than the format length
- if(!rightAligned) {
- _strPos = strLen - len;
- }
- }
- }
-
- public int next() {
- if(_prefLen > 0) {
- --_prefLen;
- return NO_CHAR;
- }
- if(_strPos < _str.length()) {
- return _textCase.apply(_str.charAt(_strPos++));
- }
- return NO_CHAR;
- }
-
- public void appendRemaining(StringBuilder sb) {
- int strLen = _str.length();
- while(_strPos < strLen) {
- sb.append(_textCase.apply(_str.charAt(_strPos++)));
- }
- }
- }
-
- private static abstract class BaseCustomNumberFmt implements Fmt
- {
- @Override
- public Value format(Args args) {
- if(args._expr.isNull()) {
- return formatNull(args);
- }
-
- BigDecimal bd = args.getAsBigDecimal();
- int cmp = BigDecimal.ZERO.compareTo(bd);
-
- return ((cmp < 0) ? formatPos(bd, args) :
- ((cmp > 0) ? formatNeg(bd, args) :
- formatZero(bd, args)));
- }
-
- protected abstract Value formatNull(Args args);
- protected abstract Value formatPos(BigDecimal bd, Args args);
- protected abstract Value formatNeg(BigDecimal bd, Args args);
- protected abstract Value formatZero(BigDecimal bd, Args args);
- }
-
- private static final class CustomGeneralFmt extends BaseCustomNumberFmt
- {
- private final Value _posVal;
- private final Value _negVal;
- private final Value _zeroVal;
- private final Value _nullVal;
-
- private CustomGeneralFmt(Value posVal, Value negVal,
- Value zeroVal, Value nullVal) {
- _posVal = posVal;
- _negVal = negVal;
- _zeroVal = zeroVal;
- _nullVal = nullVal;
- }
-
- @Override
- protected Value formatNull(Args args) {
- return _nullVal;
- }
- @Override
- protected Value formatPos(BigDecimal bd, Args args) {
- return _posVal;
- }
- @Override
- protected Value formatNeg(BigDecimal bd, Args args) {
- return _negVal;
- }
- @Override
- protected Value formatZero(BigDecimal bd, Args args) {
- return _zeroVal;
- }
- }
-
- private static final class CustomNumberFmt extends BaseCustomNumberFmt
- {
- private final BDFormat _posFmt;
- private final BDFormat _negFmt;
- private final BDFormat _zeroFmt;
- private final BDFormat _nullFmt;
-
- private CustomNumberFmt(BDFormat posFmt, BDFormat negFmt,
- BDFormat zeroFmt, BDFormat nullFmt) {
- _posFmt = posFmt;
- _negFmt = negFmt;
- _zeroFmt = zeroFmt;
- _nullFmt = nullFmt;
- }
-
- private Value formatMaybeZero(BigDecimal bd, BDFormat fmt) {
- // in theory we want to use the given format. however, if, due to
- // rounding, we end up with a number equivalent to zero, then we fall
- // back to the zero format. if we are using scientific notation,
- // however, then don't worry about this
- int maxDecDigits = fmt.getMaxDecimalDigits();
- if(maxDecDigits < bd.scale()) {
- bd = bd.setScale(maxDecDigits, NumberFormatter.ROUND_MODE);
- if(BigDecimal.ZERO.compareTo(bd) == 0) {
- // fall back to zero format
- fmt = _zeroFmt;
- }
- }
-
- return ValueSupport.toValue(fmt.format(bd));
- }
-
- @Override
- protected Value formatNull(Args args) {
- return ValueSupport.toValue(_nullFmt.format(BigDecimal.ZERO));
- }
- @Override
- protected Value formatPos(BigDecimal bd, Args args) {
- return formatMaybeZero(bd, _posFmt);
- }
- @Override
- protected Value formatNeg(BigDecimal bd, Args args) {
- return formatMaybeZero(bd.negate(), _negFmt);
- }
- @Override
- protected Value formatZero(BigDecimal bd, Args args) {
- return ValueSupport.toValue(_zeroFmt.format(bd));
- }
- }
-
- private static abstract class BDFormat
- {
- public int getMaxDecimalDigits() {
- return Integer.MAX_VALUE;
- }
-
- public abstract String format(BigDecimal bd);
- }
-
- private static final class LiteralBDFormat extends BDFormat
- {
- private final String _str;
-
- private LiteralBDFormat(String str) {
- _str = str;
- }
-
- @Override
- public String format(BigDecimal bd) {
- return _str;
- }
- }
-
- private static class BaseBDFormat extends BDFormat
- {
- private final NumberFormat _nf;
-
- private BaseBDFormat(NumberFormat nf) {
- _nf = nf;
- }
-
- @Override
- public String format(BigDecimal bd) {
- return _nf.format(bd);
- }
- }
-
- private static final class DecimalBDFormat extends BaseBDFormat
- {
- private final int _maxDecDigits;
-
- private DecimalBDFormat(DecimalFormat df) {
- super(df);
-
- int maxDecDigits = df.getMaximumFractionDigits();
- int mult = df.getMultiplier();
- while(mult > 1) {
- ++maxDecDigits;
- mult /= 10;
- }
- _maxDecDigits = maxDecDigits;
- }
-
- @Override
- public int getMaxDecimalDigits() {
- return _maxDecDigits;
- }
- }
- }
|