You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FormatUtil.java 49KB

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