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 35KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203
  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.NumberFormat;
  16. import java.time.LocalDateTime;
  17. import java.time.format.DateTimeFormatter;
  18. import java.time.format.DateTimeFormatterBuilder;
  19. import java.time.temporal.ChronoField;
  20. import java.time.temporal.TemporalField;
  21. import java.time.temporal.WeekFields;
  22. import java.util.AbstractMap;
  23. import java.util.AbstractSet;
  24. import java.util.ArrayList;
  25. import java.util.Arrays;
  26. import java.util.HashMap;
  27. import java.util.Iterator;
  28. import java.util.List;
  29. import java.util.Map;
  30. import java.util.Set;
  31. import java.util.function.BiConsumer;
  32. import com.healthmarketscience.jackcess.expr.EvalContext;
  33. import com.healthmarketscience.jackcess.expr.EvalException;
  34. import com.healthmarketscience.jackcess.expr.NumericConfig;
  35. import com.healthmarketscience.jackcess.expr.TemporalConfig;
  36. import com.healthmarketscience.jackcess.expr.Value;
  37. import static com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.ExprBuf;
  38. /**
  39. *
  40. * @author James Ahlborn
  41. */
  42. public class FormatUtil
  43. {
  44. public enum NumPatternType {
  45. GENERAL,
  46. CURRENCY {
  47. @Override
  48. protected void appendPrefix(StringBuilder fmt) {
  49. fmt.append('\u00A4');
  50. }
  51. @Override
  52. protected boolean useParensForNegatives(NumericConfig cfg) {
  53. return cfg.useParensForCurrencyNegatives();
  54. }
  55. },
  56. EURO {
  57. @Override
  58. protected void appendPrefix(StringBuilder fmt) {
  59. fmt.append('\u20AC');
  60. }
  61. @Override
  62. protected boolean useParensForNegatives(NumericConfig cfg) {
  63. return cfg.useParensForCurrencyNegatives();
  64. }
  65. },
  66. PERCENT {
  67. @Override
  68. protected void appendSuffix(StringBuilder fmt) {
  69. fmt.append('%');
  70. }
  71. },
  72. SCIENTIFIC {
  73. @Override
  74. protected void appendSuffix(StringBuilder fmt) {
  75. fmt.append("E0");
  76. }
  77. };
  78. protected void appendPrefix(StringBuilder fmt) {}
  79. protected void appendSuffix(StringBuilder fmt) {}
  80. protected boolean useParensForNegatives(NumericConfig cfg) {
  81. return cfg.useParensForNegatives();
  82. }
  83. }
  84. private enum TextCase {
  85. NONE,
  86. UPPER {
  87. @Override public char apply(char c) {
  88. return Character.toUpperCase(c);
  89. }
  90. },
  91. LOWER {
  92. @Override public char apply(char c) {
  93. return Character.toLowerCase(c);
  94. }
  95. };
  96. public char apply(char c) {
  97. return c;
  98. }
  99. }
  100. private static final Map<String,Fmt> PREDEF_FMTS = new HashMap<String,Fmt>();
  101. static {
  102. PREDEF_FMTS.put("General Date", args -> ValueSupport.toValue(
  103. args.coerceToDateTimeValue().getAsString()));
  104. PREDEF_FMTS.put("Long Date",
  105. new PredefDateFmt(TemporalConfig.Type.LONG_DATE));
  106. PREDEF_FMTS.put("Medium Date",
  107. new PredefDateFmt(TemporalConfig.Type.MEDIUM_DATE));
  108. PREDEF_FMTS.put("Short Date",
  109. new PredefDateFmt(TemporalConfig.Type.SHORT_DATE));
  110. PREDEF_FMTS.put("Long Time",
  111. new PredefDateFmt(TemporalConfig.Type.LONG_TIME));
  112. PREDEF_FMTS.put("Medium Time",
  113. new PredefDateFmt(TemporalConfig.Type.MEDIUM_TIME));
  114. PREDEF_FMTS.put("Short Time",
  115. new PredefDateFmt(TemporalConfig.Type.SHORT_TIME));
  116. PREDEF_FMTS.put("General Number", args -> ValueSupport.toValue(
  117. args.coerceToNumberValue().getAsString()));
  118. PREDEF_FMTS.put("Currency",
  119. new PredefNumberFmt(NumericConfig.Type.CURRENCY));
  120. PREDEF_FMTS.put("Euro", new PredefNumberFmt(NumericConfig.Type.EURO));
  121. PREDEF_FMTS.put("Fixed",
  122. new PredefNumberFmt(NumericConfig.Type.FIXED));
  123. PREDEF_FMTS.put("Standard",
  124. new PredefNumberFmt(NumericConfig.Type.STANDARD));
  125. PREDEF_FMTS.put("Percent",
  126. new PredefNumberFmt(NumericConfig.Type.PERCENT));
  127. PREDEF_FMTS.put("Scientific", new ScientificPredefNumberFmt());
  128. PREDEF_FMTS.put("True/False", new PredefBoolFmt("True", "False"));
  129. PREDEF_FMTS.put("Yes/No", new PredefBoolFmt("Yes", "No"));
  130. PREDEF_FMTS.put("On/Off", new PredefBoolFmt("On", "Off"));
  131. }
  132. private static final Fmt NULL_FMT = args -> ValueSupport.EMPTY_STR_VAL;
  133. private static final char QUOTE_CHAR = '"';
  134. private static final char ESCAPE_CHAR = '\\';
  135. private static final char LEFT_ALIGN_CHAR = '!';
  136. private static final char START_COLOR_CHAR = '[';
  137. private static final char END_COLOR_CHAR = ']';
  138. private static final char CHOICE_SEP_CHAR = ';';
  139. // this only seems to be useful if you have fixed length string fields which
  140. // isn't a normal thing in ms access
  141. private static final char FILL_ESCAPE_CHAR = '*';
  142. private static final char REQ_PLACEHOLDER_CHAR = '@';
  143. private static final char OPT_PLACEHOLDER_CHAR = '&';
  144. private static final char TO_UPPER_CHAR = '>';
  145. private static final char TO_LOWER_CHAR = '<';
  146. private static final char DT_LIT_COLON_CHAR = ':';
  147. private static final char DT_LIT_SLASH_CHAR = '/';
  148. private static final int NO_CHAR = -1;
  149. private static final byte FCT_UNKNOWN = 0;
  150. private static final byte FCT_LITERAL = 1;
  151. private static final byte FCT_GENERAL = 2;
  152. private static final byte FCT_DATE = 3;
  153. private static final byte FCT_NUMBER = 4;
  154. private static final byte FCT_TEXT = 5;
  155. private static final byte[] FORMAT_CODE_TYPES = new byte[127];
  156. static {
  157. setFormatCodeTypes(" $+-()", FCT_LITERAL);
  158. setFormatCodeTypes("\"!*\\[];", FCT_GENERAL);
  159. setFormatCodeTypes(":/cdwmqyhnstampmAMPM", FCT_DATE);
  160. setFormatCodeTypes(".,0#%Ee", FCT_NUMBER);
  161. setFormatCodeTypes("@&<>", FCT_TEXT);
  162. }
  163. @FunctionalInterface
  164. interface Fmt {
  165. public Value format(Args args);
  166. }
  167. @FunctionalInterface
  168. interface DateFormatBuilder {
  169. public void build(DateTimeFormatterBuilder dtfb, Args args);
  170. }
  171. private static final DateFormatBuilder PARTIAL_PREFIX =
  172. (dtfb, args) -> {
  173. throw new UnsupportedOperationException();
  174. };
  175. private static final Map<String,DateFormatBuilder> DATE_FMT_BUILDERS =
  176. new HashMap<>();
  177. static {
  178. DATE_FMT_BUILDERS.put("c",
  179. (dtfb, args) ->
  180. dtfb.append(ValueSupport.getDateFormatForType(
  181. args._ctx, args._expr.getType())));
  182. DATE_FMT_BUILDERS.put("d", new SimpleDFB("d"));
  183. DATE_FMT_BUILDERS.put("dd", new SimpleDFB("dd"));
  184. DATE_FMT_BUILDERS.put("ddd", new SimpleDFB("eee"));
  185. DATE_FMT_BUILDERS.put("dddd", new SimpleDFB("eeee"));
  186. DATE_FMT_BUILDERS.put("ddddd", new PredefDFB(TemporalConfig.Type.SHORT_DATE));
  187. DATE_FMT_BUILDERS.put("dddddd", new PredefDFB(TemporalConfig.Type.LONG_DATE));
  188. DATE_FMT_BUILDERS.put("w", new WeekBasedDFB() {
  189. @Override
  190. protected TemporalField getField(WeekFields weekFields) {
  191. return weekFields.dayOfWeek();
  192. }
  193. });
  194. DATE_FMT_BUILDERS.put("ww", new WeekBasedDFB() {
  195. @Override
  196. protected TemporalField getField(WeekFields weekFields) {
  197. return weekFields.weekOfWeekBasedYear();
  198. }
  199. });
  200. DATE_FMT_BUILDERS.put("m", new SimpleDFB("M"));
  201. DATE_FMT_BUILDERS.put("mm", new SimpleDFB("MM"));
  202. DATE_FMT_BUILDERS.put("mmm", new SimpleDFB("LLL"));
  203. DATE_FMT_BUILDERS.put("mmmm", new SimpleDFB("LLLL"));
  204. DATE_FMT_BUILDERS.put("q", new SimpleDFB("Q"));
  205. DATE_FMT_BUILDERS.put("y", new SimpleDFB("D"));
  206. DATE_FMT_BUILDERS.put("yy", new SimpleDFB("yy"));
  207. DATE_FMT_BUILDERS.put("yyyy", new SimpleDFB("yyyy"));
  208. DATE_FMT_BUILDERS.put("h", new SimpleDFB("H"));
  209. DATE_FMT_BUILDERS.put("hh", new SimpleDFB("HH"));
  210. DATE_FMT_BUILDERS.put("n", new SimpleDFB("m"));
  211. DATE_FMT_BUILDERS.put("nn", new SimpleDFB("mm"));
  212. DATE_FMT_BUILDERS.put("s", new SimpleDFB("s"));
  213. DATE_FMT_BUILDERS.put("ss", new SimpleDFB("ss"));
  214. DATE_FMT_BUILDERS.put("ttttt", new PredefDFB(TemporalConfig.Type.LONG_TIME));
  215. DATE_FMT_BUILDERS.put("AM/PM", new AmPmDFB("AM", "PM"));
  216. DATE_FMT_BUILDERS.put("am/pm", new AmPmDFB("am", "pm"));
  217. DATE_FMT_BUILDERS.put("A/P", new AmPmDFB("A", "P"));
  218. DATE_FMT_BUILDERS.put("a/p", new AmPmDFB("a", "p"));
  219. DATE_FMT_BUILDERS.put("AMPM",
  220. (dtfb, args) -> {
  221. String[] amPmStrs = args._ctx.getTemporalConfig().getAmPmStrings();
  222. new AmPmDFB(amPmStrs[0], amPmStrs[1]).build(dtfb, args);
  223. }
  224. );
  225. fillInPartialPrefixes();
  226. }
  227. private static final int NF_POS_IDX = 0;
  228. private static final int NF_NEG_IDX = 1;
  229. private static final int NF_ZERO_IDX = 2;
  230. private static final int NF_NULL_IDX = 3;
  231. private static final int NUM_NF_FMTS = 4;
  232. private static final class Args
  233. {
  234. private final EvalContext _ctx;
  235. private Value _expr;
  236. private final int _firstDay;
  237. private final int _firstWeekType;
  238. private Args(EvalContext ctx, Value expr, int firstDay, int firstWeekType) {
  239. _ctx = ctx;
  240. _expr = expr;
  241. _firstDay = firstDay;
  242. _firstWeekType = firstWeekType;
  243. }
  244. public Args coerceToDateTimeValue() {
  245. if(!_expr.getType().isTemporal()) {
  246. // format coerces boolean strings to numbers
  247. Value boolExpr = null;
  248. if(_expr.getType().isString() &&
  249. ((boolExpr = maybeGetStringAsBooleanValue()) != null)) {
  250. _expr = boolExpr;
  251. }
  252. // StringValue already handles most String -> Number -> Date/Time, so
  253. // most other convertions work here
  254. _expr = _expr.getAsDateTimeValue(_ctx);
  255. }
  256. return this;
  257. }
  258. public Args coerceToNumberValue() {
  259. if(!_expr.getType().isNumeric()) {
  260. if(_expr.getType().isString()) {
  261. // format coerces "true"/"false" to boolean values
  262. Value boolExpr = maybeGetStringAsBooleanValue();
  263. if(boolExpr != null) {
  264. _expr = boolExpr;
  265. } else {
  266. BigDecimal bd = DefaultFunctions.maybeGetAsBigDecimal(_ctx, _expr);
  267. if(bd != null) {
  268. _expr = ValueSupport.toValue(bd);
  269. } else {
  270. // convert to date to number. this doesn't happen as part of the
  271. // default value coercion behavior, but the format method tries
  272. // harder
  273. Value maybe = DefaultFunctions.maybeGetAsDateTimeValue(
  274. _ctx, _expr);
  275. if(maybe != null) {
  276. _expr = ValueSupport.toValue(maybe.getAsDouble(_ctx));
  277. }
  278. }
  279. }
  280. } else {
  281. // convert date to number
  282. _expr = ValueSupport.toValue(_expr.getAsDouble(_ctx));
  283. }
  284. }
  285. return this;
  286. }
  287. private Value maybeGetStringAsBooleanValue() {
  288. // format coerces "true"/"false" to boolean values
  289. String val = _expr.getAsString(_ctx);
  290. if("true".equalsIgnoreCase(val)) {
  291. return ValueSupport.TRUE_VAL;
  292. }
  293. if("false".equalsIgnoreCase(val)) {
  294. return ValueSupport.FALSE_VAL;
  295. }
  296. return null;
  297. }
  298. public BigDecimal getAsBigDecimal() {
  299. coerceToNumberValue();
  300. return _expr.getAsBigDecimal(_ctx);
  301. }
  302. public LocalDateTime getAsLocalDateTime() {
  303. coerceToDateTimeValue();
  304. return _expr.getAsLocalDateTime(_ctx);
  305. }
  306. public String getAsString() {
  307. return _expr.getAsString(_ctx);
  308. }
  309. }
  310. private FormatUtil() {}
  311. public static Value format(EvalContext ctx, Value expr, String fmtStr,
  312. int firstDay, int firstWeekType) {
  313. try {
  314. Args args = new Args(ctx, expr, firstDay, firstWeekType);
  315. Fmt predefFmt = PREDEF_FMTS.get(fmtStr);
  316. if(predefFmt != null) {
  317. if(expr.isNull()) {
  318. // predefined formats return empty string for null
  319. return ValueSupport.EMPTY_STR_VAL;
  320. }
  321. return predefFmt.format(args);
  322. }
  323. return parseCustomFormat(fmtStr, args).format(args);
  324. } catch(EvalException ee) {
  325. // values which cannot be formatted as the target type are just
  326. // returned "as is"
  327. return expr;
  328. }
  329. }
  330. private static Fmt parseCustomFormat(String fmtStr, Args args) {
  331. ExprBuf buf = new ExprBuf(fmtStr, null);
  332. // do partial pass to determine what type of format this is
  333. byte curFormatType = determineFormatType(buf);
  334. // reset buffer for real parse
  335. buf.reset(0);
  336. switch(curFormatType) {
  337. case FCT_GENERAL:
  338. return parseCustomGeneralFormat(buf, args);
  339. case FCT_DATE:
  340. return parseCustomDateFormat(buf, args);
  341. case FCT_NUMBER:
  342. return parseCustomNumberFormat(buf, args);
  343. case FCT_TEXT:
  344. return parseCustomTextFormat(buf, args);
  345. default:
  346. throw new EvalException("Invalid format type " + curFormatType);
  347. }
  348. }
  349. private static byte determineFormatType(ExprBuf buf) {
  350. while(buf.hasNext()) {
  351. char c = buf.next();
  352. byte fmtType = getFormatCodeType(c);
  353. switch(fmtType) {
  354. case FCT_UNKNOWN:
  355. case FCT_LITERAL:
  356. // meaningless, ignore for now
  357. break;
  358. case FCT_GENERAL:
  359. switch(c) {
  360. case QUOTE_CHAR:
  361. parseQuotedString(buf);
  362. break;
  363. case START_COLOR_CHAR:
  364. parseColorString(buf);
  365. break;
  366. case ESCAPE_CHAR:
  367. case FILL_ESCAPE_CHAR:
  368. if(buf.hasNext()) {
  369. buf.next();
  370. }
  371. break;
  372. default:
  373. // meaningless, ignore for now
  374. }
  375. break;
  376. case FCT_DATE:
  377. case FCT_NUMBER:
  378. case FCT_TEXT:
  379. // found specific type
  380. return fmtType;
  381. default:
  382. throw new EvalException("Invalid format type " + fmtType);
  383. }
  384. }
  385. // no specific type
  386. return FCT_GENERAL;
  387. }
  388. private static Fmt parseCustomGeneralFormat(ExprBuf buf, Args args) {
  389. // a "general" format is actually a "yes/no" format which functions almost
  390. // exactly like a number format (without any number format specific chars)
  391. if(!args._expr.isNull()) {
  392. args.coerceToNumberValue();
  393. }
  394. StringBuilder sb = new StringBuilder();
  395. String[] fmtStrs = new String[NUM_NF_FMTS];
  396. int fmtIdx = 0;
  397. BUF_LOOP:
  398. while(buf.hasNext()) {
  399. char c = buf.next();
  400. int fmtType = getFormatCodeType(c);
  401. switch(fmtType) {
  402. case FCT_GENERAL:
  403. switch(c) {
  404. case LEFT_ALIGN_CHAR:
  405. // no effect
  406. break;
  407. case QUOTE_CHAR:
  408. parseQuotedString(buf, sb);
  409. break;
  410. case START_COLOR_CHAR:
  411. // color strings seem to be ignored
  412. parseColorString(buf);
  413. break;
  414. case ESCAPE_CHAR:
  415. if(buf.hasNext()) {
  416. sb.append(buf.next());
  417. }
  418. break;
  419. case FILL_ESCAPE_CHAR:
  420. // unclear what this actually does. online examples don't seem to
  421. // match with experimental results. for now, ignore
  422. if(buf.hasNext()) {
  423. buf.next();
  424. }
  425. break;
  426. case CHOICE_SEP_CHAR:
  427. // yes/no (number) format supports up to 4 formats: pos, neg, zero,
  428. // null. after that, ignore the rest
  429. if(fmtIdx == (NUM_NF_FMTS - 1)) {
  430. // last possible format, ignore remaining
  431. break BUF_LOOP;
  432. }
  433. addCustomGeneralFormat(fmtStrs, fmtIdx++, sb);
  434. break;
  435. default:
  436. sb.append(c);
  437. }
  438. break;
  439. default:
  440. sb.append(c);
  441. }
  442. }
  443. // fill in remaining formats
  444. while(fmtIdx < NUM_NF_FMTS) {
  445. addCustomGeneralFormat(fmtStrs, fmtIdx++, sb);
  446. }
  447. return new CustomNumberFmt(
  448. createCustomLiteralFormat(fmtStrs[NF_POS_IDX]),
  449. createCustomLiteralFormat(fmtStrs[NF_NEG_IDX]),
  450. createCustomLiteralFormat(fmtStrs[NF_ZERO_IDX]),
  451. createCustomLiteralFormat(fmtStrs[NF_NULL_IDX]));
  452. }
  453. private static void addCustomGeneralFormat(String[] fmtStrs, int fmtIdx,
  454. StringBuilder sb)
  455. {
  456. if(sb.length() == 0) {
  457. // do special empty format handling on a per-format-type basis
  458. switch(fmtIdx) {
  459. case NF_NEG_IDX:
  460. // re-use "pos" format with '-' prepended
  461. sb.append('-').append(fmtStrs[NF_POS_IDX]);
  462. break;
  463. case NF_ZERO_IDX:
  464. // re-use "pos" format
  465. sb.append(fmtStrs[NF_POS_IDX]);
  466. break;
  467. default:
  468. // use empty string result
  469. }
  470. }
  471. fmtStrs[fmtIdx] = sb.toString();
  472. sb.setLength(0);
  473. }
  474. private static Fmt createCustomLiteralFormat(String str) {
  475. Value literal = ValueSupport.toValue(str);
  476. return args -> literal;
  477. }
  478. private static Fmt parseCustomDateFormat(ExprBuf buf, Args args) {
  479. // custom date formats don't have special null handling
  480. if(args._expr.isNull()) {
  481. return NULL_FMT;
  482. }
  483. // force to temporal value before proceeding (this will throw if we don't
  484. // have a date/time and therefore don't need to proceed with the rest of
  485. // the format translation work)
  486. args.coerceToDateTimeValue();
  487. DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder();
  488. BUF_LOOP:
  489. while(buf.hasNext()) {
  490. char c = buf.next();
  491. int fmtType = getFormatCodeType(c);
  492. switch(fmtType) {
  493. case FCT_GENERAL:
  494. switch(c) {
  495. case QUOTE_CHAR:
  496. dtfb.appendLiteral(parseQuotedString(buf));
  497. break;
  498. case START_COLOR_CHAR:
  499. // color strings seem to be ignored
  500. parseColorString(buf);
  501. break;
  502. case ESCAPE_CHAR:
  503. if(buf.hasNext()) {
  504. dtfb.appendLiteral(buf.next());
  505. }
  506. break;
  507. case FILL_ESCAPE_CHAR:
  508. // unclear what this actually does. online examples don't seem to
  509. // match with experimental results. for now, ignore
  510. if(buf.hasNext()) {
  511. buf.next();
  512. }
  513. break;
  514. case CHOICE_SEP_CHAR:
  515. // date/time doesn't use multiple pattern choices, but it does
  516. // respect the char. ignore everything after the first choice
  517. break BUF_LOOP;
  518. default:
  519. dtfb.appendLiteral(c);
  520. }
  521. break;
  522. case FCT_DATE:
  523. parseCustomDateFormatPattern(c, buf, dtfb, args);
  524. break;
  525. default:
  526. dtfb.appendLiteral(c);
  527. }
  528. }
  529. DateTimeFormatter dtf = dtfb.toFormatter(
  530. args._ctx.getTemporalConfig().getLocale());
  531. return argsParam -> ValueSupport.toValue(
  532. dtf.format(argsParam.getAsLocalDateTime()));
  533. }
  534. private static Fmt parseCustomNumberFormat(ExprBuf buf, Args args) {
  535. // force to number value before proceeding (this will throw if we don't
  536. // have a number and therefore don't need to proceed with the rest of
  537. // the format translation work)
  538. if(!args._expr.isNull()) {
  539. args.coerceToNumberValue();
  540. }
  541. StringBuilder sb = new StringBuilder();
  542. String[] fmtStrs = new String[NUM_NF_FMTS];
  543. int fmtIdx = 0;
  544. BUF_LOOP:
  545. while(buf.hasNext()) {
  546. char c = buf.next();
  547. int fmtType = getFormatCodeType(c);
  548. switch(fmtType) {
  549. case FCT_GENERAL:
  550. switch(c) {
  551. case LEFT_ALIGN_CHAR:
  552. // no effect
  553. break;
  554. case QUOTE_CHAR:
  555. parseQuotedString(buf, sb);
  556. break;
  557. case START_COLOR_CHAR:
  558. // color strings seem to be ignored
  559. parseColorString(buf);
  560. break;
  561. case ESCAPE_CHAR:
  562. if(buf.hasNext()) {
  563. sb.append(buf.next());
  564. }
  565. break;
  566. case FILL_ESCAPE_CHAR:
  567. // unclear what this actually does. online examples don't seem to
  568. // match with experimental results. for now, ignore
  569. if(buf.hasNext()) {
  570. buf.next();
  571. }
  572. break;
  573. case CHOICE_SEP_CHAR:
  574. // yes/no (number) format supports up to 4 formats: pos, neg, zero,
  575. // null. after that, ignore the rest
  576. if(fmtIdx == (NUM_NF_FMTS - 1)) {
  577. // last possible format, ignore remaining
  578. break BUF_LOOP;
  579. }
  580. addCustomGeneralFormat(fmtStrs, fmtIdx++, sb);
  581. break;
  582. default:
  583. sb.append(c);
  584. }
  585. break;
  586. default:
  587. sb.append(c);
  588. }
  589. }
  590. // fill in remaining formats
  591. while(fmtIdx < NUM_NF_FMTS) {
  592. addCustomGeneralFormat(fmtStrs, fmtIdx++, sb);
  593. }
  594. // FIXME writeme
  595. throw new UnsupportedOperationException();
  596. }
  597. private static Fmt parseCustomTextFormat(ExprBuf buf, Args args) {
  598. Fmt fmt = null;
  599. Fmt emptyFmt = null;
  600. List<BiConsumer<StringBuilder,CharSource>> subFmts = new ArrayList<>();
  601. int numPlaceholders = 0;
  602. boolean rightAligned = true;
  603. TextCase textCase = TextCase.NONE;
  604. StringBuilder pendingLiteral = new StringBuilder();
  605. boolean hasFmtChars = false;
  606. BUF_LOOP:
  607. while(buf.hasNext()) {
  608. char c = buf.next();
  609. hasFmtChars = true;
  610. int fmtType = getFormatCodeType(c);
  611. switch(fmtType) {
  612. case FCT_GENERAL:
  613. switch(c) {
  614. case LEFT_ALIGN_CHAR:
  615. rightAligned = false;
  616. break;
  617. case QUOTE_CHAR:
  618. parseQuotedString(buf, pendingLiteral);
  619. break;
  620. case START_COLOR_CHAR:
  621. // color strings seem to be ignored
  622. parseColorString(buf);
  623. break;
  624. case ESCAPE_CHAR:
  625. if(buf.hasNext()) {
  626. pendingLiteral.append(buf.next());
  627. }
  628. break;
  629. case FILL_ESCAPE_CHAR:
  630. // unclear what this actually does. online examples don't seem to
  631. // match with experimental results. for now, ignore
  632. if(buf.hasNext()) {
  633. buf.next();
  634. }
  635. break;
  636. case CHOICE_SEP_CHAR:
  637. // text format supports up to 2 formats: normal and empty/null.
  638. // after that, ignore the rest
  639. if(fmt != null) {
  640. // ignore remaining format
  641. break BUF_LOOP;
  642. }
  643. flushPendingTextLiteral(pendingLiteral, subFmts);
  644. fmt = new CharSourceFmt(subFmts, numPlaceholders, rightAligned,
  645. textCase);
  646. // reset for next format
  647. subFmts = new ArrayList<>();
  648. numPlaceholders = 0;
  649. rightAligned = true;
  650. textCase = TextCase.NONE;
  651. hasFmtChars = false;
  652. break;
  653. default:
  654. pendingLiteral.append(c);
  655. }
  656. break;
  657. case FCT_TEXT:
  658. switch(c) {
  659. case REQ_PLACEHOLDER_CHAR:
  660. flushPendingTextLiteral(pendingLiteral, subFmts);
  661. ++numPlaceholders;
  662. subFmts.add((sb,cs) -> {
  663. int tmp = cs.next();
  664. sb.append((tmp != NO_CHAR) ? (char)tmp : ' ');
  665. });
  666. break;
  667. case OPT_PLACEHOLDER_CHAR:
  668. flushPendingTextLiteral(pendingLiteral, subFmts);
  669. ++numPlaceholders;
  670. subFmts.add((sb,cs) -> {
  671. int tmp = cs.next();
  672. if(tmp != NO_CHAR) {
  673. sb.append((char)tmp);
  674. }
  675. });
  676. break;
  677. case TO_UPPER_CHAR:
  678. // an uppper and lower symbol cancel each other out
  679. textCase = ((textCase == TextCase.LOWER) ?
  680. TextCase.NONE : TextCase.UPPER);
  681. break;
  682. case TO_LOWER_CHAR:
  683. // an uppper and lower symbol cancel each other out
  684. textCase = ((textCase == TextCase.UPPER) ?
  685. TextCase.NONE : TextCase.LOWER);
  686. break;
  687. default:
  688. pendingLiteral.append(c);
  689. }
  690. break;
  691. default:
  692. pendingLiteral.append(c);
  693. }
  694. }
  695. flushPendingTextLiteral(pendingLiteral, subFmts);
  696. if(fmt == null) {
  697. fmt = new CharSourceFmt(subFmts, numPlaceholders, rightAligned,
  698. textCase);
  699. emptyFmt = NULL_FMT;
  700. } else if(emptyFmt == null) {
  701. emptyFmt = (hasFmtChars ?
  702. new CharSourceFmt(subFmts, numPlaceholders, rightAligned,
  703. textCase) :
  704. NULL_FMT);
  705. }
  706. return new CustomTextFmt(fmt, emptyFmt);
  707. }
  708. private static void flushPendingTextLiteral(
  709. StringBuilder pendingLiteral,
  710. List<BiConsumer<StringBuilder,CharSource>> subFmts) {
  711. if(pendingLiteral.length() == 0) {
  712. return;
  713. }
  714. String literal = pendingLiteral.toString();
  715. pendingLiteral.setLength(0);
  716. subFmts.add((sb, cs) -> sb.append(literal));
  717. }
  718. private static void parseCustomDateFormatPattern(
  719. char c, ExprBuf buf, DateTimeFormatterBuilder dtfb, Args args) {
  720. if((c == DT_LIT_COLON_CHAR) || (c == DT_LIT_SLASH_CHAR)) {
  721. // date/time literal char, nothing more to do
  722. dtfb.appendLiteral(c);
  723. return;
  724. }
  725. StringBuilder sb = buf.getScratchBuffer();
  726. sb.append(c);
  727. char firstChar = c;
  728. int firstPos = buf.curPos();
  729. DateFormatBuilder bestMatch = DATE_FMT_BUILDERS.get(sb.toString());
  730. int bestPos = firstPos;
  731. while(buf.hasNext()) {
  732. sb.append(buf.next());
  733. DateFormatBuilder dfb = DATE_FMT_BUILDERS.get(sb.toString());
  734. if(dfb == null) {
  735. // no more possible matches
  736. break;
  737. }
  738. if(dfb != PARTIAL_PREFIX) {
  739. // this is the longest, valid pattern we have seen so far
  740. bestMatch = dfb;
  741. bestPos = buf.curPos();
  742. }
  743. }
  744. if(bestMatch != PARTIAL_PREFIX) {
  745. // apply valid pattern
  746. buf.reset(bestPos);
  747. bestMatch.build(dtfb, args);
  748. } else {
  749. // just consume the first char
  750. buf.reset(firstPos);
  751. dtfb.appendLiteral(firstChar);
  752. }
  753. }
  754. public static String createNumberFormatPattern(
  755. NumPatternType numPatType, int numDecDigits, boolean incLeadDigit,
  756. boolean negParens, int numGroupDigits) {
  757. StringBuilder fmt = new StringBuilder();
  758. numPatType.appendPrefix(fmt);
  759. if(numGroupDigits > 0) {
  760. fmt.append("#,");
  761. DefaultTextFunctions.nchars(fmt, numGroupDigits - 1, '#');
  762. }
  763. fmt.append(incLeadDigit ? "0" : "#");
  764. if(numDecDigits > 0) {
  765. fmt.append(".");
  766. DefaultTextFunctions.nchars(fmt, numDecDigits, '0');
  767. }
  768. numPatType.appendSuffix(fmt);
  769. if(negParens) {
  770. // the javadocs claim the second pattern does not need to be fully
  771. // defined, but it doesn't seem to work that way
  772. String mainPat = fmt.toString();
  773. fmt.append(";(").append(mainPat).append(")");
  774. }
  775. return fmt.toString();
  776. }
  777. private static byte getFormatCodeType(char c) {
  778. if((c >= 0) && (c < 127)) {
  779. return FORMAT_CODE_TYPES[c];
  780. }
  781. return FCT_UNKNOWN;
  782. }
  783. private static void setFormatCodeTypes(String chars, byte type) {
  784. for(char c : chars.toCharArray()) {
  785. FORMAT_CODE_TYPES[c] = type;
  786. }
  787. }
  788. private static String parseQuotedString(ExprBuf buf) {
  789. return ExpressionTokenizer.parseStringUntil(buf, null, QUOTE_CHAR, true);
  790. }
  791. private static void parseQuotedString(ExprBuf buf, StringBuilder sb) {
  792. ExpressionTokenizer.parseStringUntil(buf, null, QUOTE_CHAR, true, sb);
  793. }
  794. private static String parseColorString(ExprBuf buf) {
  795. return ExpressionTokenizer.parseStringUntil(
  796. buf, END_COLOR_CHAR, START_COLOR_CHAR, false);
  797. }
  798. private static void fillInPartialPrefixes() {
  799. List<String> validPrefixes = new ArrayList<>(DATE_FMT_BUILDERS.keySet());
  800. for(String validPrefix : validPrefixes) {
  801. int len = validPrefix.length();
  802. while(len > 1) {
  803. --len;
  804. validPrefix = validPrefix.substring(0, len);
  805. DATE_FMT_BUILDERS.putIfAbsent(validPrefix, PARTIAL_PREFIX);
  806. }
  807. }
  808. }
  809. private static final class PredefDateFmt implements Fmt
  810. {
  811. private final TemporalConfig.Type _type;
  812. private PredefDateFmt(TemporalConfig.Type type) {
  813. _type = type;
  814. }
  815. @Override
  816. public Value format(Args args) {
  817. DateTimeFormatter dtf = args._ctx.createDateFormatter(
  818. args._ctx.getTemporalConfig().getDateTimeFormat(_type));
  819. return ValueSupport.toValue(dtf.format(args.getAsLocalDateTime()));
  820. }
  821. }
  822. private static final class PredefBoolFmt implements Fmt
  823. {
  824. private final Value _trueVal;
  825. private final Value _falseVal;
  826. private PredefBoolFmt(String trueStr, String falseStr) {
  827. _trueVal = ValueSupport.toValue(trueStr);
  828. _falseVal = ValueSupport.toValue(falseStr);
  829. }
  830. @Override
  831. public Value format(Args args) {
  832. return(args._expr.getAsBoolean(args._ctx) ? _trueVal : _falseVal);
  833. }
  834. }
  835. private static abstract class BaseNumberFmt implements Fmt
  836. {
  837. @Override
  838. public Value format(Args args) {
  839. NumberFormat df = getNumberFormat(args);
  840. return ValueSupport.toValue(df.format(args.getAsBigDecimal()));
  841. }
  842. protected abstract NumberFormat getNumberFormat(Args args);
  843. }
  844. private static final class PredefNumberFmt extends BaseNumberFmt
  845. {
  846. private final NumericConfig.Type _type;
  847. private PredefNumberFmt(NumericConfig.Type type) {
  848. _type = type;
  849. }
  850. @Override
  851. protected NumberFormat getNumberFormat(Args args) {
  852. return args._ctx.createDecimalFormat(
  853. args._ctx.getNumericConfig().getNumberFormat(_type));
  854. }
  855. }
  856. private static final class ScientificPredefNumberFmt extends BaseNumberFmt
  857. {
  858. @Override
  859. protected NumberFormat getNumberFormat(Args args) {
  860. NumberFormat df = args._ctx.createDecimalFormat(
  861. args._ctx.getNumericConfig().getNumberFormat(
  862. NumericConfig.Type.SCIENTIFIC));
  863. df = new NumberFormatter.ScientificFormat(df);
  864. return df;
  865. }
  866. }
  867. private static final class NumberFmt extends BaseNumberFmt
  868. {
  869. private final NumberFormat _df;
  870. private NumberFmt(NumberFormat df) {
  871. _df = df;
  872. }
  873. @Override
  874. protected NumberFormat getNumberFormat(Args args) {
  875. return _df;
  876. }
  877. }
  878. private static final class SimpleDFB implements DateFormatBuilder
  879. {
  880. private final String _pat;
  881. private SimpleDFB(String pat) {
  882. _pat = pat;
  883. }
  884. @Override
  885. public void build(DateTimeFormatterBuilder dtfb, Args args) {
  886. dtfb.appendPattern(_pat);
  887. }
  888. }
  889. private static final class PredefDFB implements DateFormatBuilder
  890. {
  891. private final TemporalConfig.Type _type;
  892. private PredefDFB(TemporalConfig.Type type) {
  893. _type = type;
  894. }
  895. @Override
  896. public void build(DateTimeFormatterBuilder dtfb, Args args) {
  897. dtfb.appendPattern(args._ctx.getTemporalConfig().getDateTimeFormat(_type));
  898. }
  899. }
  900. private static abstract class WeekBasedDFB implements DateFormatBuilder
  901. {
  902. @Override
  903. public void build(DateTimeFormatterBuilder dtfb, Args args) {
  904. dtfb.appendValue(getField(DefaultDateFunctions.weekFields(
  905. args._firstDay, args._firstWeekType)));
  906. }
  907. protected abstract TemporalField getField(WeekFields weekFields);
  908. }
  909. private static final class AmPmDFB extends AbstractMap<Long,String>
  910. implements DateFormatBuilder
  911. {
  912. private static final Long ZERO_KEY = 0L;
  913. private final String _am;
  914. private final String _pm;
  915. private AmPmDFB(String am, String pm) {
  916. _am = am;
  917. _pm = pm;
  918. }
  919. @Override
  920. public void build(DateTimeFormatterBuilder dtfb, Args args) {
  921. dtfb.appendText(ChronoField.AMPM_OF_DAY, this);
  922. }
  923. @Override
  924. public int size() {
  925. return 2;
  926. }
  927. @Override
  928. public String get(Object key) {
  929. return(ZERO_KEY.equals(key) ? _am : _pm);
  930. }
  931. @Override
  932. public Set<Map.Entry<Long,String>> entrySet() {
  933. return new AbstractSet<Map.Entry<Long,String>>() {
  934. @Override
  935. public int size() {
  936. return 2;
  937. }
  938. @Override
  939. public Iterator<Map.Entry<Long,String>> iterator() {
  940. return Arrays.<Map.Entry<Long,String>>asList(
  941. new AbstractMap.SimpleImmutableEntry<Long,String>(0L, _am),
  942. new AbstractMap.SimpleImmutableEntry<Long,String>(1L, _pm))
  943. .iterator();
  944. }
  945. };
  946. }
  947. }
  948. private static final class CustomTextFmt implements Fmt
  949. {
  950. private final Fmt _fmt;
  951. private final Fmt _emptyFmt;
  952. private CustomTextFmt(Fmt fmt, Fmt emptyFmt) {
  953. _fmt = fmt;
  954. _emptyFmt = emptyFmt;
  955. }
  956. private static boolean isEmptyString(Args args) {
  957. // only a string value could ever be an empty string
  958. return (args._expr.getType().isString() && args.getAsString().isEmpty());
  959. }
  960. @Override
  961. public Value format(Args args) {
  962. Fmt fmt = _fmt;
  963. if(args._expr.isNull() || isEmptyString(args)) {
  964. fmt = _emptyFmt;
  965. // ensure that we have a non-null value when formatting (null acts
  966. // like empty string in this case)
  967. args._expr = ValueSupport.EMPTY_STR_VAL;
  968. }
  969. return fmt.format(args);
  970. }
  971. }
  972. private static final class CharSourceFmt implements Fmt
  973. {
  974. private final List<BiConsumer<StringBuilder,CharSource>> _subFmts;
  975. private final int _numPlaceholders;
  976. private final boolean _rightAligned;
  977. private final TextCase _textCase;
  978. private CharSourceFmt(List<BiConsumer<StringBuilder,CharSource>> subFmts,
  979. int numPlaceholders, boolean rightAligned, TextCase textCase) {
  980. _subFmts = subFmts;
  981. _numPlaceholders = numPlaceholders;
  982. _rightAligned = rightAligned;
  983. _textCase = textCase;
  984. }
  985. @Override
  986. public Value format(Args args) {
  987. CharSource cs = new CharSource(args.getAsString(), _numPlaceholders, _rightAligned,
  988. _textCase);
  989. StringBuilder sb = new StringBuilder();
  990. _subFmts.stream().forEach(fmt -> fmt.accept(sb, cs));
  991. cs.appendRemaining(sb);
  992. return ValueSupport.toValue(sb.toString());
  993. }
  994. }
  995. private static final class CharSource
  996. {
  997. private int _prefLen;
  998. private final String _str;
  999. private int _strPos;
  1000. private final TextCase _textCase;
  1001. private CharSource(String str, int len, boolean rightAligned,
  1002. TextCase textCase) {
  1003. _str = str;
  1004. _textCase = textCase;
  1005. int strLen = str.length();
  1006. if(len > strLen) {
  1007. if(rightAligned) {
  1008. _prefLen = len - strLen;
  1009. }
  1010. } else if(len < strLen) {
  1011. // it doesn't make sense to me, but the meaning of "right aligned"
  1012. // seems to flip when the string is longer than the format length
  1013. if(!rightAligned) {
  1014. _strPos = strLen - len;
  1015. }
  1016. }
  1017. }
  1018. public int next() {
  1019. if(_prefLen > 0) {
  1020. --_prefLen;
  1021. return NO_CHAR;
  1022. }
  1023. if(_strPos < _str.length()) {
  1024. return _textCase.apply(_str.charAt(_strPos++));
  1025. }
  1026. return NO_CHAR;
  1027. }
  1028. public void appendRemaining(StringBuilder sb) {
  1029. int strLen = _str.length();
  1030. while(_strPos < strLen) {
  1031. sb.append(_textCase.apply(_str.charAt(_strPos++)));
  1032. }
  1033. }
  1034. }
  1035. private static final class CustomNumberFmt implements Fmt
  1036. {
  1037. private final Fmt _posFmt;
  1038. private final Fmt _negFmt;
  1039. private final Fmt _zeroFmt;
  1040. private final Fmt _nullFmt;
  1041. private CustomNumberFmt(Fmt posFmt, Fmt negFmt, Fmt zeroFmt, Fmt nullFmt) {
  1042. _posFmt = posFmt;
  1043. _negFmt = negFmt;
  1044. _zeroFmt = zeroFmt;
  1045. _nullFmt = nullFmt;
  1046. }
  1047. @Override
  1048. public Value format(Args args) {
  1049. if(args._expr.isNull()) {
  1050. return _nullFmt.format(args);
  1051. }
  1052. BigDecimal bd = args.getAsBigDecimal();
  1053. int cmp = BigDecimal.ZERO.compareTo(bd);
  1054. Fmt fmt = ((cmp < 0) ? _posFmt :
  1055. ((cmp > 0) ? _negFmt : _zeroFmt));
  1056. return fmt.format(args);
  1057. }
  1058. }
  1059. }